From b655096e813e4e70fa428a592b22e7040475e1f3 Mon Sep 17 00:00:00 2001 From: Brian Glusman Date: Tue, 12 May 2026 08:53:36 -0400 Subject: [PATCH 1/3] Harden Docker compose gateway exposure --- packaging/docker/README.md | 21 ++++++++++++++++----- packaging/docker/calciforge.env.example | 2 ++ packaging/docker/config.example.toml | 1 + packaging/docker/docker-compose.yml | 6 +++--- scripts/check-packaging.sh | 13 +++++++++++++ 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packaging/docker/README.md b/packaging/docker/README.md index ba7b9b5f..7f3a6672 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -8,6 +8,8 @@ From this directory: ```bash cp calciforge.env.example .env mkdir -p data data-security-proxy data-clashd +openssl rand -base64 32 > data/gateway-api-key +chmod 600 data/gateway-api-key docker compose --env-file .env build calciforge docker compose --env-file .env up -d docker compose --env-file .env exec calciforge \ @@ -29,11 +31,18 @@ to the Compose mounts before starting the container. The sample Compose file uses `/config` for clean trials; live migrations should keep paths stable unless they are intentionally changing layout. -The example starts: +The example publishes each service on `${CALCIFORGE_HOST_BIND:-127.0.0.1}` by default: -- `calciforge` on `${CALCIFORGE_PROXY_PORT:-18792}` -- `security-proxy` on `${CALCIFORGE_SECURITY_PROXY_PORT:-8888}` -- `clashd` on `${CALCIFORGE_CLASHD_PORT:-9001}` +- `calciforge` on `${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_PROXY_PORT:-18792}` +- `security-proxy` on `${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_SECURITY_PROXY_PORT:-8888}` +- `clashd` on `${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_CLASHD_PORT:-9001}` + +Keep the default loopback host binding for local trials. If you intentionally set +`CALCIFORGE_HOST_BIND=0.0.0.0` or another non-loopback address for LAN staging, +first provision a strong gateway key in `data/gateway-api-key` and require clients +to send it as `Authorization: Bearer `. Do not expose the security proxy to +untrusted networks; it is intended for controlled agent egress, not as a public +forward proxy. The Compose file builds the shared `calciforge:local` image through the `calciforge` service and reuses that image for the sidecars. Build the @@ -45,7 +54,9 @@ small staging hosts; increase it only on builders with enough RAM. The default Calciforge config points the model gateway at an OpenAI-compatible service on the host machine at `http://host.docker.internal:11434/v1`, which -matches common Ollama-compatible local testing. Edit `config.example.toml` or set +matches common Ollama-compatible local testing. It also reads the client-facing +gateway bearer token from `/var/lib/calciforge/gateway-api-key`, backed by the +`data/gateway-api-key` file created above. Edit `config.example.toml` or set `CALCIFORGE_CONFIG` before using it for real traffic. Subprocess-backed agents run inside the Calciforge container. If you configure diff --git a/packaging/docker/calciforge.env.example b/packaging/docker/calciforge.env.example index 6245cce6..e101e848 100644 --- a/packaging/docker/calciforge.env.example +++ b/packaging/docker/calciforge.env.example @@ -16,4 +16,6 @@ CALCIFORGE_CLASHD_DATA=./data-clashd CALCIFORGE_PROXY_PORT=18792 CALCIFORGE_SECURITY_PROXY_PORT=8888 CALCIFORGE_CLASHD_PORT=9001 +# Published ports bind to loopback by default. Set to 0.0.0.0 only for intentional LAN exposure. +CALCIFORGE_HOST_BIND=127.0.0.1 RUST_LOG=calciforge=info,security_proxy=info,clashd=info diff --git a/packaging/docker/config.example.toml b/packaging/docker/config.example.toml index 6da5038c..d0a90c30 100644 --- a/packaging/docker/config.example.toml +++ b/packaging/docker/config.example.toml @@ -4,6 +4,7 @@ version = 2 [proxy] enabled = true bind = "0.0.0.0:18792" +api_key_file = "/var/lib/calciforge/gateway-api-key" backend_type = "http" backend_url = "http://host.docker.internal:11434/v1" backend_api_key = "ollama" diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 740a00f0..a372b173 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -11,7 +11,7 @@ services: environment: RUST_LOG: ${RUST_LOG:-calciforge=info} ports: - - "${CALCIFORGE_PROXY_PORT:-18792}:18792" + - "${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_PROXY_PORT:-18792}:18792" volumes: - "${CALCIFORGE_CONFIG:-./config.example.toml}:/config/config.toml:ro" - "${CALCIFORGE_DATA:-./data}:/var/lib/calciforge" @@ -29,7 +29,7 @@ services: SECURITY_PROXY_CONFIG: /config/security-proxy.toml AGENT_CONFIG: /config/agents.json ports: - - "${CALCIFORGE_SECURITY_PROXY_PORT:-8888}:8888" + - "${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_SECURITY_PROXY_PORT:-8888}:8888" volumes: - "${CALCIFORGE_SECURITY_PROXY_CONFIG:-./security-proxy.example.toml}:/config/security-proxy.toml:ro" - "${CALCIFORGE_AGENTS_CONFIG:-./agents.example.json}:/config/agents.json:ro" @@ -47,7 +47,7 @@ services: CLASHD_POLICY: /config/policy.star CLASHD_AGENTS: /config/agents.json ports: - - "${CALCIFORGE_CLASHD_PORT:-9001}:9001" + - "${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_CLASHD_PORT:-9001}:9001" volumes: - "${CALCIFORGE_CLASHD_POLICY:-./policy.example.star}:/config/policy.star:ro" - "${CALCIFORGE_AGENTS_CONFIG:-./agents.example.json}:/config/agents.json:ro" diff --git a/scripts/check-packaging.sh b/scripts/check-packaging.sh index 559fefa4..f841c2a5 100755 --- a/scripts/check-packaging.sh +++ b/scripts/check-packaging.sh @@ -58,6 +58,19 @@ grep -q "calciforge-ollama-switch" "$ROOT/scripts/build-dist-archive.sh" || { exit 1 } +grep -q 'CALCIFORGE_HOST_BIND:-127.0.0.1' "$ROOT/packaging/docker/docker-compose.yml" || { + echo "Docker Compose published ports must default to loopback host binding" >&2 + exit 1 +} +grep -q 'api_key_file = "/var/lib/calciforge/gateway-api-key"' "$ROOT/packaging/docker/config.example.toml" || { + echo "Docker example config must require a client-facing gateway API key file" >&2 + exit 1 +} +grep -q 'openssl rand -base64 32 > data/gateway-api-key' "$ROOT/packaging/docker/README.md" || { + echo "Docker README must provision the sample gateway API key before startup" >&2 + exit 1 +} + installer_shell_files=( "$ROOT/scripts/install.sh" "$ROOT/scripts/clean-install-reset.sh" From 8d01fddbf1871ca679b35ffb75b7826a81c49441 Mon Sep 17 00:00:00 2001 From: Brian Glusman Date: Tue, 12 May 2026 09:29:41 -0400 Subject: [PATCH 2/3] test: tighten Docker compose loopback guard --- scripts/check-packaging.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/check-packaging.sh b/scripts/check-packaging.sh index f841c2a5..72e08016 100755 --- a/scripts/check-packaging.sh +++ b/scripts/check-packaging.sh @@ -58,10 +58,17 @@ grep -q "calciforge-ollama-switch" "$ROOT/scripts/build-dist-archive.sh" || { exit 1 } -grep -q 'CALCIFORGE_HOST_BIND:-127.0.0.1' "$ROOT/packaging/docker/docker-compose.yml" || { - echo "Docker Compose published ports must default to loopback host binding" >&2 - exit 1 -} +expected_compose_port_bindings=( + '"${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_PROXY_PORT:-18792}:18792"' + '"${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_SECURITY_PROXY_PORT:-8888}:8888"' + '"${CALCIFORGE_HOST_BIND:-127.0.0.1}:${CALCIFORGE_CLASHD_PORT:-9001}:9001"' +) +for binding in "${expected_compose_port_bindings[@]}"; do + grep -Fq "$binding" "$ROOT/packaging/docker/docker-compose.yml" || { + echo "Docker Compose published port must default to loopback host binding: $binding" >&2 + exit 1 + } +done grep -q 'api_key_file = "/var/lib/calciforge/gateway-api-key"' "$ROOT/packaging/docker/config.example.toml" || { echo "Docker example config must require a client-facing gateway API key file" >&2 exit 1 From 4e380f5656e86611785c2b9ad5e3a4bf1c532b6b Mon Sep 17 00:00:00 2001 From: Brian Glusman Date: Tue, 12 May 2026 09:25:08 -0400 Subject: [PATCH 3/3] test: tighten docker compose bind guardrail --- scripts/check-packaging.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/check-packaging.sh b/scripts/check-packaging.sh index 72e08016..2543b74e 100755 --- a/scripts/check-packaging.sh +++ b/scripts/check-packaging.sh @@ -69,6 +69,11 @@ for binding in "${expected_compose_port_bindings[@]}"; do exit 1 } done +actual_loopback_ports="$(grep -Fc 'CALCIFORGE_HOST_BIND:-127.0.0.1' "$ROOT/packaging/docker/docker-compose.yml")" +if [[ "$actual_loopback_ports" != "${#expected_compose_port_bindings[@]}" ]]; then + echo "Docker Compose should have ${#expected_compose_port_bindings[@]} loopback-bound published ports, found $actual_loopback_ports" >&2 + exit 1 +fi grep -q 'api_key_file = "/var/lib/calciforge/gateway-api-key"' "$ROOT/packaging/docker/config.example.toml" || { echo "Docker example config must require a client-facing gateway API key file" >&2 exit 1