This document is a draft proposal accompanying the code in this PR. It is intended to seed the formal SIP discussion. The code here ships the backward-compatible engine selection and the re-encryption migrator (Phases 1–2 below); both are opt-in and change nothing for existing installs by default. Flipping the default for fresh installs (Phase 3) remains future work.
Superset app-encrypts a number of sensitive fields before persisting them to the metadata database, including:
encrypted_extra (superset/models/core.py),superset/databases/ssh_tunnel/models.py),EncryptedType.These fields are encrypted with sqlalchemy_utils.EncryptedType, which defaults to AesEngine (AES-CBC). AES-CBC provides confidentiality but is unauthenticated: it has no integrity tag. An attacker with write access to the ciphertext (e.g. direct metadata-DB access, a backup, or a compromised replica) can perform bit-flipping / chosen-ciphertext manipulation to silently alter the decrypted plaintext of a secret without detection.
AesGcmEngine (AES-GCM) is authenticated encryption: tampering causes decryption to fail loudly rather than yielding attacker-influenced plaintext. Using authenticated encryption for secrets at rest is an ASVS L1 expectation (11.3.2 / cryptography best practice).
config.py already documents that operators can switch to GCM by writing a custom AbstractEncryptedFieldAdapter, but:
A three-part change, delivered incrementally so existing deployments are never broken:
SQLALCHEMY_ENCRYPTED_FIELD_ENGINE config ("aes" | "aes-gcm"), defaulting to "aes" (no behavior change for existing installs).SQLAlchemyUtilsAdapter to honor it (an explicit engine kwarg still wins, so the migrator can pin an engine).The existing SecretsMigrator (previously only used for SECRET_KEY rotation) gains an engine migration mode that:
EncryptedType column (via discover_encrypted_fields()),SECRET_KEY,Exposed via a new --engine option on the existing CLI command: superset re-encrypt-secrets --engine aes-gcm, runnable by operators with a DB backup in hand. The SECRET_KEY is unchanged; an engine change and a key rotation can also be combined (pass --previous_secret_key as well).
Once the migrator and docs are in place, change the default to "aes-gcm" for fresh installs only (e.g. keyed off an empty metadata DB / documented in UPDATING.md), keeping existing installs on "aes" until they run Phase 2.
SQLALCHEMY_ENCRYPTED_FIELD_ENGINE: Literal["aes", "aes-gcm"].superset re-encrypt-secrets --engine <name>."aes-gcm" without running the Phase 2 migrator will make existing secrets undecryptable — this is called out in the config comment and must be in UPDATING.md.re-encrypt-secrets --engine aes-gcm → set SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes-gcm" → restart → re-run re-encrypt-secrets --engine aes-gcm once more to sweep up any secrets a live instance wrote as AES-CBC during the cutover window. The canonical, more detailed version of this runbook lives in UPDATING.md; this is a summary.AesEngine allows queryability over encrypted fields; AES-GCM does not. Any code that filters/queries on an encrypted column directly must be audited before Phase 3 (none is expected, but it must be verified).re-encrypt-secrets --engine aes), since the migrator is engine-symmetric. Should rollback be documented as a supported path or discouraged?SECRET_KEY rotation + engine change in a single pass (pass --previous_secret_key alongside --engine). Is that combination worth calling out in the operator docs, or kept advanced?