Approved design spec for BE-1597 — expose the mando-bess DuckDB UI to developers via an SSM port-forward through the bastion, and (Phase 2) allow a remote client to attach to the in-memory DuckDB via Quack. This is the actionable decision/spec note to execute from (owner: Gergely “Geri” Vászon); the verified evidence behind every claim lives in BE-1597 DuckDB UI Exposure Research and is not duplicated here.

For Agents — decision summary

  • Status: design approved, not implemented. Repos: mando, optimization-universe-iac.
  • Phased. Approach 1 (dev only) now: SSM AWS-StartPortForwardingSessionToRemoteHost forwards local port 4213 through the bastion to the task ENI; browse http://localhost:4213. No ALB, no reverse proxy, no bundle patching. Approach 2 (Quack, all envs) waits for DuckDB 2.0, September 2026.
  • Never run the DuckDB UI in int/prod — its frontend is proprietary MotherDuck JS served live with no auth (see §3).
  • Browse localhost, NOT 127.0.0.1 — the bundle string-compares against the literal "localhost:4213" (see §4.3).
  • Evidence for every claim: BE-1597 DuckDB UI Exposure Research.

BE-1597 — Expose DuckDB UI via bastion host

Status: design approved, not implemented Date: 2026-07-10 Decision: phased — Approach 1 (dev only) now; Quack (all envs) on DuckDB 2.0 in September 2026 Repos: mando, optimization-universe-iac

1. The ticket, and why its premise is wrong

Ticket text: “Make duckdb ui available via bastion host. Challenge: let it through load balancer but only for bastion host. Also investigate how to remotely connect with a client to a running in-memory instance.”

The load balancer must not be involved. aws_lb.bess_os_service is already internal = true. It was never the obstacle, and chasing it is what killed both previous attempts.

The actual obstacle: the DuckDB UI’s JS bundle hardcodes "localhost:4213" === window.location.host ? "duckdb_ui" : "web". Serve it on any other origin and it silently degrades to a broken web mode. No ALB path prefix and no custom hostname can ever satisfy that check.

The fix is a port number. SSM’s AWS-StartPortForwardingSessionToRemoteHost lets the developer choose the LOCAL port. Forward local 4213, browse to http://localhost:4213, and the origin check passes for free. No listener rule, no target group, no health check, no proxy, no bundle patching.

A bastion is genuinely required, but not for the reason the ticket implies: ECS Exec supports only interactive commands, not port forwarding, so the SSM hop must land on an EC2 instance that can reach the task ENI.

2. Prior art (both abandoned, nothing merged)

  • feature/BE-1597-enable-duckdb-ui (iac): socat 4214 + dedicated target group + listener rule path_pattern = ["/duckdb*"]. Target group health-checked port 8080 /health; commits literally named temp: disable healthckeck for duckdb. Path prefix breaks the UI’s absolute asset paths.
  • feature/BE-1597-proxy-duckdbUI-thorugh-mando (mando, 0e06c93e): mando-bess mando-bess/src/route/duckdb_proxy.rs, a reqwest reverse proxy. Required rewriting <base href> and string-patching the JS bundle to defeat the origin check. Never productionized.
  • Also stale: feature/BE-1597, feature/BE-1597-mando-feature-deploy, feature/expose-duckdb-ui (iac), feature/BE-1597-add-tunnel-for-duckDB-UI / c700fee9 (mando). Authors: Andras Lederer, Gergely Vászon. March 2026.

duckdb_proxy.rs is abandoned, not revived.

Root-cause evidence

Full write-up of why both attempts were un-winnable (the hardcoded origin check, the health-check failure, the JS-bundle patching) is in Why the two previous attempts failed (root cause).

3. Why the UI cannot go to int/prod

License blocker — the DuckDB UI frontend is proprietary MotherDuck code

The ui extension is NOT statically linked into the vendored libduckdb. Verified: nm -D lib/libduckdb/1.4.2/linux-amd64/libduckdb.so shows only DuckDB::LoadStaticExtension<IcuExtension> and <JsonExtension>; no UiExtension.

So CALL start_ui_server() auto-downloads the extension from extensions.duckdb.org, and the extension then proxies the frontend assets from ui.duckdb.org on every session.

Those assets are proprietary MotherDuck code with no published license. DuckDB’s own launch post: “The repository does not contain the source code for the frontend, which is currently not available as open-source.” MotherDuck said in March 2025 they were reviewing licensing options; as of July 2026 nothing has been published. The UI docs warn: “Be sure you trust any URL you configure, as the application can access the data you load into DuckDB.”

You cannot mitigate by mirroring: ui_remote_url is only honoured when allow_unsigned_extensions is enabled, which disables extension signature verification globally.

Net effect in int_prod: an unauthenticated arbitrary-SQL console, served by unversioned unreviewable third-party JavaScript, with full access to the live trading cache. Fine on a developer’s machine or a dev container — that is the vendor’s intended usage. Not acceptable in the trading service.

Everything else in the supply chain is clean: DuckDB core, prebuilt libduckdb, duckdb-rs, libduckdb-sys, the ui EXTENSION SOURCE, quack, ICU (Unicode-3.0), yyjson — all MIT/permissive, unchanged through 2025–2026, safe to vendor closed-source with a NOTICE bundle.

Evidence: License finding (the important one).

4. Phase 1 — dev only (now)

4.0 Blocking pre-check: does the task have egress?

No NAT gateway or VPC endpoints exist in optimization-universe-iac; the VPC is a data lookup on tag base-vpc, owned by another team. If egress to duckdb.org is blocked, Phase 1 cannot work and no terraform change fixes it. Verify before writing code:

aws ssm start-session --target "$(aws ec2 describe-instances \
  --filters Name=tag:Name,Values=bess-os-bastion Name=instance-state-name,Values=running \
  --query 'Reservations[0].Instances[0].InstanceId' --output text)"
 
curl -sI https://extensions.duckdb.org | head -1
curl -sI https://ui.duckdb.org | head -1

Known weakness: the bastion sits in private.ids[0] while tasks span all private subnets, so this proves the route table’s egress, not the task’s exactly. The runtime image has no curl (only ca-certificates, libc6, libssl3), so a task-exact check is not possible until the new image ships.

4.1 Container

The UI binds IPv6 loopback ::1:4213 and has no bind-address setting (the only settings are ui_local_port, ui_remote_url, ui_polling_interval). Something must bridge it onto the task ENI.

