Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Support for configuring which reverse proxies the webserver trusts `X-Forwarded-*` headers from, via `spec.webservers.roleConfig.trustedProxies` ([#835]).

### Changed

- Internal operator refactoring: introduce a build() step in the reconciler that
Expand All @@ -16,6 +20,7 @@
[#821]: https://github.com/stackabletech/airflow-operator/pull/821
[#827]: https://github.com/stackabletech/airflow-operator/pull/827
[#828]: https://github.com/stackabletech/airflow-operator/pull/828
[#835]: https://github.com/stackabletech/airflow-operator/pull/835

## [26.7.0] - 2026-07-21

Expand Down
78 changes: 78 additions & 0 deletions docs/modules/airflow/pages/usage-guide/reverse-proxy.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
= Running behind a reverse proxy
:description: Configure which reverse proxies the Airflow webserver trusts X-Forwarded-* headers from.

When the Airflow webserver is reached through an ingress controller or another reverse proxy, every
request arrives from the proxy rather than from the client. Unless the webserver is told to trust
the proxy, it records the proxy's address as the client address, and it treats a TLS-terminated
request as plain HTTP -- which, among other things, means session cookies are not marked as
`Secure`.

Set `trustedProxies` on the webserver role to the addresses your proxy sends requests from:

[source,yaml]
----
spec:
webservers:
roleConfig:
listenerClass: external-stable
trustedProxies:
- 10.244.0.0/16 # <1>
roleGroups:
default:
replicas: 1
----

<1> IP addresses (`10.0.0.1`), CIDR networks (`10.244.0.0/16`), and `*` are accepted. `*` must be
the only entry in the list: combining it with other entries is rejected, since it would silently
degrade to trusting only those other entries.

On Airflow 3.x the operator starts the api-server with `--proxy-headers` and restricts the headers
to the listed peers. Airflow 2.x has no equivalent restriction: see
<<airflow-2-x-has-no-peer-restriction>>.

== Choosing the value

Use the narrowest range that covers your proxy, and give it as a network address (host bits zero,
e.g. `10.244.0.0/16`) or as a bare host IP (no `/prefix` at all, e.g. `10.244.0.5`).

For an ingress controller running in the cluster, list the Pod addresses as bare host IPs rather
than guessing a network:

[source,bash]
----
kubectl get pods -n ingress-nginx -o jsonpath='{.items[*].status.podIP}'
----

If your ingress controller's Pods share a known, stable CIDR (for example a dedicated node pool or
a documented Pod CIDR range for that namespace), you can use that network instead -- but confirm it
against your cluster's actual CNI configuration rather than inferring it from a single Pod IP,
since a Pod IP alone does not tell you where the network boundary is.

`*` trusts forwarded headers from any peer that can reach the webserver. Only use it when access
to the webserver is restricted by other means -- with a `cluster-internal` or
`external-unstable` ListenerClass, clients reach the Pod directly and can set the headers
themselves.

WARNING: A peer that is trusted can set the client address that ends up in the webserver's access
log. Do not list ranges wider than the proxies you operate.

[#airflow-2-x-has-no-peer-restriction]
== Airflow 2.x has no peer restriction

Airflow 2.x's webserver has no analogue support for trusted network addresses. Once enabled, it
unconditionally trusts `X-Forwarded-*` from *any* peer, the same as `trustedProxies: ["*"]` on 3.x.

This means that for Airflow 2.x, `trustedProxies: ["*"]` is the only valid configuration. Using
any other value than `*` will lead the operator to reject reconciliation.

== Interaction with overrides

On Airflow 3.x, the operator sets `FORWARDED_ALLOW_IPS` from this field, before `envOverrides` are
applied -- so an `envOverrides` entry for that variable wins over the value derived from
`trustedProxies`. This override only takes effect while `trustedProxies` is non-empty: an empty
list means the api-server is not started with `--proxy-headers` at all, so uvicorn never installs
the middleware that reads `FORWARDED_ALLOW_IPS`, and the override has no effect regardless of its
value.

On Airflow 2.x, the same applies to `AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX` and
`AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR`.
1 change: 1 addition & 0 deletions docs/modules/airflow/partials/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
** xref:airflow:usage-guide/mounting-dags.adoc[]
** xref:airflow:usage-guide/applying-custom-resources.adoc[]
** xref:airflow:usage-guide/listenerclass.adoc[]
** xref:airflow:usage-guide/reverse-proxy.adoc[]
** xref:airflow:usage-guide/storage-resources.adoc[]
** xref:airflow:usage-guide/security.adoc[]
** xref:airflow:usage-guide/logging.adoc[]
Expand Down
38 changes: 38 additions & 0 deletions extra/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5648,6 +5648,7 @@ spec:
podDisruptionBudget:
enabled: true
maxUnavailable: null
trustedProxies: []
description: This is a product-agnostic RoleConfig, which is sufficient for most of the products.
properties:
listenerClass:
Expand Down Expand Up @@ -5688,6 +5689,24 @@ spec:
nullable: true
type: integer
type: object
trustedProxies:
default: []
description: |-
Enable trusted proxies when Airflow is deployed behind a reverse proxy like Istio or nginx.

The reverse proxies whose `X-Forwarded-*` headers the webserver trusts, as IP addresses
(`10.0.0.1`), CIDR networks (`10.244.0.0/16`), or `*` for every peer. `*` must be the
only entry in the list if used: combining it with other entries is rejected, since it
would silently degrade to trusting only those other entries.

On Airflow 3.x this restricts trust to the listed peers. On 2.x it only switches
forwarded-header handling on or off: any non-empty list trusts every peer, the same as
`*`, since Airflow 2.x has no way to restrict it further.

Leave this empty (the default) and forwarded headers are ignored entirely.
items:
type: string
type: array
type: object
roleGroups:
additionalProperties:
Expand Down Expand Up @@ -11854,6 +11873,7 @@ spec:
podDisruptionBudget:
enabled: true
maxUnavailable: null
trustedProxies: []
description: This is a product-agnostic RoleConfig, which is sufficient for most of the products.
properties:
listenerClass:
Expand Down Expand Up @@ -11894,6 +11914,24 @@ spec:
nullable: true
type: integer
type: object
trustedProxies:
default: []
description: |-
Enable trusted proxies when Airflow is deployed behind a reverse proxy like Istio or nginx.

The reverse proxies whose `X-Forwarded-*` headers the webserver trusts, as IP addresses
(`10.0.0.1`), CIDR networks (`10.244.0.0/16`), or `*` for every peer. `*` must be the
only entry in the list if used: combining it with other entries is rejected, since it
would silently degrade to trusting only those other entries.

On Airflow 3.x this restricts trust to the listed peers. On 2.x it only switches
forwarded-header handling on or off: any non-empty list trusts every peer, the same as
`*`, since Airflow 2.x has no way to restrict it further.

Leave this empty (the default) and forwarded headers are ignored entirely.
items:
type: string
type: array
type: object
roleGroups:
additionalProperties:
Expand Down
Loading