Skip to content
24/48h shipping across Italy
Skip to content
Documentation navigation

Secret management

This is the authoritative inventory, threat model, and rotation runbook for credentials persisted by k0smos. It applies per tenant database. Deployment secrets such as database/Redis DSNs and the encryption keyring remain in the process environment or an external secret manager.

Classification and inventory

Reversible credentials are needed in clear text briefly when k0smos calls an external service. They use the authenticated enc:v2: envelope. The exact app_settings schema is code-owned by AppSettingOwnerCatalog; key-name substrings are never used to infer sensitivity.

Owner Exact app_settings fields Notes
Psapi / Wpapi / Esapi psapi.api_key; wpapi.wp_application_password, wpapi.woo_consumer_key, wpapi.woo_consumer_secret; esapi.api_key Import connector credentials.
Google / Maps / Facebook google.api_key, google.maps.api_key, google.oauth2.client_secret, maps.google_api_key, facebook.app_secret OAuth client IDs and redirect URIs are identifiers, not secrets.
Captcha / mail captcha.secret_key, mail.smtp.password Runtime verification and SMTP authentication.
Payment payment.token_secret, payment.methods.stripe.secret_key, payment.methods.stripe.webhook_secret, payment.methods.paypal.client_secret, payment.methods.revolut.api_key, payment.methods.revolut.webhook_secret Provider secrets and webhook authentication.
AI ai.provider.profiles, ai.provider.default.api_key, ai.anonymizer.remote.auth_token; client.{openai,ollama,anthropic,openrouter,nvidia,atlascloud,kimi,sensenova}.api_key The complete profile value is protected when credentials are nested JSON.
Integration registries integration.clients.registry, integration.agent_connectors.registry The complete structured registry is protected; queue messages remain credential-free.
Storage storage.profiles The complete structured registry protects passwords, private keys, and passphrases.
Calendar / Invoice calendar.integration.google.access_token, calendar.integration.google.refresh_token, invoice.access_token Delegated and vendor access credentials.
Monitoring / imports fireflyiiiapi.token, oneuptimeapi.api_key, oneuptimeapi.default_heartbeat_secret Admin payloads expose only presence/masked state.
Login hardening security.login.secret_key The admin API never returns the key or embeds it in the displayed login path.

Reversible secrets outside app_settings:

Storage Field Maintenance command
users totp_secret secrets:user-totp
module_opcua_servers password opcua:secrets (only when Opcua is active)

Credentials that must remain one-way are deliberately not encrypted:

  • user and Ecommerce customer password hashes;
  • legacy and scoped personal API-token SHA-256 hashes;
  • OAuth authorization-code, access-token, and rotating refresh-token hashes;
  • payment webhook replay-token hashes and pending-change email OTP hashes.

The legacy users.password compatibility column is treated as a password-hash column by the repository. Cart.token and Data Exchange translation tokens are workflow identifiers rather than operator/service credentials and are not part of the reversible-secret catalog.

Envelope and keyring contract

SecretStoreInterface is provider-neutral. The shipped implementation uses AES-256-GCM with a random 96-bit nonce and 128-bit authentication tag. The compact base64url envelope is:

enc:v2:<version | algorithm | key-id length | key-id |
        SHA-256(context) | nonce | tag | ciphertext>

The authenticated associated data contains purpose, tenant ID, declared owner, and exact field. Moving an envelope to another tenant, owner, or field therefore fails authentication. The compact representation safely fits generated TOTP secrets in the existing VARCHAR(255) column, so no schema widening is needed.

Production keys are independent from JWT_SECRET:

K0SMOS_SECRET_CURRENT_KEY_ID=2026-08
K0SMOS_SECRET_KEYS={"2026-08":"BASE64_ENCODED_32_BYTE_KEY"}

K0SMOS_SECRET_KEYS is a JSON map containing the current key and at most three previous 32-byte keys, each encoded with standard base64. Key IDs contain only letters, digits, dot, underscore, or dash and are at most 64 characters. Generate key material through the platform secret manager or openssl rand -base64 32; never paste generated values into committed files, tickets, logs, or command history shared with other users.

enc:v1: AppSettings values remain readable during migration using the legacy JWT_SECRET derivation, but every write produces enc:v2:. TOTP and OPC UA plaintext rows are also readable only as a migration bridge; new writes are always v2. Authentication, missing-key, and malformed-envelope failures are hard errors and never degrade to ciphertext or an empty value.

Threat model and trust boundaries

