Autenticación con proxy

Truke KF no incorpora código propio de SAML, LDAP, Kerberos ni MFA. Cuando tu organización se autentica con alguno de esos protocolos, colocas un proxy inverso delante de KF: el proxy termina el protocolo corporativo, verifica al usuario y pasa la identidad verificada a KF como un pequeño conjunto de cabeceras HTTP. Este es el canal de cabeceras que se presenta en la página de autenticación; esta página es la referencia para operaciones —configuración lista para copiar— de los tres proxies que suelen anteponerse a KF: nginx, Caddy y Apache httpd.

Cómo funciona el modelo de confianza

En cada petición hacia KF, el proxy debe hacer cuatro cosas:

  1. Autenticar al usuario aguas arriba (OIDC / SAML / LDAP / MFA).
  2. Inyectar las cuatro cabeceras de identidad derivadas del resultado verificado.
  3. Inyectar la cabecera de secreto estático (X-Auth-Secret).
  4. Eliminar cualquier copia de las anteriores que envíe el cliente.

La cabecera de secreto es la frontera de seguridad, no la eliminación. KF rechaza toda petición cuyo X-Auth-Secret falte o sea incorrecto (devolviendo 403), de modo que un cliente que falsifique X-Authenticated-User sigue sin poder entrar: no conoce el secreto. La eliminación del paso 4 es defensa en profundidad.

Esa separación importa porque la eliminación es fácil de hacer mal. El forward_auth + copy_headers de Caddy, por ejemplo, no elimina una cabecera de identidad suministrada por el cliente —la cabecera solo se sobrescribe cuando el servicio de autenticación la devuelve, así que un claim ausente deja pasar el valor del cliente (CVE-2026-30851, versiones estables desde Caddy v2.10.0). Las configuraciones de abajo eliminan explícitamente y nunca confían en el flujo de autenticación para hacerlo.

Dos requisitos previos para cualquier proxy

  • Enlaza KF solo a loopback (127.0.0.1:8080) —o a una interfaz privada protegida por cortafuegos de modo que el proxy sea la única vía de acceso. Si KF es alcanzable directamente, el canal de cabeceras es falsificable por muy bien que esté configurado el proxy.
  • Comparte un secreto de alta entropía entre el proxy y KF:
openssl rand -hex 32        # expórtalo como KF_HEADER_SECRET en ambos lados

KF lo lee mediante shared_secret_env KF_HEADER_SECRET en su sección [auth]; el proxy inyecta ese valor como X-Auth-Secret.

El contrato de cabeceras

El proxy inyecta cinco cabeceras. Cuatro llevan la identidad verificada; la quinta es el secreto compartido:

Cabecera inyectadaClaim de origen (ejemplo OIDC)Se convierte en KF en
X-Authenticated-Userpreferred_usernameel uid
X-Authenticated-Emailemailel correo
X-Authenticated-Namenameel nombre visible
X-Authenticated-Groupsgroupsetiquetas ACL (solo si el mapa de grupos está activo)
X-Auth-Secretel secreto compartido (constante)

X-Authenticated-User lleva el valor del que KF deriva el uid —mantenlo estable y sin espacios. Los nombres de cabecera de arriba son los predeterminados; se configuran en el bloque header de la sección [auth] de KF (user_header, email_header, name_header, groups_header, secret_header).

nginx

Se combina con oauth2-proxy (OIDC / OAuth2) en 127.0.0.1:4180. Para SAML, antepón a nginx una pasarela con soporte SAML (Authelia / Keycloak) mediante el mismo patrón auth_request, o usa Apache en su lugar —nginx open-source y oauth2-proxy no hablan SAML.

Mecanismo de eliminación: en nginx, proxy_set_header sobrescribe la cabecera del cliente hacia el upstream, así que fijar explícitamente cada cabecera de confianza es en sí mismo la eliminación. Una cabecera con valor vacío se omite, nunca se toma del cliente.

# /etc/nginx/conf.d/kf.conf
# KF está en 127.0.0.1:8080 y protegido por cortafuegos; nginx es la única vía.

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;

    # Endpoints de oauth2-proxy
    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;

        # Captura la identidad verificada de oauth2-proxy
        # (ejecuta oauth2-proxy con --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;

        # Secreto. Mantenlo fuera de config legible por todos; restringe permisos.
        set $kf_secret "REPLACE_WITH_SECRET";

        # Inyecta las cabeceras de confianza. Cada set SOBRESCRIBE la copia del cliente.
        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;
    }
}

Caddy

Se combina con oauth2-proxy mediante forward_auth. Para SAML, apunta forward_auth a una pasarela con soporte SAML (Authelia / Keycloak) y ajusta los nombres de las cabeceras copiadas.

