Securing the API Server and UI

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:

s2.sh (excerpt): the same four requests with two users' tokensShell
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
Output
{"sub":"2","role":null,"lifetime_s":86400}
ADMIN    dags 200  connections 200  config 403  trigger 200
ANALYST  dags 200  connections 403  config 403  trigger 403

The 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).