Threat Protection / remaining boundary
Database dump or stolen database backup Values are authenticated ciphertext; keys are stored separately. Row names, key IDs, sizes, and presence remain visible metadata.
Log, trace, debug panel, template, or queue leak Maintenance returns counts/key IDs only; known UI payloads are write-only/masked; exceptions are redacted; queue contracts must not carry credentials. A new consumer still requires review.
Cross-tenant or cross-field copy Tenant/owner/field context is authenticated and a copied envelope fails closed.
Compromised application process Not mitigated: a process with the tenant database and keyring can decrypt credentials used by that tenant. Restrict process, debug, shell, and secret-manager access.
Malicious application administrator The UI cannot read stored secrets, but an authorized administrator can replace them and trigger integrations. ACL and audit controls remain required.
Stolen keyring alone It is insufficient without an encrypted database/backup, but must still be treated as a critical secret.
Loss of all applicable keys Permanent data loss for every envelope using those keys. There is no recovery bypass. Restore a matching escrowed keyring or replace credentials.
Partial/interrupted rotation Commands are rerunnable, use bounded transactions, skip current envelopes, and compare the scanned value during update so concurrent application writes are not overwritten.
Rollback Readers must be deployed before v2 writes. A binary rollback to a release that cannot read v2 is unsafe after protection starts; roll forward or deploy a compatibility reader while retaining old keys.

Backup and disaster recovery gate

Before the first v2 write or every key rotation:

  1. Back up each tenant database and verify its restore in an isolated location.
  2. Export the complete current/previous keyring to a separate encrypted escrow with access control, recovery ownership, and a recorded key-ID inventory.
  3. Prove the restored application can run verify for AppSettings, TOTP, and active module-owned secret stores without revealing values.
  4. Record backup retention. An old encrypted backup still requires its old key; do not delete that key until the backup expires or is replaced by a verified post-rotation backup.

Never store the database backup and its only keyring copy in the same security domain. Recovery operators must know that losing every matching key makes the encrypted values unrecoverable; vendor credentials then need replacement.

Inventory, protection, and verification

Run the applicable commands for every tenant. Examples use a reserved hostname:

php bin/console secrets:app-settings inventory --host=k0smos.example.com
php bin/console secrets:user-totp inventory --host=k0smos.example.com
php bin/console opcua:secrets inventory --host=k0smos.example.com

php bin/console secrets:app-settings protect --dry-run --host=k0smos.example.com
php bin/console secrets:user-totp protect --dry-run --host=k0smos.example.com
php bin/console opcua:secrets protect --dry-run --host=k0smos.example.com

php bin/console secrets:app-settings protect --batch-size=100 --host=k0smos.example.com
php bin/console secrets:user-totp protect --batch-size=100 --host=k0smos.example.com
php bin/console opcua:secrets protect --batch-size=100 --host=k0smos.example.com

php bin/console secrets:app-settings verify --host=k0smos.example.com
php bin/console secrets:user-totp verify --host=k0smos.example.com
php bin/console opcua:secrets verify --host=k0smos.example.com

protect and rotate are equivalent maintenance writes: they protect plaintext/legacy rows and rekey previous-key envelopes. status/inventory report counts without decrypting valid envelopes; verify authenticates every envelope and fails while plaintext, legacy, stale, or unreadable values remain. Opcua commands exist only for tenants where that module is active.

Dual-read, single-write rotation

  1. Create the new 32-byte key and add it to K0SMOS_SECRET_KEYS beside the old key. Do not remove the old entry.

  2. Deploy readers containing both keys to every web/CLI/worker process, then set K0SMOS_SECRET_CURRENT_KEY_ID to the new ID. Restart processes so they share one keyring view.

  3. Take and verify the pre-rotation database/keyring backup.

  4. Run each rotate --dry-run, then rotate --batch-size=100, for every tenant. Rerun after interruption; already-current rows are skipped.

  5. Run all applicable verify commands. They must report zero plaintext, legacy, stale, and invalid rows.

  6. Run all applicable retirement checks:

    php bin/console secrets:app-settings retire-check --retire-key=2026-07 --host=k0smos.example.com
    php bin/console secrets:user-totp retire-check --retire-key=2026-07 --host=k0smos.example.com
    php bin/console opcua:secrets retire-check --retire-key=2026-07 --host=k0smos.example.com
    
  7. Take and restore-test a post-rotation backup. Keep the old key while any retained backup or tenant can still contain its key ID.

  8. Only after every tenant, process, active module, and retained backup is clear, remove the previous key and restart processes. Repeat verify afterwards.

Owner-by-owner migration is supported by writing a credential through its normal admin service before the bulk command. After each tranche, verify is the authoritative isolated-database proof that every catalogued non-empty row is v2 and readable.