Skip to main content

Hardening a Circus Server

The initial setup deliberately grants broad access — sudo, password auth — to keep configuration simple. Once the server is configured and the first deploy is verified, harden it. Do the steps in order: if you disable password auth before confirming key auth works, you can lock yourself out and need console access to recover.

Lock down SSH

  1. Verify key auth first. ssh -i circus_deploy_key deploy@YOUR_IP "echo ok" must succeed before you change anything.
  2. Disable root login — set PermitRootLogin no in sshd_config and restart sshd; confirm ssh root@YOUR_IP is rejected.
  3. Disable password authentication — set PasswordAuthentication no and restart; confirm a password-only attempt is rejected.
  4. (Optional) remove sudo from deploy — CI needs only Docker access. Removing sudo tightens the blast radius, but then server maintenance needs a separate admin user or console. Skip this if you're the only operator and rely on sudo for upkeep.

Firewall

Default-deny inbound and open only what Caddy and SSH need:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (Caddy redirect + ACME)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable

Note that Postgres (5432) is deliberately not opened — the container publishes no host port at all, so it's reachable only over the Docker network or via docker compose exec on the box itself.

Supply-chain protection

The platform enforces a 30-day npm package-age policy via .npmrc (min-release-age=30) — no dependency published in the last 30 days can be installed, blunting supply-chain attacks that rely on rapid uptake of a malicious release. If you develop plugins and add dependencies, also consider installing Socket on the repo; it flags suspicious package changes (typosquatting, install scripts, obfuscated code) on PRs.

Known limitations (accepted tradeoffs)

This single-host, demo-oriented model accepts some risks — worth documenting, worth revisiting before serving real clients:

  • Secrets sit in plain text in .env / n8n.env on disk (mitigation: key-only SSH + firewall; for real production, a secrets manager).
  • GHCR_TOKEN passes through the SSH session during docker login (mitigation: rotate periodically).
  • Postgres trusts local connections — any process on the box can exec into it without a password (mitigation: restricted server access).
  • Staging and production share one host — acceptable because production here is a demo, not a client system; use separate servers for real production.