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-StartPortForwardingSessionToRemoteHostforwards local port 4213 through the bastion to the task ENI; browsehttp://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, NOT127.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 rulepath_pattern = ["/duckdb*"]. Target group health-checked port 8080/health; commits literally namedtemp: disable healthckeck for duckdb. Path prefix breaks the UI’s absolute asset paths.feature/BE-1597-proxy-duckdbUI-thorugh-mando(mando, 0e06c93e): mando-bessmando-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
uiextension is NOT statically linked into the vendored libduckdb. Verified:nm -D lib/libduckdb/1.4.2/linux-amd64/libduckdb.soshows onlyDuckDB::LoadStaticExtension<IcuExtension>and<JsonExtension>; noUiExtension.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_urlis only honoured whenallow_unsigned_extensionsis 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 -1Known 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_bessNotes:
- The old branch polled in a loop waiting for
[::1]:4213before starting socat. Unnecessary —TCP-LISTEN,forkbinds immediately and only dials its target on accept(), by which time the UI is up. Deleting the loop deletes the race. execkeepsmando_bessas PID 1 so ECS’s SIGTERM reaches it.- NO Rust changes.
DUCK_DB_UI_SERVERis already read in mando-bess atmando-bess/src/lib.rs:181(DuckDb mode) and:301(the DuckDbPostgres path prod uses). - No
EXPOSE 4214and 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.tf — bess_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, never127.0.0.1The bundle compares against the literal string
"localhost:4213".http://127.0.0.1:4213fails the origin check and silently degrades the UI to brokenwebmode. 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 '...'), orFROM 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 oneConnection::open_in_memory()andconnect()returnstry_clone(), so every pooled r2d2 connection shares the same in-memory DB. Exactly whatrepo_duckdb.rsrelies 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 undermando-bess/database/2025_Q4/, all passingcurrent_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 crate1.10504.0. A caret range would silently resolve to it.Cargo.toml:60pins=1.4.2exactly, 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.rsandpy-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
- 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.
- Is the bastion-subnet egress check an acceptable proxy for task egress, or do we need the image shipped first to test exactly?
- Should socat ship in the prod image, or do we build a dev-only variant? Current design: ship it.
- 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); containerbess-os-service-mando, clusterbess-os-cluster, servicebess-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-bessmando-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
Related
- 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 abandonedduckdb_proxy.rslived here. - mando-lib —
repo_duckdb.rs+ the vendoredlib/libduckdb/1.4.2;DEFAULT_LIB_DIRinbuild.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).