Fernet encryption — препарирование под капотом
Connection passwords и Variable values в metadata DB шифруются Fernet. Это симметричный encryption из библиотеки cryptography от PyCA, использующий AES-128-CBC для конфиденциальности и HMAC-SHA256 для аутентификации. Airflow выбрал Fernet не случайно: он simple, well-vetted, имеет nice property — обнаружить tampering сразу при decryption.
Этот урок — killer-deep разбор Fernet token формата, key rotation механики и того, как восстанавливать систему после потери ключа.
Что такое Fernet (формально)
Fernet — это spec от Heroku (github.com/fernet/spec), реализованный в PyCA cryptography. Это не алгоритм, а scheme: композиция уже известных примитивов в один envelope формат.
Из спеки:
“Fernet guarantees that a message encrypted using it cannot be manipulated or read without the key. Fernet is an implementation of symmetric (also known as ‘secret key’) authenticated cryptography.”
Ключевые свойства:
- Симметричный: один ключ для encrypt и decrypt
- Authenticated: HMAC catches tampering at decrypt time
- Versioned: первый байт token — version (
0x80= current) - No replay protection: timestamp есть, но используется advisory (Airflow not enforce)
Структура Fernet token
Token — это urlsafe_b64encode(...) от следующего byte string:
| Version | Timestamp | IV | Ciphertext | HMAC |
| 1 byte | 8 bytes | 16 bytes | N*16 bytes | 32 bytes |
Затем всё это base64-урласафно кодируется → строка вида:
gAAAAABnVHj4xMyz8L7QwR3Z2K... (~100+ символов)
Эта строка попадает в колонку connection.password или variable.val.
Откуда два sub-key
Fernet key — это 32 байта (256 бит) base64-encoded → 44 символа output:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# kZqJ7gN3vP8XmQ5RtY2L9WhCbD4FaUeI6NoPxKsM1Bw=
Эти 32 байта разделены на два sub-key:
key[0:16](16 байт) → signing key для AES-128key[16:32](16 байт) → HMAC key для SHA-256
Это classic pattern «derive multiple keys из one master key» — но Fernet делает это просто split-ом, не KDF. Безопасно потому, что 256 бит entropy достаточно для двух independent 128-bit keys (security рассматривается в Fernet spec).
Encrypt/Decrypt в Airflow
В коде Airflow это абстрагировано в airflow.models.crypto:
# Псевдокод
from cryptography.fernet import MultiFernet, Fernet
def get_fernet():
fernet_key = conf.get("core", "fernet_key")
if "," in fernet_key:
# Multiple keys → key rotation mode
keys = [Fernet(k.strip().encode()) for k in fernet_key.split(",")]
return MultiFernet(keys)
else:
return Fernet(fernet_key.encode())
# При SET-е connection.password
@property
def password(self):
if self._password and self.is_encrypted:
fernet = get_fernet()
return fernet.decrypt(self._password.encode()).decode()
return self._password
@password.setter
def password(self, value):
if value:
fernet = get_fernet()
self._password = fernet.encrypt(value.encode()).decode()
self.is_encrypted = True
Каждое чтение conn.password — это on-the-fly decrypt. На каждый Hook init минимум один Fernet decrypt. Это cheap (~microseconds), но накапливается на больших DAGs — отсюда [secrets] use_cache в новых версиях.
MultiFernet — multi-key support
MultiFernet — это wrapper, который принимает список Fernet objects:
from cryptography.fernet import MultiFernet, Fernet
key_new = Fernet(b"new_key_base64==")
key_old = Fernet(b"old_key_base64==")
multi = MultiFernet([key_new, key_old]) # new ПЕРВЫЙ!
# Encrypt → использует ПЕРВЫЙ key (new)
token = multi.encrypt(b"secret")
# Decrypt → пробует каждый key по порядку, пока не получит valid
plaintext = multi.decrypt(token) # works для tokens, зашифрованных любым ключом
Это позволяет rolling key rotation без downtime: пока MultiFernet имеет старый ключ в списке, старые tokens decryptable; новые tokens используют новый ключ. После того как все старые tokens переезжированы, старый ключ можно удалить.
Key rotation strategy
Сценарий: ключ скомпрометирован или вы хотите ротировать ежеквартально.
CLI airflow rotate-fernet-key:
# 1. Generate new key (например, через ENV из CI/CD)
NEW_FERNET=$(python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')
# 2. Update env (через secret manager / k8s secret)
export AIRFLOW__CORE__FERNET_KEY="${NEW_FERNET},${OLD_FERNET}"
# 3. Restart scheduler/webserver/workers (rolling)
kubectl rollout restart deployment/airflow-scheduler -n airflow
# 4. Запустить rotation
airflow rotate-fernet-key
# Output:
# Connection 'pg_prod_warehouse': re-encrypted
# Connection 'snowflake_prod': re-encrypted
# Variable 'api_endpoint': re-encrypted
# ... (всего 47 records re-encrypted)
# 5. Remove old key
export AIRFLOW__CORE__FERNET_KEY="${NEW_FERNET}"
kubectl rollout restart deployment/airflow-scheduler -n airflow
Между step 2 (multi-key deploy) и step 3 (rotation) не задерживайтесь надолго. Если в этом окне старый ключ leaked, любой новый token также может быть decrypted (поскольку MultiFernet тоже знает new key). Production target: вся rotation процедура завершается за минуты, не дни.
Disaster scenario: lost Fernet key
Самый тяжёлый сценарий — Fernet key потерян, и в metadata DB остались passwords, которые никто не может расшифровать.
Что вы увидите при попытке использовать connection:
cryptography.fernet.InvalidToken: Token cannot be decrypted
Это значит — Fernet верифицировал HMAC (или не верифицировал) и не смог достать plaintext. Либо tampered, либо wrong key.
Recovery options:
-
Восстановить ключ из backup — если ключ хранился в Vault / 1Password / GCP KMS с versioning, можно восстановить предыдущую версию. Это правильный workflow.
-
Re-create connections руками — если ключ безвозвратно потерян, passwords unrecoverable. Вам придётся:
- Сгенерировать новый Fernet key
- Очистить
connection.passwordиconnection.extraдля всех записей:UPDATE connection SET password=NULL, extra=NULL - Через UI или CLI заново добавить connections с правильными credentials (которые у вас есть из другого источника — например, password manager)
-
Use Secrets Backend retroactively — если переезд на Vault planned, это возможность сделать его сейчас: connections исчезают из DB, переезжают в Vault, Fernet больше не нужен для них.
Fernet key — это критический secret в Airflow. Потеря = потеря всех connections и encrypted variables. Должен быть: (1) сгенерирован cryptographically secure RNG, (2) храниться в external vault (никогда в git, никогда в k8s plain secret без encryption-at-rest), (3) иметь disaster recovery procedure (где он backup-нут, кто имеет access, как restore-ить).
Что НЕ шифруется
Fernet — это column-level encryption для password и extra. Остальное в DB — plaintext:
connection.host,connection.login,connection.port,connection.schema— plaintextconnection.conn_type,connection.description— plaintextdag.fileloc,task_instance.log,xcom.value— plaintext (XCom особенно — храните осторожно)slot_pool.pool— plaintext
Это значит — если злоумышленник имеет SQL access (read-only) к metadata DB, он видит всё, кроме encrypted columns. Login видно, host видно, можно понять структуру системы. Защита от этого — TLS to DB, network policies, RBAC в Postgres.
XCom values — частая ошибка. Люди кладут туда API responses, которые могут содержать токены. По умолчанию XCom plaintext. С Airflow 2.0+ можно конфигурировать [core] xcom_backend = my_module.MyXComBackend и реализовать encryption там — но out-of-box не зашифровано.
Performance impact
Fernet encrypt/decrypt — fast:
- Encrypt 100-byte plaintext: ~5-10 µs
- Decrypt: ~5-10 µs
- HMAC verify: ~3 µs
На каждый task instance с Hook → один BaseHook.get_connection → один Fernet decrypt. Для DAG с 100 tasks это 100 decrypts ≈ 1 ms total. Negligible.
Но если у вас top-level Variable.get(‘x’) в DAG-файле, каждый DAG parse делает Fernet decrypt → 30s interval × N DAGs → может быть noticeable load. Урок 04 разбирает этот pitfall.
Проверка ключа
Быстрая sanity check, что ключ корректный:
from cryptography.fernet import Fernet
key = "kZqJ7gN3vP8XmQ5RtY2L9WhCbD4FaUeI6NoPxKsM1Bw="
try:
f = Fernet(key.encode())
token = f.encrypt(b"test")
plaintext = f.decrypt(token)
print(f"OK: {plaintext.decode()}")
except Exception as e:
print(f"INVALID KEY: {e}")
Если ваш key не 44 символа (32 байта base64) — это битый ключ. Airflow при startup проверит и логирует warning Could not decode Fernet key если формат неправильный, при этом encryption ломается и passwords хранятся открытым текстом (is_encrypted=False).