Skip to content

Keys and secrets

Secret Purpose
encryption.key Master key, at least 32 bytes. Anvil derives a key per purpose from it (HKDF, Anvil\Security\KeyRing): component tokens, the tenant switch, encrypted TOTP secrets and recovery codes, API signing secrets and list cursors, and the audit key unless anvil.auditKey is set.
encryption.previousKeys Master keys used before a rotation, comma separated, oldest first. They only decrypt data of the old key.
anvil.auditKey, anvil.auditPreviousKeys Key of the audit log and the keys used before it.
anvil.sso.<id>.clientSecret, anvil.sso.<id>.spPrivateKey Single sign-on client secret and SAML key.
database.default.password Database.

Generate keys on a trusted machine and print them instead of writing a .env:

Terminal window
php spark key:generate --show # encryption.key
php spark anvil:audit:generate-key # anvil.auditKey

Keep them in a secret store and hand them to the server as .env (readable only by the PHP user and the deploy user) or as environment variables. CodeIgniter also reads environment variables with _ instead of . (encryption_key, anvil_auditKey); they must be set for PHP-FPM and for the user that runs the scheduled commands. Never commit .env, and do not reuse the keys of development or staging.

Give the audit log a key of its own from the start (anvil.auditKey): it can then be rotated on its own, as described in Key rotation.

Rotate the master key when it may be known to others (e.g. a former administrator, a leaked .env or backup) and at the intervals of your key management policy.

  1. php spark anvil:audit:verify: the audit log must be intact.

  2. Generate a new key with php spark key:generate --show.

  3. On every server at the same time: append the current key to encryption.previousKeys (comma separated, oldest first) and set encryption.key to the new key.

    encryption.key = hex2bin:…new key…
    encryption.previousKeys = hex2bin:…key used so far…
  4. php spark anvil:rekey: encrypts the TOTP secrets of all users and the signing secrets of all API clients (of all tenants) with the new key, records security.rekeyed in the audit log and reports secrets that no key opens.

  5. Remove the old key from encryption.previousKeys once nothing needs it any more (see below), and from the backups of the configuration.

What happens in between:

  • New data is always encrypted with the current key; previous keys only decrypt.
  • Logged in users have to reload open pages: component tokens, list cursors of the API and the tenant switch use only the current key, they are short-lived anyway.
  • Recovery codes are stored as keyed hashes and cannot be encrypted again. They keep working while their key is in encryption.previousKeys; anvil:rekey reports how many users still have codes of a previous key. Remove the key when these users have generated new codes, or accept that their remaining codes stop working (TOTP itself keeps working).
  • Audit log: without anvil.auditKey its key is derived from encryption.key, and encryption.previousKeys are previous audit keys as well. The first new entry records audit.key_rotated. Keep these keys as long as the log is kept, in anvil.auditPreviousKeys once they leave encryption.previousKeys; better, give the audit log a key of its own.