Skip to content

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.


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_KEY and ENCRYPTION_SALT to 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.

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 preSharedKeypre_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.

CategoryExample keys
Auth credentialspassword, api_key, token, client_secret, credential, cookie, session_token
VPN / cryptoprivate_key, psk, pre_shared_key, tls_key, tls_auth, shared_secret, wireguard_private_key, ipsec_secret
RADIUS / SNMPradius_secret, snmp_community, shared_key, auth_password, encryption_password
Certificatescert, certificate, ca, ca_chain, tls_certificate
OTP / MFAmfa_secret, mfa_backup_codes, otp_secret
UniFi-specificx_passphrase, x_password, x_iapp_key, x_authkey
Proxmox-specificvncticket, csrf_prevention_token, cipassword, ciuserdata
RouterOS-specificauth_key, key_passphrase, private_key_passphrase

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 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:

Terminal window
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.


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.


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 connection

Credentials are decrypted in memory only when an adapter needs to connect to a device. They are never written to logs.


SECRET_KEY is used for JWT signing. Rotating it:

  1. Invalidates all active JWT sessions - every logged-in user will be logged out.
  2. Also renders all Fernet-encrypted credentials unreadable, because SECRET_KEY is the PBKDF2 password used to derive the Fernet key (app/core/crypto.py). Changing either SECRET_KEY or ENCRYPTION_SALT changes 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 as ENCRYPTION_SALT rotation below). There is no built-in migration tool for this.

To rotate:

Terminal window
# Generate a new value
NEW_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
# Update your .env.pro file, then restart
docker compose --env-file .env.pro up -d api worker scheduler

Rotating ENCRYPTION_SALT requires a credential migration:

  1. Before changing the salt, export all encrypted credentials, decrypt them with the old key material, and re-encrypt with the new key material.
  2. 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.

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.

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.


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.


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.