Mecanismo de eliminación: como no se puede confiar en copy_headers para eliminar (véase la nota del CVE más arriba), las cabeceras del cliente se borran primero con request_header -…, dentro de un bloque route que fija el orden de ejecución (eliminar → autenticar → proxy).

# Caddyfile
# KF está en 127.0.0.1:8080 y protegido por cortafuegos; Caddy es la única vía.
# Ejecuta Caddy con KF_HEADER_SECRET definido en el entorno.

kf.example.com {
    route {
        # 1. Elimina incondicionalmente las cabeceras de confianza del cliente.
        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. Autentica y copia la identidad verificada sobre la petición limpia.
        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. Inyecta el secreto (sobrescribe) y reenvía a KF.
        reverse_proxy 127.0.0.1:8080 {
            header_up X-Auth-Secret {env.KF_HEADER_SECRET}
        }
    }
}

Ejecuta oauth2-proxy con --set-xauthrequest para que las cabeceras de identidad estén siempre presentes en las peticiones autenticadas. Como la eliminación va primero y detrás está la puerta del secreto, un valor de relleno o un claim ausente falla de forma cerrada en KF en vez de filtrar un valor del cliente.

Apache httpd

Se combina con mod_auth_openidc (OIDC). Para SAML, cambia a mod_auth_mellon —sus atributos aparecen como variables de entorno MELLON_, así que solo cambian las líneas RequestHeader set … %{MELLON_}e.

Mecanismo de eliminación: RequestHeader unset borra la cabecera del cliente; RequestHeader set la sobrescribe. Los claims se exponen como variables de entorno y se leen con %{VAR}e.

# KF está en 127.0.0.1:8080 y protegido por cortafuegos; Apache es la única vía.
# Módulos: mod_proxy mod_proxy_http mod_headers mod_auth_openidc
# Haz visible el secreto del sistema para Apache:
PassEnv KF_HEADER_SECRET


    ServerName 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 como variables OIDC_CLAIM_*

    
        AuthType openid-connect
        Require  valid-user

        # 1. Elimina las cabeceras de confianza del cliente.
        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. Inyecta la identidad verificada (set = sobrescribir).
        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. Inyecta el secreto.
        RequestHeader set X-Auth-Secret "%{KF_HEADER_SECRET}e"

        ProxyPass        http://127.0.0.1:8080/
        ProxyPassReverse http://127.0.0.1:8080/
    

Si una variable de entorno de claim no está definida cuando se ejecuta RequestHeader, usa OIDCPassClaimsAs both y confirma que la cabecera llega; la lectura de la variable de entorno es la vía más limpia cuando funciona.

Verifica el despliegue

Ejecuta estas comprobaciones contra un despliegue en vivo —prueban la frontera, no el caso feliz:

# A. Identidad falsificada, sin secreto -> nunca autenticado como el usuario falso.
#    Se espera una redirección a login o 403; nunca una sesión para 'attacker'.
curl -is https://kf.example.com/ -H 'X-Authenticated-User: attacker' | head -n1

# B. Identidad falsificada Y un secreto incorrecto -> 403.
curl -is https://kf.example.com/ \
  -H 'X-Authenticated-User: attacker' \
  -H 'X-Auth-Secret: wrong' | head -n1

# C. Acceso directo saltándose el proxy -> rechazado (KF enlaza a loopback).
#    Desde cualquier host que no sea el de KF:
curl -is http://KF_HOST:8080/ -H 'X-Authenticated-User: attacker'

Criterios de aprobado: A y B nunca producen una sesión para attacker, B devuelve 403 y C no puede conectar. Si C conecta desde otro host, KF no está enlazado a loopback y el canal es inseguro —corrige el enlace antes que nada.

Lista de comprobación

  • KF enlazado a 127.0.0.1 (o privado + cortafuegos); no alcanzable directamente.
  • Secreto generado con openssl rand -hex 32, idéntico en ambos lados y almacenado fuera de config legible por todos.
  • Las cuatro cabeceras de identidad más X-Auth-Secret se eliminan y luego se fijan.
  • X-Authenticated-User lleva un valor estable y sin espacios.
  • El terminador de autenticación devuelve las cabeceras de identidad en cada petición autenticada (--set-xauthrequest para oauth2-proxy; OIDCPassClaimsAs para mod_auth_openidc).
  • Las verificaciones A–C pasan.
  • Bloque header de [auth] en KF: enabled true, secret_header X-Auth-Secret, shared_secret_env KF_HEADER_SECRET, trusted_proxy definido y una política jit elegida.

Esta página cubre solo el canal de cabeceras de confianza. El inicio de sesión único nativo (OIDC sin proxy) y el acceso por token para la API REST y el endpoint MCP no necesitan nada de esto —véase autenticación y acceso.