Encryption and the SECRET_KEY
=====================================
This section is for TOM administrators and describes the relationship
between the ``settings.SECRET_KEY`` and the way TOMToolkit encrypts
sensitive user data. If you are a TOM developer looking for how to
create an encrypted database field, your documentation is here:
:doc:`/customization/encrypted_model_fields`
------
TOM Toolkit encrypts sensitive user data (API keys, observatory
credentials, anything declared with :class:`EncryptedProperty`) using a
single Fernet cipher derived from Django's ``settings.SECRET_KEY``.
That means when an encrypted field is written to or read from the database,
a cipher is created. The cipher can encrypt unencrypted plaintext (in) and decrypt
encrypted ciphertext (out). Cipher creation requires an encryption key.
TOMToolkit creates an encryption key that is based upon, but not identical
to, Django's ``settings.SECRET_KEY``. That's how the ``SECRET_KEY`` is related
to TOMToolkit's encryption.
Treat ``SECRET_KEY`` like an encryption key
-------------------------------------------
If you lose ``SECRET_KEY`` (and any active
``SECRET_KEY_FALLBACKS`` entries), every encrypted field becomes
unrecoverable. Keep ``SECRET_KEY`` secret, never commit it, and back
it up through whatever channel your other production secrets use.
The standard Django guidance applies — see the
`Django deployment checklist
`_
and the
`SECRET_KEY documentation
`_.
.. warning::
Rotating ``SECRET_KEY`` without first running the rotation procedure
below will leave every previously-encrypted field unreadable. Follow
the procedure exactly — don't just edit ``SECRET_KEY`` in your env.
Graceful ``SECRET_KEY`` rotation
--------------------------------
Because TOMToolkit's encryption scheme depends on the value of ``settings.SECRET_KEY``,
if you need to change your ``SECRET_KEY``, we must decrypt the encrypted data
with a cipher derived from the old ``SECRET_KEY`` and re-encrypt it with a cipher derived
from the new ``SECRET_KEY``. The following procedure explains the process in full.
We use Django's built-in
`SECRET_KEY_FALLBACKS `_
mechanism to rotate keys without an outage and without data loss. The
encryption module's ``decrypt()`` tries the primary derived key first
and then a derived key for each ``SECRET_KEY_FALLBACKS`` entry; the
``encrypt()`` path always uses the primary. So once a new
``SECRET_KEY`` is in place with the old key in fallbacks, **reads of
existing encrypted data continue to work**, and **new writes use the new
key**.
Scaffolded TOMs already include ``SECRET_KEY_FALLBACKS = []`` in their
``settings.py`` (added by ``tom_setup``); if your TOM predates that
template change, just add the line yourself.
The end-to-end procedure:
1. **Stage the old key as a fallback and install a new
``SECRET_KEY``**. In ``settings.py``, move the existing
``SECRET_KEY`` value into ``SECRET_KEY_FALLBACKS`` and set
``SECRET_KEY`` to the new value::
SECRET_KEY = ''
SECRET_KEY_FALLBACKS = ['']
(Or via env vars, whatever pattern your deployment uses.)
.. warning::
Remember to keep secret keys secret! The actual values should never be committed to github, or saved anywhere publicly accessible.
2. **Restart the server.** All existing encrypted data is still
readable (via the fallback). All new writes — including any
re-encryption — use the new primary key. Django's HMAC signing
machinery also honours the fallback, so existing signed cookies
and password-reset tokens stay valid.
3. **Re-encrypt existing data forward** so it no longer depends on the
fallback::
python manage.py rotate_encryption_key
The command walks every :class:`EncryptedProperty` field across
``INSTALLED_APPS``, decrypts each value (transparently using either
the primary or a fallback), and re-encrypts under the primary. After
this, no value in the database requires the fallback to decrypt.
4. **Remove the fallback** from ``settings.py``::
SECRET_KEY = ''
SECRET_KEY_FALLBACKS = []
Restart. You're now fully on the new key.
If anything goes wrong between steps 1 and 3, simply leave the fallback
in place — the system stays functional indefinitely with both keys
active. ``rotate_encryption_key`` is idempotent: running it again is
always safe.
If the command reports per-row failures, those rows were encrypted under
a key that is no longer in either ``SECRET_KEY`` or ``SECRET_KEY_FALLBACKS``
(i.e., that key has been forgotten). Add it back if you can; otherwise
the data on those rows is lost.
What if ``SECRET_KEY`` is lost?
-------------------------------
If you lose ``SECRET_KEY`` and have no backup:
- Every :class:`EncryptedProperty` value (saved API keys, observatory
credentials) becomes unrecoverable. The ciphertext is still in the
database; the key needed to decrypt it is gone.
- All Django signing-dependent states also break: outstanding password-reset
tokens, signed URLs, persistent session cookies. Users will
need to log in again and possibly re-enter any saved secrets.
Treat ``SECRET_KEY`` backup with the same seriousness as your database
backup.
Key Derivation Function Implementation Details
----------------------------------------------
The derivation uses `HKDF `_
(RFC 5869) with a domain-separator label, so the encryption key is
cryptographically independent of the way Django
uses ``SECRET_KEY`` for HMAC signing. See
:mod:`tom_common.encryption` for the implementation.
See also
--------
- :doc:`/customization/encrypted_model_fields` — how plugin authors
declare and use encrypted fields.
- `Django deployment checklist
`_
— the broader hardening you should do before going live.