harden.sh writes a fresh .env (mode 600) before the stack starts, replacing three known values: a Fernet key, which encrypts connection passwords and variables in the metadata database (rotate it by setting new,old and running airflow rotate-fernet-key); a random JWT secret, which signs every API token and must match on all components; and a random admin password. s2.sh then sends four requests as the admin and as a Viewer called analyst:
code() { curl -s -o /dev/null -w "%{http_code}" "${@:2}" -H "Authorization: Bearer $1"; }
for who in ADMIN ANALYST; do
T=${!who}
printf '%-8s dags %s connections %s config %s trigger %s\n' $who \
$(code $T $B/api/v2/dags) $(code $T $B/api/v2/connections) $(code $T $B/api/v2/config) \
$(code $T -X POST $B/api/v2/dags/booknest_secure/dagRuns \
-H 'Content-Type: application/json' -d '{"logical_date": null}')
done{"sub":"2","role":null,"lifetime_s":86400}
ADMIN dags 200 connections 200 config 403 trigger 200
ANALYST dags 200 connections 403 config 403 trigger 403The first line, the analyst's decoded token, shows the default [api_auth] jwt_expiration_time: a leaked token works for a day, so shorten it. Even the admin cannot read the configuration, which holds the database URL, because [api] expose_config defaults to False. Finally, publish the API server only behind a reverse proxy that terminates TLS, leave /api/v2/monitor/health as the one anonymous endpoint, and sign people in through your identity provider (FAB's OAuth or LDAP, or the Keycloak 44,186 auth manager).