Dockerfile:

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates libc6 libssl3 socat \
    && rm -rf /var/lib/apt/lists/*
 
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

docker-entrypoint.sh:

#!/bin/sh
set -e
 
if [ "${DUCK_DB_UI_SERVER:-}" = "true" ]; then
    socat TCP4-LISTEN:4214,fork,reuseaddr TCP6:[::1]:4213 &
fi
 
exec mando_bess

Notes:

  • The old branch polled in a loop waiting for [::1]:4213 before starting socat. Unnecessary — TCP-LISTEN,fork binds immediately and only dials its target on accept(), by which time the UI is up. Deleting the loop deletes the race.
  • exec keeps mando_bess as PID 1 so ECS’s SIGTERM reaches it.
  • NO Rust changes. DUCK_DB_UI_SERVER is already read in mando-bess at mando-bess/src/lib.rs:181 (DuckDb mode) and :301 (the DuckDbPostgres path prod uses).
  • No EXPOSE 4214 and no second portMappings entry. In awsvpc mode both are documentation only; the task ENI accepts any port the SG allows. The old branch added both to no effect.
  • Accepted tradeoff: socat ships in the prod image although the listener never starts there (~400 KB). The risk is the listener, not the binary. A dev-only image variant would cost a second image to build, tag and keep in sync.

4.2 Terraform

security_group.tfbess_os_ecs uses INLINE ingress blocks (lines 9–15). A standalone aws_vpc_security_group_ingress_rule is silently reaped on the next apply, so the rule must live inside the resource:

  dynamic "ingress" {
    for_each = var.environment == "dev" ? [1] : []
 
    content {
      description     = "DuckDB UI via bastion SSM port-forward"
      from_port       = 4214
      to_port         = 4214
      protocol        = "tcp"
      security_groups = [aws_security_group.bastion.id]
    }
  }

This references the bastion security group, not the VPC CIDR the existing 8080–8081 rule uses. That is the ticket’s “only for bastion host,” and it is stricter than anything the ALB route could have given. No dependency cycle: bastion has egress only and never references bess_os_ecs.

bess_os_ecs.tf — one entry in the mando container’s environment array:

{ name = "DUCK_DB_UI_SERVER", value = tostring(var.environment == "dev") },

var.environment, not local.environment_group: the group collapses int+prod and dev+test, so it is the wrong knob for a dev-only gate.

4.3 Developer workflow

optimization-universe-iac/scripts/duckdb-ui.sh:

#!/usr/bin/env bash
set -euo pipefail
 
cluster=bess-os-cluster
service=bess-os-service
 
bastion=$(aws ec2 describe-instances \
    --filters Name=tag:Name,Values=bess-os-bastion Name=instance-state-name,Values=running \
    --query 'Reservations[0].Instances[0].InstanceId' --output text)
 
task=$(aws ecs list-tasks --cluster "$cluster" --service-name "$service" \
    --query 'taskArns[0]' --output text)
 
ip=$(aws ecs describe-tasks --cluster "$cluster" --tasks "$task" \
    --query "tasks[0].attachments[0].details[?name=='privateIPv4Address'].value | [0]" \
    --output text)
 
echo "open http://localhost:4213 — localhost, not 127.0.0.1"
 
aws ssm start-session --target "$bastion" \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters "{\"host\":[\"$ip\"],\"portNumber\":[\"4214\"],\"localPortNumber\":[\"4213\"]}"

The task IP resolves on every run: desired_count = 1 and Fargate re-IPs on each deploy. Developers need session-manager-plugin installed locally.

Gotcha: browse localhost, never 127.0.0.1

The bundle compares against the literal string "localhost:4213". http://127.0.0.1:4213 fails the origin check and silently degrades the UI to broken web mode. This detail is the entire ticket.

4.4 Verification

The socat bridge is the only non-trivial logic and is testable without AWS. The local compose setup in optimization-universe-iac/local/ already maps 4213:4214. Bring it up with DUCK_DB_UI_SERVER=true, then confirm curl -sf http://localhost:4214/ >/dev/null succeeds and the browser renders the UI at http://localhost:4213. If that passes, dev adds only the SSM hop and the SG rule. (For the broader local mando stack, see Local Docker Setup.)

4.5 Explicitly not built

No ALB listener rule. No target group. No health check. No /duckdb* path prefix. No reverse proxy in mando-bess. No <base href> rewriting. No JS bundle patching.

5. Phase 2 — Quack, all envs (September 2026, on DuckDB 2.0)

DuckDB has no wire protocol before 1.5.3, so there is no way to attach a remote client to a running in-memory instance today. The ticket’s second half is unanswerable on 1.4.2.

Quack (announced 2026-05-12, shipped in 1.5.3 on 2026-05-20, https://duckdb.org/2026/05/12/quack-remote-protocol) is a core, DuckDB-signed, MIT extension. It does not need allow_unsigned_extensions, autoinstalls and autoloads, and can be pre-installed at Docker build time into an extension_directory for airgapped operation.

  • Server: CALL quack_serve('quack:0.0.0.0:9494', allow_other_hostname => true) returns an auth_token
  • Client: ATTACH 'quack:host:9494' AS cache (TOKEN '...'), or FROM quack_query(...)
  • Works against an in-memory instance. Full DuckDB feature set over HTTP, concurrent readers and writers.

Shape: gate quack_serve behind an env var, SG ingress 9494 from the bastion SG, SSM port-forward, then run duckdb -ui ON THE LAPTOP and ATTACH the remote. Token auth instead of none. Nothing fetched from a third-party CDN by prod. The UI runs where MotherDuck’s JS is already trusted — the developer’s own machine. This also satisfies the ticket’s second half for any DuckDB client, not just the UI.

Why September and not now: Quack is Beta in 1.5.x — “the protocol, the function names, and implementation details are all subject to change.” It stabilizes in DuckDB 2.0, September 2026, the same month DuckDB 1.4.x LTS reaches EOL. Bumping now means two upgrades and a possible protocol change between them. Waiting means one.

6. Upgrade assessment: 1.4.2 → 2.0

Releases since 1.4.2: 1.4.3 (2025-12-09 LTS), 1.4.4 (2026-01-26 LTS), 1.5.0 “Variegata” (2026-03-09), 1.5.1, 1.5.2, 1.5.3 (2026-05-20, quack), 1.5.4 (2026-06-17). 1.4.x LTS EOL September 2026.

Verified safe for mando:

  • C API is additive across 1.4.3–1.5.4; no removed or renamed symbols. Regenerate bindings when bumping the vendored .so.
  • In-memory semantics unchanged: DuckdbConnectionManager::memory() holds one Connection::open_in_memory() and connect() returns try_clone(), so every pooled r2d2 connection shares the same in-memory DB. Exactly what repo_duckdb.rs relies on.
  • Arrow appender unchanged; appender-arrow, r2d2, chrono, serde_json, uuid features all still exist.
  • ICU and JSON remain statically linked in official distributions.
  • Storage format churn is irrelevant — mando is in-memory only (mando-bess/src/lib.rs:177,246).
  • 1.5.0 breaking change date_trunc(DATE) now returns TIMESTAMP: mando’s only date_trunc uses are in Postgres migrations under mando-bess/database/2025_Q4/, all passing current_timestamp. Unaffected.
  • 1.5.0 deprecated -> lambda syntax: mando uses no list_transform / list_filter / list_reduce. Unaffected.
  • Toolchain: the 1.5.x crate needs rustc >= 1.85.1 / edition 2024. rust-toolchain.toml pins 1.89.0. Fine.

Traps:

  • duckdb-rs changed its version scheme at 1.5.0 to 1.MAJOR_MINOR_PATCH.x. DuckDB 1.5.4 is published as crate 1.10504.0. A caret range would silently resolve to it. Cargo.toml:60 pins =1.4.2 exactly, so mando is currently safe — keep the exact pin or use ~.
  • Vendored libs must be re-downloaded for all three platforms: libduckdb-linux-amd64.zip, libduckdb-osx-universal.zip, libduckdb-windows-amd64.zip. DEFAULT_LIB_DIR consts live in mando-lib mando-lib/build.rs and py-mando/build.rs, plus the COPY in Dockerfile.

Full timeline, crate-scheme detail and per-change verification: Upgrade assessment: 1.4.2 → 1.5.4.

7. Open questions

  1. Does the dev Fargate task have egress to extensions.duckdb.org and ui.duckdb.org? Blocking for Phase 1. See 4.0. Owner: whoever owns base-vpc.
  2. Is the bastion-subnet egress check an acceptable proxy for task egress, or do we need the image shipped first to test exactly?
  3. Should socat ship in the prod image, or do we build a dev-only variant? Current design: ship it.
  4. Phase 2 assumes DuckDB 2.0 lands in September with Quack stable. If it slips, revisit — bump to 1.5.x with Beta Quack, or hold on 1.4.5 LTS past EOL?

8. Reference

  • Bastion: optimization-universe-iac/terraform/bastion.tf — t3.medium, private subnet, no public IP, SSM only, SG egress-all + zero ingress
  • ALB: terraform/bess_os_load_balancer.tf — internal = true, 443 only, single TG on 8080
  • ECS: terraform/bess_os_ecs.tf — Fargate, desired_count = 1, enable_execute_command = var.environment == "dev" (line 499); container bess-os-service-mando, cluster bess-os-cluster, service bess-os-service
  • SG: terraform/security_group.tf:4-23 — inline ingress blocks
  • Env mapping: terraform/main.tf:5-6 — int and prod both map to int_prod
  • DuckDB usage: mando-lib mando-lib/src/repo_duckdb.rs, mando-bess mando-bess/src/lib.rs:177,246 (:memory:), Cargo.toml:60
  • DuckDB UI extension docs: https://duckdb.org/docs/stable/core_extensions/ui
  • UI launch post: https://duckdb.org/2025/03/12/duckdb-ui
  • Quack announcement: https://duckdb.org/2026/05/12/quack-remote-protocol
  • BE-1597 DuckDB UI Exposure Research — the evidence note: verified root cause of both prior attempts, the origin check, UI-extension constraints, the full license finding, Quack details, and the 1.4.2 → 1.5.4 upgrade assessment. This spec deliberately does not duplicate that evidence.
  • mando-bess — the service hosting the in-memory DuckDB / DuckDB UI; reads DUCK_DB_UI_SERVER (src/lib.rs:181,301), :memory: at :177,246; the abandoned duckdb_proxy.rs lived here.
  • mando-librepo_duckdb.rs + the vendored lib/libduckdb/1.4.2; DEFAULT_LIB_DIR in build.rs.
  • Agent Context — DuckDB 1.4.2 in the tech stack; in-memory repository pattern.
  • Alpiq BESS — project overview.
  • Local Docker Setup — running the mando stack locally (where the DuckDB UI is unproblematic).