Truke KF carries no SAML, LDAP, Kerberos, or MFA code of its own. When your organisation authenticates with one of those, you put a reverse proxy in front of KF: the proxy terminates the enterprise protocol, verifies the user, and passes the verified identity to KF as a small set of HTTP headers. This is the header channel introduced on the authentication page; this page is the ops-facing reference — copyable configuration for the three proxies KF deployments front with: nginx, Caddy, and Apache httpd.
On every request to KF the proxy must do four things:
X-Auth-Secret).The secret header is the security boundary — not the strip. KF rejects any
request whose X-Auth-Secret is absent or wrong (returning 403), so a client
that forges X-Authenticated-User still cannot get in: it does not know the
secret. The strip in step 4 is defence in depth.
That separation matters because the strip is easy to get wrong. Caddy's
forward_auth + copy_headers, for instance, does not strip a
client-supplied identity header — the header is only overwritten when the auth
service returns it, so a missing claim lets the client's value pass through
(CVE-2026-30851, stable releases from Caddy v2.10.0). The configurations below
strip explicitly and never rely on the auth flow to do it for them.
127.0.0.1:8080) — or to a private interface
firewalled so the proxy is the only reachable path. If KF is reachable
directly, the header channel is forgeable no matter how the proxy is configured.openssl rand -hex 32 # export as KF_HEADER_SECRET on both sides
KF reads it via shared_secret_env KF_HEADER_SECRET in its [auth] section;
the proxy injects that value as X-Auth-Secret.
The proxy injects five headers. Four carry the verified identity; the fifth is the shared secret:
| Injected header | Source claim (OIDC example) | Becomes in KF |
|---|---|---|
X-Authenticated-User | preferred_username | the uid |
X-Authenticated-Email | email | the email |
X-Authenticated-Name | name | the display name |
X-Authenticated-Groups | groups | ACL labels (only if the group map is on) |
X-Auth-Secret | the shared secret (constant) | — |
X-Authenticated-User carries the value KF derives the uid from — keep it
stable and space-free. The header names above are the defaults; they are
configurable in the KF [auth] header block (user_header, email_header,
name_header, groups_header, secret_header).
Pairs with oauth2-proxy (OIDC / OAuth2) on 127.0.0.1:4180. For SAML, front
nginx with a SAML-capable gateway (Authelia / Keycloak) via the same
auth_request pattern, or use Apache instead — nginx open-source and oauth2-proxy
do not speak SAML.
Strip mechanism: in nginx, proxy_set_header overwrites the client's header
toward the upstream, so setting every trusted header explicitly is itself the
strip. A header set to an empty value is omitted, never taken from the client.
# /etc/nginx/conf.d/kf.conf
# KF is on 127.0.0.1:8080 and firewalled; nginx is the only path to it.
server {
listen 443 ssl;
http2 on;
server_name kf.example.com;
ssl_certificate /etc/ssl/kf/fullchain.pem;
ssl_certificate_key /etc/ssl/kf/privkey.pem;
# oauth2-proxy endpoints
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location = /oauth2/auth {
internal;
proxy_pass http://127.0.0.1:4180;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
auth_request /oauth2/auth;
error_page 401 = @signin;
# Capture verified identity from oauth2-proxy
# (run oauth2-proxy with --set-xauthrequest).
auth_request_set $kf_user $upstream_http_x_auth_request_preferred_username;
auth_request_set $kf_email $upstream_http_x_auth_request_email;
auth_request_set $kf_name $upstream_http_x_auth_request_user;
auth_request_set $kf_groups $upstream_http_x_auth_request_groups;
# Secret. Keep this out of world-readable config; restrict file perms.
set $kf_secret "REPLACE_WITH_SECRET";
# Inject trusted headers. Each set OVERWRITES any client-supplied copy.
proxy_set_header X-Authenticated-User $kf_user;
proxy_set_header X-Authenticated-Email $kf_email;
proxy_set_header X-Authenticated-Name $kf_name;
proxy_set_header X-Authenticated-Groups $kf_groups;
proxy_set_header X-Auth-Secret $kf_secret;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
location @signin {
return 302 /oauth2/start?rd=$scheme://$host$request_uri;
}
}
Pairs with oauth2-proxy via forward_auth. For SAML, point forward_auth at
a SAML-capable gateway (Authelia / Keycloak) and adjust the copied header names.
Strip mechanism: because copy_headers cannot be trusted to strip (see the
CVE note above), the client headers are deleted first with request_header -…,
inside a route block that pins execution order (strip → authenticate → proxy).
# Caddyfile
# KF is on 127.0.0.1:8080 and firewalled; Caddy is the only path to it.
# Run Caddy with KF_HEADER_SECRET set in the environment.
kf.example.com {
route {
# 1. Strip client-supplied trusted headers, unconditionally.
request_header -X-Authenticated-User
request_header -X-Authenticated-Email
request_header -X-Authenticated-Name
request_header -X-Authenticated-Groups
request_header -X-Auth-Secret
# 2. Authenticate, then copy verified identity onto the clean request.
forward_auth 127.0.0.1:4180 {
uri /oauth2/auth
copy_headers X-Auth-Request-Preferred-Username>X-Authenticated-User \
X-Auth-Request-Email>X-Authenticated-Email \
X-Auth-Request-User>X-Authenticated-Name \
X-Auth-Request-Groups>X-Authenticated-Groups
}
# 3. Inject the secret (overwrites) and proxy to KF.
reverse_proxy 127.0.0.1:8080 {
header_up X-Auth-Secret {env.KF_HEADER_SECRET}
}
}
}
Run oauth2-proxy with --set-xauthrequest so the identity headers are always
present for authenticated requests. Because the strip runs first and the secret
gate stands behind it, a placeholder or missing claim fails closed at KF rather
than leaking a client value.
Pairs with mod_auth_openidc (OIDC). For SAML, swap in mod_auth_mellon —
its attributes surface as MELLON_ environment variables, so only the
RequestHeader set … %{MELLON_}e lines change.
Strip mechanism: RequestHeader unset deletes the client header;
RequestHeader set overwrites. Claims are exposed as environment variables and
read with %{VAR}e.
# KF is on 127.0.0.1:8080 and firewalled; Apache is the only path to it. # Modules: mod_proxy mod_proxy_http mod_headers mod_auth_openidc # Make the OS secret visible to Apache: PassEnv KF_HEADER_SECRETServerName kf.example.com SSLEngine on SSLCertificateFile /etc/ssl/kf/fullchain.pem SSLCertificateKeyFile /etc/ssl/kf/privkey.pem OIDCProviderMetadataURL https://idp.example.com/.well-known/openid-configuration OIDCClientID kf-app OIDCClientSecret "REPLACE" OIDCRedirectURI https://kf.example.com/oauth2callback OIDCCryptoPassphrase "REPLACE" OIDCPassClaimsAs environment # claims as OIDC_CLAIM_* env vars AuthType openid-connect Require valid-user # 1. Strip client-supplied trusted headers. RequestHeader unset X-Authenticated-User RequestHeader unset X-Authenticated-Email RequestHeader unset X-Authenticated-Name RequestHeader unset X-Authenticated-Groups RequestHeader unset X-Auth-Secret # 2. Inject verified identity (set = overwrite). RequestHeader set X-Authenticated-User "%{OIDC_CLAIM_preferred_username}e" RequestHeader set X-Authenticated-Email "%{OIDC_CLAIM_email}e" RequestHeader set X-Authenticated-Name "%{OIDC_CLAIM_name}e" RequestHeader set X-Authenticated-Groups "%{OIDC_CLAIM_groups}e" # 3. Inject the secret. RequestHeader set X-Auth-Secret "%{KF_HEADER_SECRET}e" ProxyPass http://127.0.0.1:8080/ ProxyPassReverse http://127.0.0.1:8080/
If a claim env var is unset when RequestHeader runs, set OIDCPassClaimsAs both
and confirm the header lands; the env-var read is the cleaner path when it works.
Run these against a live deployment — they test the boundary, not the happy path:
# A. Forged identity, no secret -> never authenticated as the forged user. # Expect a redirect to login or 403; never a logged-in session for 'attacker'. curl -is https://kf.example.com/ -H 'X-Authenticated-User: attacker' | head -n1 # B. Forged identity AND a wrong secret -> 403. curl -is https://kf.example.com/ \ -H 'X-Authenticated-User: attacker' \ -H 'X-Auth-Secret: wrong' | head -n1 # C. Direct hit bypassing the proxy -> refused (KF binds to loopback). # From any host other than the KF host: curl -is http://KF_HOST:8080/ -H 'X-Authenticated-User: attacker'
Pass criteria: A and B never produce a session for attacker, B returns
403, and C cannot connect. If C connects from another host, KF is not bound to
loopback and the channel is unsafe — fix the bind before anything else.
127.0.0.1 (or private + firewalled); not directly reachable.openssl rand -hex 32, identical on both sides, and
stored out of world-readable config.X-Auth-Secret are stripped, then set.X-Authenticated-User carries a stable, space-free value.--set-xauthrequest for oauth2-proxy; OIDCPassClaimsAs for
mod_auth_openidc).[auth] header block: enabled true, secret_header X-Auth-Secret,
shared_secret_env KF_HEADER_SECRET, trusted_proxy set, and a jit policy
chosen.This page covers the trusted-header channel only. Native single sign-on (OIDC with no proxy) and bearer-token access for the REST API and the MCP endpoint need none of this — see authentication & access.