Home DevOps Airflow Security: The DAG That Leaked DB Credentials
Advanced 3 min · September 04, 2026
Airflow Security RBAC and Secrets

Airflow Security: The DAG That Leaked DB Credentials

Airflow security locks down leaked DB credentials with Fernet rotation, secrets backends, and RBAC least privilege.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 25 min
  • Admin access to an Airflow instance and its config
  • Basic understanding of connections and Variables
  • A secret manager account (Vault, AWS, or GCP)
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Airflow security means unique Fernet keys, secrets backends like Vault, and RBAC roles scoped to least privilege per team
  • Key components: Fernet encryption for stored connections, OAuth or LDAP auth, DAG-level RBAC grants, and audit logs on the API
  • Performance insight: secret-backend caching answers connection lookups in milliseconds, so centralizing secrets adds under 50ms per task startup
  • Production insight: one team found a live prod password in git history months old; rotation touched every deployment that ever pulled the repo
  • Biggest mistake: running the documented default Fernet key, which makes every encrypted connection password decryptable from a DB backup
✦ Definition~90s read
What is Airflow Security RBAC and Secrets?

Airflow security is the discipline of unique Fernet keys, backend-managed secrets, and least-privilege RBAC so one leaked credential can't sink the platform. It covers encryption at rest, SSO authentication, and immutable audit logs.

Imagine a hotel where every room key hangs on one labeled board behind the front desk, and someone photocopied the board into the guest newsletter.
Plain-English First

Imagine a hotel where every room key hangs on one labeled board behind the front desk, and someone photocopied the board into the guest newsletter. Airflow security means moving those keys into a real safe, giving each staff member only the keys their job needs, and changing the safe combination regularly.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Airflow holds the keys to your kingdom. Every warehouse password, API token, and cloud credential flows through its connections, and one leaked DAG file hands attackers the whole map.

One team learned this when a database password sat in plain text inside a DAG. It shipped to git, got copied into three branches, and worked perfectly until the day it didn't. The cleanup took longer than the breach.

You'll lock this down with Fernet rotation, a real secrets backend, and RBAC that gives each team the smallest key that works. Least privilege. Zero drama.

The Threat Model for a Workflow Platform

Airflow is a credential concentrator. Connections hold warehouse passwords, cloud keys, and API tokens, Variables hold the rest, and every task execution touches them. Your threat model starts there, not at the login page.

Attackers don't need exploits when secrets sit in git. A read-only repo viewer, a forgotten fork, or a DB backup without its key discipline each hand over the kingdom. You'll defend all three surfaces or you'll defend none.

Scope the model per environment. Prod gets unique keys, SSO-backed roles, and backend-managed secrets. Dev gets the same shapes with throwaway values so nobody learns bad habits from a lax playground.

📊 Production Insight
Repo viewers and DB backups are attackers too in the model. Secrets in git outlive every employee. Rule: prod keys unique, SSO-backed, backend-managed.
🎯 Key Takeaway
Airflow concentrates every credential, so git, backups, and the UI are all secret surfaces. Model all three per environment or the one you skip becomes the breach.

Fernet: Encrypt, Store, and Rotate the Key

Fernet encrypts connection passwords and sensitive Variables at rest in the metadata DB. It protects backups and disk images, not runtime memory or git history. Know what it covers before trusting it.

The key lives in AIRFLOW__CORE__FERNET_KEY and must be unique per environment. The documented example key is public knowledge, so shipping it means encrypting with a password printed in the manual. Generate a real one and store it in your secret manager.

Rotate append-first. New key decrypts first while old ciphertext stays readable, you re-save connections to re-encrypt them, then you drop the old key. You'll practice on staging because the one-step shortcut orphans every secret at once.

Store the key like it matters. AIRFLOW__CORE__FERNET_KEY overrides airflow.cfg, and AIRFLOW__CORE__FERNET_KEY_CMD reads it from a file or KMS wrapper so it never sits in env dumps. Generate before creating users, back it up somewhere only the Airflow processes can read, and rotate on a calendar — a leaked default sample key decrypts every connection for anyone with DB read access.

scripts/rotate-fernet-key.shBASH
1
2
3
4
5
6
7
8
9
10
11
# 1. generate a fresh key (cryptography must be installed)
NEW_KEY=$(python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')
echo "new key staged (prefix): ${NEW_KEY:0:6}..."

# 2. append-first: new key decrypts, old ciphertext still readable
OLD_KEY=$(airflow config get-value core fernet_key)
AIRFLOW__CORE__FERNET_KEY="$NEW_KEY,$OLD_KEY" airflow connections list >/dev/null && echo OK

# 3. re-save each connection so it re-encrypts under the new key
# airflow connections add --conn-id prod_warehouse --conn-type postgres ... (repeat per conn)
# 4. verify a decrypt, then drop the old key from the setting
⚠ Rotate Keys Without Orphaning Secrets
Never replace the Fernet key in one step. Append the new key first, re-save connections, verify, then drop the old key. One-step replacement orphans every stored secret.
📊 Production Insight
Default Fernet keys make encryption decorative against DB backups. Append-first rotation avoids orphans. Rule: new key first, re-save, verify, then drop old.
🎯 Key Takeaway
Fernet seals the database, not the repo. Unique key per environment, stored in a secret manager, rotated append-first with re-saved connections.

Secrets Backends Versus Connection Encryption

Secrets backends move credentials out of Airflow's database into Vault or a cloud manager. Airflow looks up connections at runtime, rotation happens in one place, and no deploy is needed to roll a password. You'll feel the difference the first time security asks for an emergency rotation.

Connection encryption and backends complement each other. Fernet still seals whatever Airflow stores locally, while the backend holds the crown jewels with its own KMS keys and rotation schedules. Use both; neither replaces the other.

Migrate team by team. Point the backend at Airflow, move one team's connections, verify their DAGs resolve secrets, then move the next. You'll avoid a flag-day migration and each team learns the new lookup pattern with support nearby.

Know the lookup order cold: secrets backend first, then environment variables (AIRFLOW_CONN_ / AIRFLOW_VAR_), then the metastore last — and it's not configurable. That order is why Vault-first works: set [secrets] backend to VaultBackend (or SecretsManagerBackend / CloudSecretManagerBackend) with connections_path and variables_path, and DAG code never changes when rotation happens. On Airflow 3 you can also scope a separate backend just for workers, so execution nodes resolve only what they need instead of inheriting the scheduler's full secret surface.

dags/billing_reconcile.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pendulum
from airflow.sdk import dag, task
from airflow.providers.postgres.hooks.postgres import PostgresHook

@dag(
    dag_id="billing_reconcile",
    schedule="0 5 * * *",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["tier-1", "billing"],
)
def billing_reconcile():
    @task
    def reconcile() -> str:
        # secret comes from the backend-backed connection, never code
        hook = PostgresHook(postgres_conn_id="prod_billing_ro")
        rows = hook.get_records("SELECT count(*) FROM charges WHERE ds = '{{ ds }}'")
        return f"charges={rows[0][0]}"

    reconcile()

billing_reconcile()
📊 Production Insight
Emergency rotations without backends mean redeploying everything. Backend lookups cost under 50ms with caching. Rule: crown jewels in Vault, Fernet for the rest.
🎯 Key Takeaway
Backends centralize rotation outside deploys while Fernet seals local storage. Migrate one team at a time and verify secret resolution before moving on.

RBAC Roles and DAG-Level Access

RBAC turns one shared admin login into named humans with scoped power. Built-in roles cover the ladder: Viewer reads, User runs, Op manages runs and connections in scope, Admin configures. DAG-level grants slice horizontally per team.

Design for the incident you fear. Analysts get read on dev DAGs so curiosity can't break prod. On-call gets Op on their team's DAGs so 3 AM clears don't wait for platform. Platform keeps Admin behind SSO group membership that revokes with HR offboarding.

Review quarterly. Roles accrete like sediment: every exception becomes permanent unless someone prunes. You'll pull the user-role export, ask each manager to justify prod grants, and delete the unjustified ones the same day.

Harden the surfaces around RBAC too. Set expose_config = False so the UI never leaks full config to curious roles, and give the API its own auth backend rather than inheriting the webserver's. Authentication itself lives in webserver_config.py (AUTH_DB by default; OAuth, OpenID, LDAP, REMOTE_USER supported) — password auth is day one, LDAP/OAuth is production.

📊 Production Insight
Shared admin turns every curious click into potential prod impact. Scoped roles contain mistakes to one team's DAGs. Rule: justify every prod grant quarterly.
🎯 Key Takeaway
Named users, laddered roles, DAG-level slices per team, SSO groups for prod. Quarterly pruning keeps exceptions from fossilizing into standing access.

Authentication: Default Versus OAuth and LDAP

Default auth proves the UI works, not that your org is secure. Production needs OAuth or LDAP so credentials live in the identity provider, MFA applies, and deprovisioning happens in one place when someone leaves.

Map identity groups to Airflow roles at login. Data-eng lands as Op on their DAGs, analytics as Viewer, platform as Admin. You'll sync on login so role changes propagate without manual UI edits.

Test the offboarding path, not just login. Disable a test user in the IdP and confirm their Airflow session dies and API tokens fail. Joiners get celebrated; leavers get verified.

config/auth-production.ymlYAML
1
2
3
4
5
6
7
8
9
10
auth_manager: SimpleAuthManager  # dev only
# --- production: OAuth-backed login with group mapping ---
# AIRFLOW__CORE__AUTH_MANAGER=airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
# AIRFLOW__FAB__AUTH_TYPE=AUTH_OAUTH
# AIRFLOW__FAB__AUTH_ROLES_SYNC_AT_LOGIN=True
# AIRFLOW__FAB__AUTH_USER_REGISTRATION=True
# map IdP groups to Airflow roles:
# data-eng -> Op on team DAGs, analytics -> Viewer, platform -> Admin

airflow users list -o json 2>/dev/null | head -c 1500
📊 Production Insight
Local UI users survive HR offboarding silently. SSO ties access to employment status. Rule: test user disablement end-to-end quarterly.
🎯 Key Takeaway
SSO moves identity to the IdP with MFA and one-place offboarding. Map groups to roles at login and verify the leaver path, not just the login path.

Audit Logs and the API Surface

Audit logs turn who changed what from archaeology into a query. API and webserver logs record connection edits, role grants, and variable changes with actor and timestamp. Ship them to immutable storage where even admins can't rewrite history.

Alert on the sensitive three. Connection edits, role grants, and Variable changes each deserve a Slack ping to the platform channel. You'll catch the well-meaning analyst editing prod before the blast, not after.

Read the log monthly. Patterns emerge: Friday-afternoon connection tweaks, repeated role escalations before launches, Variables edited instead of connections. Each pattern becomes a guardrail or a training note.

Ship audit logs somewhere you'll actually read them. Airflow's event logs record who touched what; forwarding them to your SIEM turns a post-leak shrug into a timeline. Pair that with TLS on the webserver (web_server_ssl_cert/key) and least-privilege service accounts for the scheduler, and each layer shrinks what one leaked credential can reach.

📊 Production Insight
Most leaks start as legitimate-looking edits, not attacks. Alerting on the sensitive three catches intent early. Rule: ship audit logs where nobody can edit them.
🎯 Key Takeaway
Immutable audit logs plus alerts on connection edits, role grants, and Variable changes catch risky edits while they're still reversible.
● Production incidentPOST-MORTEMseverity: high

The DAG That Leaked Database Credentials

Symptom
A routine security scan flagged a live production password in the DAG repo. Git history showed it present for months across multiple branches and forks. The same password worked against the warehouse from any network with access, and nobody could say who had viewed or copied it. Every deployment, notebook, and screenshot became part of the exposure surface.
Assumption
The team treated DAG files like application code with no special risk. Passwords in config files felt normal, the repo was private, and Airflow was an internal tool, so the threat model stopped at the login screen. Convenience beat caution on every review, and nobody asked where the warehouse password actually lived.
Root cause
Credentials were embedded directly in DAG code instead of connections, so version control became a secret store with full history. The Fernet key was the documented default shared across environments, meaning even properly stored connection passwords were decryptable by anyone with a metadata DB backup. No secrets backend existed and RBAC granted broad access, so nothing constrained the spread.
Fix
Every hardcoded credential moved into connections backed by AWS Secrets Manager, and the Fernet key was rotated using the append-first procedure. RBAC was rebuilt with per-team DAG-level grants: analysts read dev, engineers run staging, and only platform owners touch prod connections. Repo scanning joined CI so a committed secret fails the build, and the exposed password was rotated everywhere it had spread.
Key lesson
  • Connections are the only sanctioned home for credentials. Anything in code, Variables, or chat logs is a leak with a timestamp, not a secret.
  • Default encryption keys are decoration. Generate a unique Fernet key per environment, store it in a secret manager, and rotate it on a schedule.
  • Least privilege limits blast radius. Per-team DAG roles turn a compromised credential into a contained incident instead of a platform-wide breach.
Production debug guideFour credential and access failures, with the exact commands that contain each one.4 entries
Symptom · 01
A credential is found hardcoded in a DAG file
Fix
Search the repo for password, secret, token, and api_key in DAG files: grep -rniE 'password|secret|api_key|token' dags/ | grep -v conn_id. For each hit, move the value into a connection or secret backend, rotate the exposed credential immediately, then purge it from git history.
Symptom · 02
Fernet key is the default example or shared across environments
Fix
Generate a new key and check whether the current one is the documented default: python3 -c 'print(open("/dev/stdin").read())' < <(airflow config get-value core fernet_key) and compare against your secret manager. If it matches the example key or is shared across envs, rotate now following the append-first procedure.
Symptom · 03
Too many users hold the Admin role
Fix
List UI users and roles via the API: curl -s -u admin:$PW $BASE/api/v2/users | head -c 2000. Anyone with Admin outside platform engineering gets downgraded to Op or User with DAG-level grants. Verify SSO group mapping still matches the identity provider. Confirm lookup order while you're there: backend first, then AIRFLOW_CONN_/AIRFLOW_VAR_ env, then metastore — a stale env var can shadow the vault value you just rotated.
Symptom · 04
A production connection changed with no change record
Fix
Tail the webserver audit log for connection edits: grep -i 'connection' $AIRFLOW_HOME/logs/webserver/*.log | tail -20. Confirm each edit has a change ticket; revoke and rotate any connection edited outside the process.
Secret Storage Options Compared
StorageEncryptionRotation effortBest for
Airflow connections + FernetFernet key at restRe-key plus re-save connectionsSmall teams, single instance
HashiCorp Vault backendVault-managed, dynamic leasesRotate in Vault, Airflow followsRegulated orgs, dynamic creds
AWS Secrets Manager backendKMS-managed keysAutomatic rotation schedulesAWS-native fleets
GCP Secret Manager backendGoogle-managed keysVersioned secrets, easy rollbackGCP-native fleets
Environment variablesNone (plaintext in env)Redeploy every serviceLocal dev only, never prod
DAG files / gitNone (visible to all readers)Rewrite history plus rotateNever acceptable
Secrets lookup orderCreds resolving from the wrong storeBackend, env, metastore — not configurabletasks
API auth backendUI-locked but API-open deploymentsDedicated API backend, no legacy experimental APItasks
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
scriptsrotate-fernet-key.shNEW_KEY=$(python3 -c 'from cryptography.fernet import Fernet; print(Fernet.gener...Fernet
dagsbilling_reconcile.pyfrom airflow.sdk import dag, taskSecrets Backends Versus Connection Encryption
configauth-production.ymlauth_manager: SimpleAuthManager # dev onlyAuthentication

Key takeaways

1
Generate and back up the Fernet key before creating users; prefer FERNET_KEY_CMD from KMS, and rotate on a calendar with the prepend, rotate-fernet-key, drop sequence.
2
Resolve connections and variables from a secrets backend (Vault, AWS SM, GCP SM)
lookup order is backend, env, metastore, and rotation then skips deploys entirely.
3
Start every human at Viewer and grant up, with access_control on each DAG; custom team roles like FinanceTeam own their DAGs without touching Admin.
4
Put the webserver behind OAuth/LDAP via webserver_config.py, give the API its own auth backend, and set expose_config = False.
5
Ship event audit logs to your SIEM and run schedulers on least-privilege service accounts so one leak can't walk the whole platform.

Common mistakes to avoid

4 patterns
×

Running production on the default or example Fernet key

Symptom
Anyone with repo access plus the metadata DB backup can decrypt every stored connection password, because the key is public knowledge.
Fix
Generate a fresh key with python -c Fernet.generate_key, append it (comma-separated, new key first) to AIRFLOW__CORE__FERNET_KEY, re-save connections so they re-encrypt, then drop the old key. Test decrypt on staging before touching prod.
×

One shared admin connection for every DAG

Symptom
A compromised analytics DAG credential grants write access to the billing warehouse; blast radius equals the whole platform.
Fix
Create one connection per environment with scoped credentials and grant DAG-level roles per team. Audit quarterly with airflow connections list filtered by team and delete anything unused for 90 days.
×

Pasting secrets into DAG files or variables

Symptom
A routine repo audit surfaces a production password in a DAG committed 8 months ago; rotation requires touching every deployment that pulled it.
Fix
Move secrets to Vault or AWS Secrets Manager behind the Airflow secrets backend, then delete the literals from git history with a rotation (not just a revert). Scan the repo with a secrets detector in CI to prevent recurrence.
×

Giving every engineer Admin because RBAC feels slow

Symptom
A well-meaning analyst edits a production connection in the UI to debug a failure and breaks three DAGs; no approval, no trace of intent.
Fix
Map teams to the built-in roles (Viewer, User, Op, Admin) plus DAG-level access entries, and require SSO group membership for prod roles. Review role membership alongside access reviews each quarter. Cover the API backend and expose_config in the same pass — RBAC on the UI means little if the API or config view stays wide open.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the Fernet key lifecycle: what it encrypts, where it lives, and ...
Q02SENIOR
How do authentication and RBAC divide responsibilities in Airflow?
Q03JUNIOR
Why must credentials live in connections, never in DAG code?
Q01 of 03SENIOR

Explain the Fernet key lifecycle: what it encrypts, where it lives, and how you rotate it.

ANSWER
Fernet is symmetric encryption for connection passwords and sensitive Variables stored in the metadata DB. The key lives in AIRFLOW__CORE__FERNET_KEY and must be unique per environment, stored in a secret manager, and rotated by appending the new key first, re-saving connections, then dropping the old key. The incident happened because the key was the documented default, so the encrypted passwords were effectively plaintext to anyone with a DB backup.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What does the Fernet key actually protect?
02
How do I rotate the Fernet key without downtime?
03
Can I use Vault and Airflow connections together?
04
Which RBAC roles should a 20-person data team use?
05
How do I detect the next credential leak early?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
September 04, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

3 min read · try the examples if you haven't

Previous
Airflow Performance Tuning
31 / 37 · Airflow
Next
Airflow Data Quality Gates