Skip to content

feat(security): Docker allow-list proxy, isolated Constellation network, key-based SSH, Prefect auth plumbing - #54

Merged
danielroberts20 merged 4 commits into
mainfrom
security/pi-hardening
Oct 7, 2026
Merged

danielroberts20 merged 4 commits into
mainfrom
security/pi-hardening

Conversation

@danielroberts20

Copy link
Copy Markdown
Owner

Summary

Hardening pass over the Pi stack. Every change below was tested in isolation first (throwaway containers or an in-process SSH server), and each is backward compatible so the rollout can be staged.

Change What it does
Docker allow-list proxy The dashboard no longer mounts /var/run/docker.sock (:ro does not limit the Docker API, so it was root on the host). A small HAProxy proxy (server/docker-proxy/haproxy.cfg, 8.6 MB, read-only, all capabilities dropped) permits only what the dashboard does: follow the logs of the API and Trevor, restart the API, Trevor and the dashboard. Listing, exec, create, rm and any other container are refused with 403.
Constellation on its own network Constellation (public uploads and media) shared the travelnet network with the API, Prefect and the dashboard. It now sits on a constellation network shared only with nginx; it reaches TravelNet through the public hostname like any outside client.
nginx re-resolves upstreams A literal host in proxy_pass is resolved once at start-up, so a recreated container with a new IP was a 502 until a reload. Reproduced and fixed (resolver with a 10 s TTL); this also makes the network move above safe.
Key-based SSH to the compute PC COMPUTE_SSH_KEY_PATH is tried first, the password only as a fallback, so it can be migrated without a flag day; COMPUTE_PASSWORD becomes optional. With COMPUTE_KNOWN_HOSTS_PATH the host key is pinned (unknown or changed keys refused before credentials are sent). Tested against a real in-process SSH server.
Prefect auth plumbing The dashboard receives PREFECT_API_AUTH_STRING; the prefect-server.service draft reads the server's copy from a root-only env file. The Prefect server enforces HTTP Basic auth while keeping /api/health open (verified on the pinned server image).
Reboot script tests The credential reaches curl on stdin, never as an argument.
Health monitor Watches the new proxy container.

Rollout order (important)

  1. Clients first (credentials are ignored while the server is open): this PR plus Dashboard and Trevor PRs, then docker compose up -d.
  2. Then enable auth on the Prefect server (env file + unit restart).
  3. Network move: bring up TravelNet's nginx (creates the constellation network) before recreating Constellation.

Companion PRs: Dashboard (Prefect credentials), Trevor (key-based SSH), Constellation (network).

🤖 Generated with Claude Code

danielroberts20 and others added 4 commits October 7, 2026 09:45
…rk, dynamic nginx upstreams

- The dashboard no longer mounts /var/run/docker.sock (":ro" does not limit the Docker
  API, so that was root on the host). It uses a small HAProxy allow-list proxy
  (server/docker-proxy/haproxy.cfg) that permits only what the dashboard does:
  follow the logs of the API and Trevor, and restart the API, Trevor and the dashboard.
  Everything else (list, exec, create, rm, other containers) is refused with 403.
  Tested against a disposable container, including under the proxy's restricted runtime
  (read-only, all capabilities dropped, no-new-privileges, 32 MB).
- Constellation, the most exposed app, moves to its own `constellation` network shared
  only with nginx, so it can no longer reach the API, Prefect or the dashboard over the
  Docker network. It reaches TravelNet through the public hostname already.
- nginx re-resolves its upstreams through Docker's DNS every 10 s. A literal host in
  proxy_pass is resolved once at start-up, so a recreated container with a new IP was
  a 502 until a reload (reproduced and fixed in a throwaway test).
- The dashboard receives PREFECT_API_AUTH_STRING for the upcoming Prefect server auth.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The API reached the compute PC with a password and trusted any host key
(AutoAddPolicy). get_ssh_client() now:
- offers the private key at COMPUTE_SSH_KEY_PATH first and the password only as a
  fallback (paramiko's order), so the key can be rolled out before the password is
  removed; COMPUTE_PASSWORD is now optional;
- pins the host key when COMPUTE_KNOWN_HOSTS_PATH points at a known_hosts file, refusing
  unknown or changed keys before any credentials are sent; without it the previous
  trust-on-first-use behaviour is unchanged;
- no longer tries the ssh-agent or default key locations, and fails with a clear error if
  no credentials are configured at all.

Tested with mocks and against a real in-process SSH server (key auth, password fallback,
impostor host key, host missing from known_hosts, no pinning).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…e Docker proxy

- ingest mounts the compute key directory (a directory, so a missing key can never turn
  into a root-owned directory created by Docker) at /run/secrets/compute.
- .env.example documents COMPUTE_SSH_KEY_PATH, COMPUTE_KNOWN_HOSTS_PATH and
  PREFECT_API_AUTH_STRING (commented out: CI copies this file to .env).
- the host health monitor now expects the travelnet-docker-proxy container.
- prefect-server.service draft reads its auth string from a root-only env file instead of
  putting the credential in the world-readable unit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…n stdin

graceful_reboot.sh (gitignored) polls the Prefect API for in-flight flow runs, which now
needs HTTP Basic credentials. They must reach curl as a config line on stdin ("-K -"),
never as an argument, so they cannot be read from the process list. Tests assert both
that the credential is delivered and that it never appears in an argument.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying travelnet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 44993d3
Status: ✅  Deploy successful!
Preview URL: https://de08f244.travelnet.pages.dev
Branch Preview URL: https://security-pi-hardening.travelnet.pages.dev

View logs

@danielroberts20
danielroberts20 merged commit 928165b into main Oct 7, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant