NPM con OAuth2 Authorization Code usando OAuth2-Proxy

Fecha publicación
Dificultad
Avanzado
#
13
3 more properties
Publicado
tags
IoT, Gateways, logs, métricas, plataforma, Grafana, Alloy, Docker, sistema, Mimir, Loki, MinIO, almacenamiento, AWS, S3, visualización, monitoreo, captura, servidores, integración, DB, BBDD, servers, integration, metrics, systems, logs, keycloak, oauth2, oauth2-client-credentials, OAuth2-Proxy, Identity Provider, Authoritzation Code

vídeo local

Descripción

Se describe al detalle cómo Nginx Proxy Manager (NPM) controla y delega la autenticación del la web de usuario de Grafana MIMIR a Keycloak, usando OAuth2 Authorization Code. Además se explica como se combian este proceso con el proceso de la protección de las APIs a través de un accesso de OAuth2 Client Credentials con Access Token gestionado por la aplicación cliente. Así pues con un SSO, Keycloak, podemos gestionar ambos accesos.

Secuencia del proceso OAuth2 Authorization Code con reverse proxy

A continuación se detalla, en 22 pasos, el flujo OAuth2 Authorization Code con HTTP proxy para acceder a la interfaz de administración de Grafana Mimir, incorporando los matices específicos del entorno:
OAuth2-OAuth2 Authorization Code with HTTP proxy.drawio.svg
Fichero .png con código vectorial de .drawio incluido
OAuth2-OAuth2 Authorization Code with HTTP proxy.drawio.png
  1. Solicitud Inicial del Usuario: El usuario ingresa la URL pública de la UI de Grafana Mimir en su navegador (por ejemplo, https://mimir.example.com).
  2. Recepción de la Petición en NPM: Nginx Proxy Manager (NPM) recibe la solicitud. NPM actúa exclusivamente como reverse proxy: enruta URLs y termina las conexiones SSL/TLS.
  3. Delegación de la Identificación al OAuth2 Proxy: NPM delega la identificación del usuario al componente OAuth2 Proxy, sin intervenir en la lógica de autenticación.
  4. Verificación de Sesión en OAuth2 Proxy: El OAuth2 Proxy revisa si el navegador presenta una cookie de sesión válida. Al no encontrarla, determina que el usuario debe autenticarse.
  5. Redirección a Keycloak (Inicio de OAuth2): OAuth2 Proxy construye una URL de autorización para Keycloak, incluyendo parámetros como el client_id, scope (por ejemplo, "openid email profile offline_access roles"), y la redirect_uri a la que se enviará el código de autorización.
  6. Redirección del Navegador a Keycloak: El navegador es redirigido desde el OAuth2 Proxy a la pantalla de autenticación de Keycloak.
  7. Presentación de la Pantalla de Login en Keycloak: Keycloak muestra su formulario de login, solicitando al usuario sus credenciales.
  8. Ingreso de Credenciales por el Usuario: El usuario introduce su nombre de usuario y contraseña (u otro mecanismo configurado en Keycloak).
  9. Validación de Credenciales en Keycloak: Keycloak valida las credenciales y, de ser correctas, autentica al usuario y crea su propia sesión interna.
  10. Emisión del Código de Autorización: Tras una autenticación exitosa, Keycloak genera un código de autorización temporal y redirige al navegador a la redirect_uri configurada por OAuth2 Proxy, adjuntando el código en la URL.
  11. Recepción del Código por OAuth2 Proxy: El navegador regresa al OAuth2 Proxy con el código de autorización en la query string.
  12. Intercambio del Código por Tokens: OAuth2 Proxy actúa como cliente OAuth2 y envía una petición al endpoint de tokens de Keycloak, aportando el código, el client_id y el client_secret.
  13. Validación y Emisión de Tokens en Keycloak: Keycloak verifica el código y, si es válido, responde con un access token y, opcionalmente, un refresh token.
  14. Recepción y Almacenamiento de los Tokens: OAuth2 Proxy recibe el token y lo almacena internamente. Además, se encarga de la lógica para refrescar los tokens cuando sea necesario.
  15. Creación de la Cookie de Sesión: OAuth2 Proxy genera y entrega al navegador una cookie de sesión, que servirá para identificar al usuario en futuras peticiones sin tener que repetir el proceso de OAuth2.
  16. Redirección Final a la URL Original: Con la sesión establecida en OAuth2 Proxy, se redirige al navegador de vuelta a la URL original de Grafana Mimir.
  17. Nueva Solicitud del Navegador a través de NPM: El navegador realiza una nueva solicitud a la UI de Grafana Mimir, esta vez incluyendo la cookie de sesión emitida por OAuth2 Proxy.
  18. Terminado SSL/TLS por NPM y Enrutamiento: NPM vuelve a terminar el túnel SSL/TLS y enruta la solicitud autenticada hacia Grafana Mimir.
  19. Recepción de la Solicitud en Grafana Mimir: Grafana Mimir recibe la solicitud. Dado que Mimir no gestiona usuarios ni mantiene sesiones, ignora los datos de autenticación.
  20. Procesamiento de la Petición en Mimir: Grafana Mimir responde a la solicitud sirviendo la interfaz de administración sin personalización basada en la identidad, ya que su diseño no contempla la gestión de usuarios.
  21. Visualización de la Interfaz de Administración: El navegador muestra la UI de administración de Grafana Mimir, permitiendo al usuario interactuar con el sistema.
  22. Acceso Continuado Basado en la Cookie de Sesión del Proxy: Mientras la cookie de sesión en el navegador siga vigente (y el OAuth2 Proxy se encargue de refrescar los tokens), el usuario accederá a la UI de Mimir sin necesidad de reautenticarse, aunque Mimir en sí no gestiona ni conserva la identidad del usuario.
Este flujo demuestra cómo, en este escenario, NPM y OAuth2 Proxy trabajan en conjunto para gestionar la autenticación mediante OAuth2 Authorization Code. NPM se encarga únicamente de la terminación SSL/TLS y el enrutamiento, mientras que OAuth2 Proxy se ocupa de la negociación OAuth2 con Keycloak, del almacenamiento y renovación de tokens, y de entregar una cookie de sesión al navegador. Grafana Mimir, por su parte, simplemente sirve la UI de gestión, ignorando la información de usuario, ya que no implementa un sistema de usuarios ni sesiones.

¿Realmente hace falta OAuth2 Proxy?

  1. Gestiona la lógica de OAuth2 para el servicio que estamos protegiendo. Así pues, este servicio no tiene que ocuparse de la autenticación ni la sesión del usuario.
  2. Muy relacionado con el punto anterior, OAuth2 Proxy se ocupará de hacer caché del access token y el refresh token. Si esto no fuera así, sería NPM quien debería ocuparse de este proceso complicando muchísimio su configuración.
  3. Cuando el JWT se va llenando de información de sesión hace que cada query que lanza el navegador del usuario ocupe varios KBytes cosa que es muy poco eficiente y además pueda dar problemas al llegar al límite del espacio que pueden tener las cabeceras HTTP.
    Lo que hará OAuth2 Proxy es crear una cookie de sesión que evitará tener que transportar el JWT arriba y abajo.
  4. Las cookies de sesión en OAuth2 Proxy se guardan en Redis. Por lo tanto el caché queda disponible por si queremos tener un sistema distribuido en el que haya más de un NPM+OAuth2 Proxy compartiendo sesiones de usuario.

Configuración NGINX Proxy Manager (NPM)

image.png
image.png
image.png
image.png

ficheros de configuración custom

/data/nginx/custom/i40sys_mimir_custom_root.conf

Bash
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
auth_request /oauth2/auth;
proxy_set_header X-Auth-Request-Redirect $request_uri;

error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri;
error_page 403 = /oauth2/sign_in?rd=$scheme://$host$request_uri;

auth_request_set $user   $upstream_http_x_auth_request_user;
auth_request_set $email  $upstream_http_x_auth_request_email;
auth_request_set $username $upstream_http_x_auth_request_preferred_username;
auth_request_set $groups $upstream_http_x_auth_request_groups;
auth_request_set $token  $upstream_http_x_auth_request_access_token;

proxy_set_header X-User-ID  $user;
proxy_set_header X-Email $email;
proxy_set_header X-Username $username;
proxy_set_header X-Groups $groups;    
proxy_set_header X-Access-Token $token;

/data/nginx/custom/i40sys_mimir_custom.conf

Bash
client_body_buffer_size 256M;
proxy_read_timeout 600;
proxy_connect_timeout 600;
proxy_send_timeout 600;
client_header_timeout 600s;
client_body_timeout 600s;
proxy_request_buffering off;

location /oauth2/auth {
  internal;
  proxy_pass http://oauth_proxy_i40sys-proxy-1:4180;

  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Scheme $scheme;
  proxy_set_header Content-Length "";
  proxy_pass_request_body off;
}

location /oauth2/ {
  proxy_pass http://oauth_proxy_i40sys-proxy-1:4180;

  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Scheme $scheme;
  proxy_set_header X-Auth-Request-Redirect $request_uri;
}

location /access-token/validation {
  internal;
  proxy_method GET;
  proxy_pass https://iam_i40sys-keycloak-1:8443/realms/observability/protocol/openid-connect/userinfo;

  proxy_set_header Host iam.i40sys.com;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Scheme $scheme;
  proxy_set_header Content-Length "";
  proxy_pass_request_body off;
}

location ~ ^/(api|prometheus)/ {
  proxy_pass http://observability_i40sys-mimir-1$request_uri;
  auth_request /access-token/validation;

  # proxy_set_header Upgrade $http_upgrade;
  # proxy_set_header Connection "upgrade";
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  
  auth_request_set $user $upstream_http_x_auth_request_user;
  proxy_set_header X-User-ID $user;
  auth_request_set $email $upstream_http_x_auth_request_email;
  proxy_set_header X-Email $email;
  auth_request_set $username $upstream_http_x_auth_request_preferred_username;
  proxy_set_header X-Username $username;
  auth_request_set $groups $upstream_http_x_auth_request_groups;
  proxy_set_header X-Groups $groups;
  auth_request_set $token $upstream_http_x_auth_request_access_token;
  proxy_set_header X-Access-Token $token;
}

notas sobre el docker-compose.yaml

El fichero de configuración es estándard, es decir, cualquier fichero que uses está bien. Lo más relevante a tener en cuenta es que el volumen que contiene los ficheros custom debe montarse en /data para que los anteriores ficheros esten mapeados.
Concretamente la configuración es:
YAML
services:
  nginx-reverse-proxy:
    image: 'lepresidente/nginxproxymanager:latest'
		...
    volumes:
      - "/data/rproxy/data:/data"
    ...
concretamente en nuestro escenario el fichero docker-compose.yaml es algo complejo porqué NPM usa Crowdsec para hacer filtrado. Dejamos una copia a continuación por si puede ayudar a alguien.
docker-compose.yaml
YAML
services:
  nginx-reverse-proxy:
    container_name: nginx-reverse-proxy
    image: 'lepresidente/nginxproxymanager:latest'
    hostname: "nginx-reverse-proxy"
    security_opt:
      - no-new-privileges=true
    entrypoint:
      - "/init"
    environment:
      PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
      CURL_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
      SUPPRESS_NO_CONFIG_WARNING: 1
      S6_BEHAVIOUR_IF_STAGE2_FAILS: 1
      S6_CMD_WAIT_FOR_SERVICES_MAXTIME: 0
      S6_FIX_ATTRS_HIDDEN: 1
      S6_KILL_FINISH_MAXTIME: 10000
      S6_VERBOSITY: 1
      NODE_ENV: production
      CROWDSEC_BOUNCER_APIKEY: ${CROWDSEC_BOUNCER_APIKEY}
      CROWDSEC_OPENRESTY_BOUNCER: |
        ENABLED=true
        API_URL=http://crowdsec:8080
        API_KEY=${CROWDSEC_BOUNCER_APIKEY}
        EXCLUDE_DOMAINS="metrics.i40sys.com,iam.i40sys.com,logs.i40sys.com,proxy.i40sys.com"
      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: 'true'
    ipc: "private"
    restart: "unless-stopped"
    volumes:
      - "/data/rproxy/data:/data"
      - "/data/rproxy/letsencrypt:/etc/letsencrypt"
    working_dir: "/app"
    networks:
      - "services"
      - "crowdsec"
    ports:
      - "10.2.0.134:81:81/tcp"
      - 80:80
      - 443:443

  crowdsec:
    image: docker.io/crowdsecurity/crowdsec:latest
    container_name: crowdsec
    environment:
      - COLLECTIONS=crowdsecurity/nginx-proxy-manager
    volumes:
      - /docker-data/crowdsec/db:/var/lib/crowdsec/data/
      - /docker-data/crowdsec/config:/etc/crowdsec/
      #- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
      - /data/rproxy/data/logs/:/var/log/npm:ro
    networks:
      crowdsec:
    restart: unless-stopped
    security_opt:
      - no-new-privileges=true
    depends_on:
      - nginx-reverse-proxy

networks:
  crowdsec:
    external: true
    name: "crowdsec"
  services:
    external: true
    name: "services"

Configuración de OAuth2 Proxy

docker-compose.yaml

YAML
name: oauth_proxy_i40sys

x-logging:
  &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "1"

services:
  redis:
    image: redis:alpine
    # container_name: redis
    networks:
      - private
    volumes:
      - /docker-data/oauth2-proxy.i40sys/redis:/data  # Optional: for data persistence
    logging: *default-logging
    
  proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
    command: --config=/oauth2-proxy.cfg --code-challenge-method=S256
    environment:
      - OAUTH2_PROXY_COOKIE_SECRET=LJAxLJAxLJAxLJAxLJAxMQ==
      - OAUTH2_PROXY_BACKEND_LOGOUT_URL=https://iam.i40sys.com/realms/observability/protocol/openid-connect/logout?id_token_hint={id_token}
    networks:
      - private
      - proxy-network
    volumes:
      - /docker-data/oauth2-proxy.i40sys/config/oauth2-proxy.cfg:/oauth2-proxy.cfg
    depends_on:
      - redis
    logging: *default-logging
networks:
  private:
  proxy-network:
    name: services
    external: true

config/oauth2-proxy.cfg

Bash
## OAuth2 Proxy Config File
## https://github.com/oauth2-proxy/oauth2-proxy

http_address="0.0.0.0:4180"
reverse_proxy = true
redirect_url="https://proxy.i40sys.com/oauth2/callback"

standard_logging = true
standard_logging_format = "[{{.Timestamp}}] [{{.File}}] {{.Message}}"
request_logging = true
request_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
auth_logging = true
auth_logging_format = "{{.Client}} - {{.Username}} [{{.Timestamp}}] [{{.Status}}] {{.Message}}"

pass_user_headers = true
email_domains = "*"

client_id = "oauth2_proxy"
client_secret = "kxRSwAqJqcxoB4GJ7LaeUJ9n3ymJBljh"

pass_access_token = true
cookie_name = "oauth2_proxy_i40sys"
# cookie_secret = ""
cookie_domains = ".i40sys.com"
# cookie_expire = "168h"
cookie_refresh = "5m"
cookie_secure = true
cookie_httponly = false

whitelist_domains = [
    "iam.i40sys.com:*",
    "*.i40sys.com:*",
    "*:*"
]

oidc_issuer_url = "https://iam.i40sys.com/realms/observability"
provider = "oidc"
provider_display_name = "Keycloak"
skip_provider_button = "true"
set_authorization_header = true
set_xauthrequest =  true

cookie_csrf_per_request=true
cookie_csrf_expire="5m"
cookie_samesite="lax"

session_store_type="redis"
redis_connection_url="redis://redis:6379"

show_debug_on_error=true

References