Skip to main content

TLS and HTTP headers

The standalone server (autoair serve, and autoair local) handles TLS, reverse proxies, security headers, CORS and request limits itself. All settings are in the server section and, for CORS, at the top level (AUTOAIR_CORS_ORIGINS, AUTOAIR_CORS_ALLOW_METHODS, AUTOAIR_CORS_ALLOW_HEADERS, AUTOAIR_CORS_EXPOSE_HEADERS, AUTOAIR_CORS_MAX_AGE, AUTOAIR_CORS_ALLOW_CREDENTIALS).

Plugin mode

Inside Airflow (plugin mode), Airflow's API server owns TLS, proxy handling, CORS and response headers; AutoAir adds no security headers there. Configure those in Airflow. The request-body limit still applies to AutoAir's routes.

TLS for outgoing connections (a corporate CA for Airflow, git hosts and AI providers, client certificates, egress proxies) is configured separately in the outbound section; see Configuration.

HTTPS​

autoair serve runs uvicorn. Give it a certificate chain and a private key to serve HTTPS directly:

autoair serve --ssl-certfile /etc/autoair/tls.pem --ssl-keyfile /etc/autoair/tls.key
# or
AUTOAIR_SERVER__SSL_CERTFILE=/etc/autoair/tls.pem
AUTOAIR_SERVER__SSL_KEYFILE=/etc/autoair/tls.key
AUTOAIR_SERVER__SSL_KEYFILE_PASSWORD=... # only for an encrypted key; no CLI option
AUTOAIR_SERVER__SSL_MIN_VERSION=TLSv1.3 # default TLSv1.2
AUTOAIR_SERVER__SSL_CIPHERS='ECDHE+AESGCM:ECDHE+CHACHA20' # default: Python's secure set
SettingCLI optionDefaultDescription
server.ssl_certfile--ssl-certfileunsetCertificate chain (PEM); enables HTTPS
server.ssl_keyfile--ssl-keyfileunsetPrivate key (PEM)
server.ssl_keyfile_passwordnone (secret)unsetPassword of an encrypted key
server.ssl_min_versionnoneTLSv1.2Lowest TLS version: TLSv1.2 or TLSv1.3 (TLSv1.3 needs uvicorn 0.47 or newer; Airflow 3.0 and 3.1 constraints install older ones)
server.ssl_ciphersnonePython's defaultOpenSSL cipher string (TLS 1.2 suites)
server.ssl_ca_certs--ssl-ca-certsunsetCA bundle that client certificates are verified against
server.ssl_cert_reqs--ssl-cert-reqsnoneClient certificates: none, optional, required

Mutual TLS​

To require client certificates, set the CA that issues them and required (or optional to accept clients without one):

autoair serve --ssl-certfile /etc/autoair/tls.pem --ssl-keyfile /etc/autoair/tls.key \
--ssl-ca-certs /etc/autoair/clients-ca.pem --ssl-cert-reqs required

Client certificates authenticate the TLS connection only. Users are still identified by their Airflow session or token (SSO and sessions).

Configuration errors​

Inconsistent TLS options stop the server before it binds (exit code 2):

  • server.ssl_cert_reqs needs server.ssl_ca_certs (mutual TLS): optional or required without a CA bundle.
  • server.ssl_keyfile / ssl_cert_reqs need server.ssl_certfile: a key or client-certificate requirement without a certificate.

Paths may use ~. Certificates are read at start-up: renewing them needs a restart (a rolling restart with several replicas).

TLS at a proxy instead​

Terminating TLS at a load balancer or ingress is equally supported. AutoAir then listens on plain HTTP inside the network, and the proxy must be trusted for X-Forwarded-Proto so that AutoAir knows the original request was HTTPS (for Secure cookies, HSTS and links). See the next section.

Reverse proxies and forwarded headers​

SettingCLI optionDefaultDescription
server.proxy_headersnonetrueTrust X-Forwarded-Proto and X-Forwarded-For from the addresses below
server.forwarded_allow_ips--forwarded-allow-ips (comma-separated)["127.0.0.1"]Proxy addresses whose X-Forwarded-* headers are trusted; * trusts every peer
server.public_urlnoneunsetAutoAir's public base URL as users reach it, for example https://autoair.example.com or https://airflow.example.com/autoair

With the defaults, only a proxy on the same host is trusted. Behind a load balancer or an ingress controller, list its address (or the pod network range when it is not stable):

AUTOAIR_SERVER__FORWARDED_ALLOW_IPS='["10.0.0.10"]'
# or
autoair serve --forwarded-allow-ips 10.0.0.10,10.0.0.11

What depends on it:

  • the client IP used by the sign-in rate limiter and, for unauthenticated calls, the API rate limiter. Without a trusted proxy, every user shares the proxy's address and one user's failures lock everybody out;
  • the scheme: server.cookie_secure=auto sets Secure and HSTS is sent only when the request is known to be HTTPS.

Use * only when the proxy strips incoming X-Forwarded-* headers from clients; otherwise clients can spoof their address.

server.public_url is not derived from headers. Set it whenever AutoAir is behind a proxy: it is used for the return URL of "Sign in with Airflow" and for the link back to a DAG in commits and pull requests (unset: no links).

Security headers​

With server.security_headers=true (the default), the standalone server adds these headers to every response that does not set them itself:

