Secrets and Credentials
FreeSDN handles a large volume of sensitive material: device credentials, SIP/AMI/ARI secrets, WireGuard and IPsec pre-shared keys, SSO client secrets, LDAP bind passwords, TOTP seeds, and API keys. This page documents how each category is protected at rest, on read, and on rotation.
Credential encryption at rest
Section titled “Credential encryption at rest”Device and controller credentials are stored in Fernet-encrypted columns in the database. Fernet provides authenticated symmetric encryption (AES-128-CBC with HMAC-SHA256). The encryption key is derived from SECRET_KEY (the PBKDF2 password) using PBKDF2-HMAC-SHA256 at 260,000 iterations, salted by ENCRYPTION_SALT. Changing either value changes the derived key and renders all stored credential ciphertext unreadable.
This means:
- A raw database dump does not expose credentials - the dump contains ciphertext that requires both
SECRET_KEYandENCRYPTION_SALTto decrypt. - If either key material variable changes, previously encrypted credentials become unreadable (see Rotation below).
- The encryption is done in the application layer (
app/core/crypto.py). There is no plaintext storage path.
Central secret redaction on reads
Section titled “Central secret redaction on reads”Every response from an adapter passes through app.core.redaction.redact_secrets before reaching an API consumer. This is a recursive, depth-bounded function that walks the response structure and replaces sensitive field values with a redacted marker.
The strip-list matches field names after a three-step normalisation: camelCase/PascalCase boundaries are split by inserting underscores at case transitions (so preSharedKey → pre_shared_key), the result is lower-cased, and then hyphens are converted to underscores. This means RouterOS pre-shared-key, OPNsense pre_shared_key, and Omada preSharedKey all resolve to the same strip-list entry. The camelCase splitting (added in audit-v6 W4) is the mechanism that catches vendor keys like preSharedKey, securityKey, and apiSecret - without it those keys pass the redactor unmasked.
What is always redacted
Section titled “What is always redacted”| Category | Example keys |
|---|---|
| Auth credentials | password, api_key, token, client_secret, credential, cookie, session_token |
| VPN / crypto | private_key, psk, pre_shared_key, tls_key, tls_auth, shared_secret, wireguard_private_key, ipsec_secret |
| RADIUS / SNMP | radius_secret, snmp_community, shared_key, auth_password, encryption_password |
| Certificates | cert, certificate, ca, ca_chain, tls_certificate |
| OTP / MFA | mfa_secret, mfa_backup_codes, otp_secret |
| UniFi-specific | x_passphrase, x_password, x_iapp_key, x_authkey |
| Proxmox-specific | vncticket, csrf_prevention_token, cipassword, ciuserdata |
| RouterOS-specific | auth_key, key_passphrase, private_key_passphrase |
What the redacted value looks like
Section titled “What the redacted value looks like”Redacted values are replaced with "***" in the response body. The field is present in the shape (so clients know a credential exists) but the value is never transmitted.
ENCRYPTION_SALT
Section titled “ENCRYPTION_SALT”ENCRYPTION_SALT is the per-deployment salt used in PBKDF2 key derivation. It must:
- Be set to a unique, random value before the first deployment.
- Never change after credentials have been stored - rotating it without migrating ciphertext will render all stored credentials unreadable.
- Be kept secret with the same care as
SECRET_KEY.
Generate a value at deployment time:
python -c "import secrets; print(secrets.token_hex(24))"Store both SECRET_KEY and ENCRYPTION_SALT in your secrets manager or environment file - never in version control.
API key storage
Section titled “API key storage”User API keys are stored as SHA-256 hashed values. The raw key is shown exactly once at creation time and is not recoverable afterward. If a key is lost, it must be revoked and recreated.
How secrets flow through the system
Section titled “How secrets flow through the system”Device / controller │ │ vendor API response (plaintext credentials in JSON/XML) ▼ Adapter layer │ │ redact_secrets() strips sensitive keys ▼ API consumer (browser / integration) (never sees raw credential values)
Config write (UI / API) │ │ sensitive fields accepted on write, encrypted before DB insert ▼ Fernet ciphertext in Postgres │ │ decrypted only in the adapter layer, never returned to callers ▼ Device credential used for outbound connectionCredentials are decrypted in memory only when an adapter needs to connect to a device. They are never written to logs.
Rotation
Section titled “Rotation”Rotating SECRET_KEY
Section titled “Rotating SECRET_KEY”SECRET_KEY is used for JWT signing. Rotating it:
- Invalidates all active JWT sessions - every logged-in user will be logged out.
- Also renders all Fernet-encrypted credentials unreadable, because
SECRET_KEYis the PBKDF2 password used to derive the Fernet key (app/core/crypto.py). Changing eitherSECRET_KEYorENCRYPTION_SALTchanges the derived key and makes all stored credential ciphertext unreadable. Before rotating, export all encrypted credentials, decrypt them with the old key material, and re-encrypt with the new key (same procedure asENCRYPTION_SALTrotation below). There is no built-in migration tool for this.
To rotate:
# Generate a new valueNEW_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
# Update your .env.pro file, then restartdocker compose --env-file .env.pro up -d api worker schedulerRotating ENCRYPTION_SALT
Section titled “Rotating ENCRYPTION_SALT”Rotating ENCRYPTION_SALT requires a credential migration:
- Before changing the salt, export all encrypted credentials, decrypt them with the old key material, and re-encrypt with the new key material.
- Then update the environment variable and restart.
There is no built-in migration tool for this in the current release. Plan salt rotation carefully. For most deployments, the right answer is to set a strong salt at deployment time and not rotate it unless a key compromise is suspected.
Rotating device credentials
Section titled “Rotating device credentials”Device credentials (controller passwords, API tokens, etc.) can be updated in Controllers → [controller] → Edit. The new credential is encrypted and stored immediately; the next adapter connection uses the updated value. There is no service restart required.
Revoking API keys
Section titled “Revoking API keys”API keys are revoked immediately from Settings → API Keys → Revoke. The revocation takes effect on the next request - there is no grace period or token expiry window that must expire first.
Audit trail
Section titled “Audit trail”Every credential creation, update, and deletion generates an AuditLogRecord. Audit records include the actor’s user ID, name, email, actor type (user/system/api_key), IP address, and timestamp. They are stored in the separate audit schema, isolated from application data schemas, and cannot be deleted by org_admin or below.
Credential values are never included in audit records - only the action and the resource identifier are logged.
Secrets never logged
Section titled “Secrets never logged”The application-level logger is configured to strip known sensitive patterns before writing to stdout/logdb. Adapter connection strings (which may contain embedded passwords) are sanitised before appearing in log lines. If you observe a raw credential in a log line, treat it as a bug and report it through the responsible disclosure process.