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.
1. Strong secrets (boot-refusal enforced)
Section titled “1. Strong secrets (boot-refusal enforced)”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.
# Generate strong values - do NOT reuse these examplespython -c "import secrets; print(secrets.token_hex(32))" # for SECRET_KEYpython -c "import secrets; print(secrets.token_hex(24))" # for ENCRYPTION_SALT| Variable | Requirement | Notes |
|---|---|---|
SECRET_KEY | ≥ 32 characters; not the default string | Signs JWT tokens. Rotation invalidates all sessions. |
ENCRYPTION_SALT | Non-default value | Derives the Fernet encryption key for stored credentials. |
POSTGRES_PASSWORD | Strong, non-default | Backend database password. |
LOGDB_URL | Must be set | Connection string for TimescaleDB. |
REDIS_PASSWORD | Must be set | Valkey/Redis broker and cache password. |
The minimum user-account password length is 12 characters.
2. TLS and edge configuration
Section titled “2. TLS and edge configuration”FreeSDN uses Caddy as the default edge, providing automatic HTTPS. Set CADDY_SITE_ADDRESS:
| Value | Result |
|---|---|
:80 | Plain HTTP - only when running behind a TLS-terminating load balancer |
localhost | Local TLS via Caddy’s internal CA |
your.domain.com | Automatic Let’s Encrypt certificate |
# .env.pro (copy from .env.pro.example and fill in secrets)CADDY_SITE_ADDRESS=sdn.example.comAll 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.
# Verify docs are off - expect 404curl -sf https://your.domain/api/v1/docs4. Metrics endpoint auth token
Section titled “4. Metrics endpoint auth token”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:
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.
5. Marketplace publisher key
Section titled “5. Marketplace publisher key”Pin the publisher signing key before enabling the marketplace. The platform refuses unsigned catalogs by default:
# Generate a key pair for your private registrypython 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.
# Only set this after reviewing your adapter config and staged changes:ADAPTER_READ_ONLY=false7. GPG-encrypted backups
Section titled “7. GPG-encrypted backups”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:
BACKUP_GPG_RECIPIENT=operator@example.com # GPG key in the keyringBACKUP_RETENTION_DAYS=7 # prune older archivesWithout 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:
docker compose --env-file .env.pro --profile dr up -d8. Rate limiting
Section titled “8. Rate limiting”Rate limiting is on by default:
| Limit | Default |
|---|---|
| General API (per principal) | 600 requests/min (RATE_LIMIT_RPM) |
Auth endpoints (/auth/*) | 5 requests/min per IP (hardcoded) |
| Per-principal burst | 120 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.
9. Agent key pinning
Section titled “9. Agent key pinning”For the strongest auto-update posture, bake the backend’s release signing key fingerprint into each Agent at install time:
# Get the fingerprint from your backendcurl -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.
10. User registration
Section titled “10. User registration”User self-registration is disabled by default. Enable it only for a multi-tenant SaaS-style deployment:
ALLOW_REGISTRATION=true # off by defaultIn the default configuration, only administrators can create accounts.
11. MFA enforcement
Section titled “11. MFA enforcement”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.
12. Review container security settings
Section titled “12. Review container security settings”The production Compose configuration applies the following to all containers:
no-new-privileges: true- prevents privilege escalation inside containers.- Non-root runtime users (
appuserfor the backend;nginxfor 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.
13. Plugin installation policy
Section titled “13. Plugin installation policy”Plugin installation is restricted to super_admin. Before installing any plugin:
- Review the plugin’s source code and manifest.
- Confirm the declared permissions match the plugin’s stated purpose.
- Treat the install as deploying a Python package into your production environment - that is the accurate mental model.
Checklist summary
Section titled “Checklist summary”-
SECRET_KEYset to a strong random value (≥ 32 chars) -
ENCRYPTION_SALTset to a non-default value -
POSTGRES_PASSWORD,LOGDB_URL,REDIS_PASSWORDset - TLS configured via
CADDY_SITE_ADDRESS -
FORWARDED_ALLOW_IPSset if behind a proxy -
ENABLE_DOCSnot set (defaults off in production) -
METRICS_AUTH_TOKENset -
MARKETPLACE_PUBLISHER_PUBLIC_KEYset (orMARKETPLACE_ALLOW_UNSIGNED=1for private dev) -
ADAPTER_READ_ONLYreviewed - leavetrueuntil you are ready for live writes -
BACKUP_GPG_RECIPIENTconfigured and backup/restore path tested - Agent
release_public_key_sha256pinned 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_adminlevel
Next steps: Security Model - Roles and Permissions