HeaderValueSetting
Content-Security-Policythe built-in policy below, or your ownserver.content_security_policy, server.frame_ancestors
X-Content-Type-Optionsnosniffalways
Referrer-Policystrict-origin-when-cross-originserver.referrer_policy (empty: not sent)
X-Frame-OptionsSAMEORIGIN when frame_ancestors is ["'self'"], DENY when it is ["'none'"], not sent otherwiseserver.frame_ancestors
Strict-Transport-Securitymax-age=<hsts_seconds> (+ includeSubDomains, preload) on HTTPS responses onlyserver.hsts_seconds, server.hsts_include_subdomains, server.hsts_preload

uvicorn's Server header is not sent.

Content-Security-Policy​

The built-in policy is written for the single-page app:

default-src 'self';
script-src 'self' 'sha256-<hash of the page's inline configuration script>';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data:;
connect-src 'self';
worker-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self'
  • Scripts are limited to AutoAir's own files plus the exact inline configuration script of the UI page, allowed by its SHA-256 hash.
  • Inline styles are allowed because Chakra UI (emotion) and CodeMirror inject <style> elements at runtime.
  • img-src https: lets ui.logo_url point to any HTTPS image.
  • frame-ancestors comes from server.frame_ancestors.

To allow embedding the UI in a portal, add its origin:

AUTOAIR_SERVER__FRAME_ANCESTORS="[\"'self'\", \"https://portal.example.com\"]"
# the same in YAML
server:
frame_ancestors: ["'self'", "https://portal.example.com"]

["'none'"] forbids embedding entirely.

server.content_security_policy replaces the whole header with your own string. frame_ancestors is then ignored (put frame-ancestors in your policy), no X-Frame-Options is sent, and the UI page's inline-script hash is no longer added: your policy must allow the UI's inline configuration script or the UI will not start. Test a custom policy in a browser before rolling it out.

HSTS​

Strict-Transport-Security is off by default (server.hsts_seconds=0). Enable it only when every client reaches AutoAir over HTTPS, for every host name it is served on:

AUTOAIR_SERVER__HSTS_SECONDS=31536000
AUTOAIR_SERVER__HSTS_INCLUDE_SUBDOMAINS=false

HSTS is sent only on responses AutoAir knows were served over HTTPS (directly, or through a trusted proxy that sets X-Forwarded-Proto: https).

Disabling the headers​

AUTOAIR_SERVER__SECURITY_HEADERS=false removes all of them, for example when a proxy in front sets its own. /api/config and autoair config show mask this setting's value as *** because its name ends in headers (see Secrets).

CORS​

Cross-origin requests are refused unless you list origins. CORS applies to the standalone server only.

SettingDefaultDescription
AUTOAIR_CORS_ORIGINS[]Allowed origins, for example ["https://portal.example.com"]
AUTOAIR_CORS_ALLOW_METHODSGET, POST, PUT, PATCH, DELETE, OPTIONSMethods allowed cross-origin
AUTOAIR_CORS_ALLOW_HEADERSAuthorization, Content-Type, If-Match, X-AutoAir-CSRF, X-Requested-WithRequest headers allowed cross-origin
AUTOAIR_CORS_EXPOSE_HEADERSETag, Retry-AfterResponse headers readable cross-origin
AUTOAIR_CORS_MAX_AGE600Seconds browsers cache a preflight
AUTOAIR_CORS_ALLOW_CREDENTIALStrueAllow cookies and credentials cross-origin
  • With ["*"], Access-Control-Allow-Credentials is never sent, whatever cors_allow_credentials says: a wildcard would otherwise reflect any origin with credentials. List explicit origins to allow credentialed calls.
  • Origins in cors_origins are also accepted by the CSRF check for cookie-authenticated state-changing requests.
  • ETag and If-Match are part of the defaults because spec saves use optimistic locking.

Request limits and compression​

SettingDefaultDescription
server.max_body_bytes16777216 (16 MiB)Largest request body; above it 413 with request body larger than N bytes. Checked on Content-Length and while streaming. Per-feature limits (for example interchange.max_source_chars) still apply
server.limit_concurrencyunsetMost concurrent connections before uvicorn answers 503
server.keep_alive_timeout5Idle seconds before a keep-alive connection closes
server.graceful_shutdown_timeout30Seconds in-flight requests get on shutdown
server.gzip_min_size, server.gzip_level2048, 6Responses above this size are gzip-compressed
server.static_max_age31536000Cache-Control max-age of content-hashed UI assets (immutable); entry files are always revalidated

Per-user API rate limits (limits.*, 429 with Retry-After) are described in Configuration.

Cookies​

Attributes of the cookies AutoAir sets (server.cookie_secure, server.cookie_samesite) and the CSRF header rule are described in SSO and sessions.

Production example​

autoair serve --workers 4 \
--ssl-certfile /etc/autoair/tls.pem --ssl-keyfile /etc/autoair/tls.key
AUTOAIR_SERVER__PUBLIC_URL=https://autoair.acme.io
AUTOAIR_SERVER__FORWARDED_ALLOW_IPS='["10.0.0.10"]' # the load balancer
AUTOAIR_SERVER__HSTS_SECONDS=31536000 # only when HTTPS everywhere
AUTOAIR_SERVER__FRAME_ANCESTORS="[\"'none'\"]" # never embedded
AUTOAIR_SERVER__COOKIE_SECURE=always

See also Standalone behind a proxy and the production checklist.