Skip to content

Production Hardening Checklist

Work through this checklist top-to-bottom before exposing a FreeSDN instance to the network. Each section describes the requirement, the relevant environment variable or setting, and how to verify it.


FreeSDN refuses to start in production or staging if any of the following are weak or missing. Set all of them before your first docker compose up.

Terminal window
# Generate strong values - do NOT reuse these examples
python -c "import secrets; print(secrets.token_hex(32))" # for SECRET_KEY
python -c "import secrets; print(secrets.token_hex(24))" # for ENCRYPTION_SALT
VariableRequirementNotes
SECRET_KEY≥ 32 characters; not the default stringSigns JWT tokens. Rotation invalidates all sessions.
ENCRYPTION_SALTNon-default valueDerives the Fernet encryption key for stored credentials.
POSTGRES_PASSWORDStrong, non-defaultBackend database password.
LOGDB_URLMust be setConnection string for TimescaleDB.
REDIS_PASSWORDMust be setValkey/Redis broker and cache password.

The minimum user-account password length is 12 characters.


FreeSDN uses Caddy as the default edge, providing automatic HTTPS. Set CADDY_SITE_ADDRESS:

ValueResult
:80Plain HTTP - only when running behind a TLS-terminating load balancer
localhostLocal TLS via Caddy’s internal CA
your.domain.comAutomatic Let’s Encrypt certificate
# .env.pro (copy from .env.pro.example and fill in secrets)
CADDY_SITE_ADDRESS=sdn.example.com

All internal data-tier services (Postgres, Valkey, LogDB) have no host-port bindings - they are accessible only within the internal freesdn-network Docker network and are unreachable from the public network.


3. API docs are unconditionally off in production

Section titled “3. API docs are unconditionally off in production”

API docs (/api/v1/docs, /api/v1/redoc) are disabled unconditionally when ENVIRONMENT=production. The application gate is settings.ENABLE_DOCS and settings.ENVIRONMENT != "production" - the environment check cannot be overridden by any value of ENABLE_DOCS. Setting ENABLE_DOCS=1 on a production instance has no effect on docs availability.

ENABLE_DOCS is only meaningful in non-production environments (development, staging). To suppress docs there as well, set ENABLE_DOCS=false.

Terminal window
# Verify docs are off - expect 404
curl -sf https://your.domain/api/v1/docs

The /metrics endpoint is not served at all in production or staging unless METRICS_AUTH_TOKEN is set (fail-closed, CAN-019). Set it to enable authenticated Prometheus scraping:

Terminal window
METRICS_AUTH_TOKEN=$(python -c "import secrets; print(secrets.token_hex(32))")

Configure your Prometheus scraper to send Authorization: Bearer <token>. Without this token, the route is never mounted and no telemetry is accessible. Note: in the development environment only, the endpoint is exposed without auth for local convenience.


Pin the publisher signing key before enabling the marketplace. The platform refuses unsigned catalogs by default:

Terminal window
# Generate a key pair for your private registry
python scripts/sign_marketplace_catalog.py keygen
# Set the public key in your environment
# Generate with: python scripts/sign_marketplace_catalog.py keygen
# The script outputs PUBLIC_KEY_HEX=<hex>. Paste that hex value here.
MARKETPLACE_PUBLISHER_PUBLIC_KEY=<hex-encoded-ed25519-public-key>

For a private registry where you control all plugins, MARKETPLACE_ALLOW_UNSIGNED=1 is acceptable. For public-facing or multi-tenant environments, always pin the key.


6. Adapter write gate - keep ADAPTER_READ_ONLY on until you are ready

Section titled “6. Adapter write gate - keep ADAPTER_READ_ONLY on until you are ready”

The adapter staging system requires both ADAPTER_READ_ONLY=false and force=true to push changes to live devices. The default is ADAPTER_READ_ONLY=true.

Terminal window
# Only set this after reviewing your adapter config and staged changes:
ADAPTER_READ_ONLY=false

The pg-backup service GPG-encrypts both the primary database and LogDB before writing backup archives. In production it fails closed if no GPG recipient is configured:

Terminal window
BACKUP_GPG_RECIPIENT=operator@example.com # GPG key in the keyring
BACKUP_RETENTION_DAYS=7 # prune older archives

Without a GPG recipient, backups do not run in production. Test your backup and restore path before going live.

For off-site DR, enable the dr profile and configure rclone:

Terminal window
docker compose --env-file .env.pro --profile dr up -d

Rate limiting is on by default:

LimitDefault
General API (per principal)600 requests/min (RATE_LIMIT_RPM)
Auth endpoints (/auth/*)5 requests/min per IP (hardcoded)
Per-principal burst120 requests/second (RATE_LIMIT_BURST)

If you run behind a load balancer, set FORWARDED_ALLOW_IPS (see section 2). Without it, the rate limiter sees your proxy’s IP as every client’s IP, defeating per-client limits.


For the strongest auto-update posture, bake the backend’s release signing key fingerprint into each Agent at install time:

Terminal window
# Get the fingerprint from your backend
curl -s https://your.domain/api/v1/agents/releases/public-key
# Set in agent config:
release_public_key_sha256=<sha256-of-public-key>

Without this, the Agent uses trust-on-first-use (it pins the key on first successful update verification). TOFU is acceptable; the explicit pin is stronger because it rejects a server-side key-swap.


User self-registration is disabled by default. Enable it only for a multi-tenant SaaS-style deployment:

Terminal window
ALLOW_REGISTRATION=true # off by default

In the default configuration, only administrators can create accounts.


Require MFA for all elevated roles. Go to Settings → Security → MFA Policy and enable it for operator and above. Users without MFA enrolled are prompted to enroll on their next login.


The production Compose configuration applies the following to all containers:

  • no-new-privileges: true - prevents privilege escalation inside containers.
  • Non-root runtime users (appuser for the backend; nginx for the nginx edge escape-hatch profile). The default Caddy edge runs as the upstream Caddy image default user.
  • Read-only filesystems on stateless containers (Valkey; the optional nginx edge profile). The default Caddy edge container does not set a read-only rootfs because Caddy writes cert/ACME state to its mounted volumes.
  • Per-container CPU and memory resource limits.
  • Log rotation to prevent disk exhaustion.

Review these settings if you use a custom docker-compose.override.yml.


Plugin installation is restricted to super_admin. Before installing any plugin:

  1. Review the plugin’s source code and manifest.
  2. Confirm the declared permissions match the plugin’s stated purpose.
  3. Treat the install as deploying a Python package into your production environment - that is the accurate mental model.

  • SECRET_KEY set to a strong random value (≥ 32 chars)
  • ENCRYPTION_SALT set to a non-default value
  • POSTGRES_PASSWORD, LOGDB_URL, REDIS_PASSWORD set
  • TLS configured via CADDY_SITE_ADDRESS
  • FORWARDED_ALLOW_IPS set if behind a proxy
  • ENABLE_DOCS not set (defaults off in production)
  • METRICS_AUTH_TOKEN set
  • MARKETPLACE_PUBLISHER_PUBLIC_KEY set (or MARKETPLACE_ALLOW_UNSIGNED=1 for private dev)
  • ADAPTER_READ_ONLY reviewed - leave true until you are ready for live writes
  • BACKUP_GPG_RECIPIENT configured and backup/restore path tested
  • Agent release_public_key_sha256 pinned at install time
  • User self-registration (ALLOW_REGISTRATION) disabled unless needed
  • MFA policy enforced for elevated roles
  • Plugin install policy documented and enforced at the super_admin level

Next steps: Security Model - Roles and Permissions