Skip to content

garnser/nginx-oauth2-proxy

Repository files navigation

NGINX OIDC Gateway (docker/)

Reverse‑proxy stack that fronts your apps with OpenID Connect SSO and group‑based access control. It’s built around nginx-proxy, an oauth2-proxy sidecar, and a small NJS (JavaScript) policy engine that lets you declare who can access what — per host and per path.

Heads‑up: The repo contains demo hostnames like app1.domain.com and example creds inside .env-oauth2. Do not use those in production. Replace with your own issuer, client ID/secret, and domains.


Features

  • 🔐 SSO via OIDC using oauth2-proxy
  • 👥 Group‑based authorization with a flexible policy (JSON) evaluated in NGINX via NJS
  • 🧩 Works with nginx-proxy (docker-gen template) so each app can advertise its own auth policy via env vars
  • 🔁 Smart auth flow: 401 → /oauth2/start redirect; 403 → debug‑friendly denial page
  • 🧪 Example apps included for quick validation

What’s in here

/docker
  Dockerfile                    # Prebakes nginx.conf, template, NJS, error pages
  docker-compose.yml            # Core stack (nginx-proxy + docker-socket-proxy)
  docker-compose.override.yml   # Examples: oauth2-proxy, redis, demo apps
  .env-oauth2                   # Example oauth2-proxy config (replace values!)
  nginx/
    nginx.conf                  # Loads NJS; includes conf.d/*.conf
    conf.d/00-oidc.conf         # Declares variables + imports NJS module
    njs/oidc.js                 # Policy merge + authz gate (NJS)
    policy.json                 # Global fallback policy (optional)
    vhost.d/default             # Common vhost bits: auth subrequest locations
    vhost.d/default_location    # Gate hook + headers to upstream
    error_pages/access_denied.html
    certs/default.crt,key       # Dev default cert (for HTTPS bootstrapping)

Components

  • nginx-proxy: terminates TLS and routes by VIRTUAL_HOST; renders config via docker‑gen template.

  • oauth2-proxy: handles OIDC login against your IdP, sets auth headers for NGINX.

  • NJS policy gate (njs/oidc.js): evaluates a policy from multiple sources (highest precedence first):

    1. POLICY_JSON env on the app container (per‑host rules)
    2. PERMITTED_GROUPS env on the app container (legacy fallback)
    3. /etc/nginx/policy.json global file inside the proxy image
    4. Built‑in fallback that makes everything public
  • docker-socket-proxy (in core compose): safe, read‑only access for docker‑gen.

  • redis (in override): session store for oauth2‑proxy.


Request flow (high level)

  1. Browser → https://app.example.com/securenginx-proxy vhost

  2. Protected locations include vhost.d/default_location which does:

    • auth_request /__gate → internal chain: /__oauth2_auth (oauth2-proxy /oauth2/auth) → NJS policy check
  3. If unauthenticated ⇒ 401 mapped to 302 /oauth2/start?rd=$request_uri

  4. After OIDC login, oauth2‑proxy sets headers; NJS evaluates policy rules:

    • allow/deny based on path, method, group membership
  5. On deny (403) you get a debug page (/access_denied.html) with helpful headers.

Auth flow


Quick start

Requires Docker + Docker Compose. The examples assume you control *.example.com DNS pointing to your host. Replace demo domains in env vars.

  1. Clone and configure

    • Copy docker/.env-oauth2 as a starting point and replace:

      • OAUTH2_PROXY_OIDC_ISSUER_URL
      • OAUTH2_PROXY_CLIENT_ID
      • OAUTH2_PROXY_CLIENT_SECRET
      • OAUTH2_PROXY_ALLOWED_REDIRECT_DOMAINS
    • (Optional) put a real cert/key in docker/nginx/certs/ or use a TLS terminator in front.

  2. Run the stack

    cd docker
    docker compose -f docker-compose.yml -f docker-compose.override.yml up -d --build
  3. Hit the demo apps

    • https://app1.example.com/ → public root with an /admin.html that requires app1_admins
    • https://app2.example.com/ → requires app2_admins2 for /

If you prefer to bring only the proxy layer first, start with just docker-compose.yml and add oauth2‑proxy + apps later.


How to declare access policy (per app)

You have two ways to attach policy to an app container (the one with VIRTUAL_HOST). Use one of them per app:

1) Rich policy via POLICY_JSON (recommended)

Attach an environment variable on the app container:

services:
  app1:
    image: nginx:alpine
    environment:
      VIRTUAL_HOST: app1.example.com
      POLICY_JSON: >-
        {"rules":[
          {"path":"^/public/","public":true},
          {"path":"^/admin\\.html$","methods":["GET","POST"],"allow":["app1_admins"]},
          {"path":"/","allow":["app1_users"]}
        ]}

Rule schema (evaluated in order):

  • path (regex or string prefix) — defaults to /
  • methods array — optional HTTP methods filter
  • public: true — allow all, no login required
  • allow: ["groupA","groupB"] — user must be in any of the listed groups

2) Simple "one‑liner" via PERMITTED_GROUPS

services:
  app2:
    image: nginx:alpine
    environment:
      VIRTUAL_HOST: app2.example.com
      PERMITTED_GROUPS: "app2_admins,app2_users"

This is syntactic sugar that becomes:

{"rules":[{"path":"/","allow":["app2_admins","app2_users"]}]}

Global fallback policy (optional)

Place defaults in nginx/policy.json inside the image (baked by Dockerfile). For example:

{
  "default": { "rules": [ { "public": true } ] },
  "app2.example.com": { "rules": [ { "path": "/", "allow": ["app2_admins"] } ] }
}

oauth2-proxy configuration (IdP)

These live in .env-oauth2 (override with your values):

  • OAUTH2_PROXY_PROVIDER=oidc
  • OAUTH2_PROXY_OIDC_ISSUER_URL=https://<your-idp>/realms/<realm>
  • OAUTH2_PROXY_CLIENT_ID / OAUTH2_PROXY_CLIENT_SECRET
  • OAUTH2_PROXY_OIDC_GROUPS_CLAIM=groups (adjust if your IdP uses another claim)
  • OAUTH2_PROXY_COOKIE_SECRET (32+ byte base64, generate your own)
  • OAUTH2_PROXY_SESSION_STORE_TYPE=redis and OAUTH2_PROXY_REDIS_CONNECTION_URL=redis://redis:6379
  • OAUTH2_PROXY_ALLOWED_REDIRECT_DOMAINS=.example.com

Tip: Verify /oauth2/ping and /oauth2/auth healthchecks are green in logs.


NGINX headers to upstream apps

When access is allowed, these headers are forwarded to your app:

  • X-User — from oauth2-proxy user
  • X-Email — email claim
  • X-Groups — comma‑separated groups

For troubleshooting (enabled in vhost.d/default_location), the proxy also returns helpful X-Authz-* headers — remove those once validated.


Local development vs production

  • TLS: The image ships with a dummy cert. Replace with a real cert or terminate TLS in front (e.g., ELB/ALB, Traefik, caddy) and forward to this stack.
  • Docker socket access: The core stack uses docker‑socket‑proxy with read‑only scopes (safer than mounting /var/run/docker.sock directly). The docker-compose.broken.yml shows an alternative wiring using a unix socket and user namespaces for reference.
  • Scaling: You can run multiple app containers behind one VIRTUAL_HOST; policies apply per host.

Common problems

  • Redirect loop after login

    • Check cookie domain/secure flags; make sure you’re on HTTPS and ALLOWED_REDIRECT_DOMAINS matches the host.
    • Ensure system clocks are in sync.
  • 403 when user is in the right group

    • Confirm the IdP actually emits the groups claim (or adjust OAUTH2_PROXY_OIDC_GROUPS_CLAIM).
    • Verify the exact group names in the token vs your policy JSON.
  • App isn’t picked up by nginx-proxy

    • Container must be on the same Docker network and set VIRTUAL_HOST.
    • If using a custom external network name, export DEFAULT_NETWORK env for nginx‑proxy.

Make it your own

  • Extend njs/oidc.js to add custom matchers (e.g., header‑based rules, IP allowlists).
  • Replace demo domains with your own and wire real apps.
  • Consider moving policy sources to a config service if you need centralized control.

Configure Keycloak (SSO)

This stack expects an OIDC client in Keycloak configured like below. You can either import a JSON client (recommended for dev) or click through the admin UI.

Click-through setup

  1. Create client

    • Client type: OpenID Connect
    • Client ID: your app ID (e.g., app1)
    • Name: friendly name
    • Always display in console: optional
  2. Capability config

    • Client authentication: On (confidential client)
    • Authorization: Off (not needed)
    • Standard flow: On (Authorization Code)
    • Implicit flow: Off
    • Direct access grants: Off
    • Service accounts: Off
  3. Login settings

    • Valid redirect URIs: each app’s oauth2-proxy callback, e.g.: https://app1.example.com/oauth2/callback https://app2.example.com/oauth2/callback
    • Web origins: + (or specify exact origins)
    • Front-channel logout: On
  4. Credentials

    • Client Authenticator: Client ID and Secret
    • Client secret: generate a new secret and copy it into your OAUTH2_PROXY_CLIENT_SECRET.
  5. Advanced / Attributes (defaults are fine unless you require PAR, MTLS, etc.)

    • PKCE method S256 is supported (keep default).
  6. Client scopes

    • Default: roles, profile, email (plus web-origins, acr).
    • Optional: offline_access if you plan to use refresh tokens via oauth2-proxy (not required here).
  7. Protocol mappers (group claim) Add both of the following so tokens contain the user’s groups under the same claim name — this keeps things compatible across token types:

    • Group Membership mapper

      • Mapper type: Group Membership
      • Token claim name: groups
      • Add to access token: On
      • Add to ID token: On
      • Add to userinfo: On
      • Full group path: Off (unless you want /realm/group/subgroup)
    • Realm Role mapper

      • Mapper type: User Realm Role
      • Token claim name: groups
      • Multivalued: On
      • Include in access/ID token & userinfo: On

Why two mappers? Some orgs assign access via realm roles instead of (or in addition to) groups. Mapping both into a single groups claim means your NGINX/NJS policy can just check groups.

What oauth2‑proxy expects

Match these Keycloak values with your .env-oauth2:

  • OAUTH2_PROXY_PROVIDER=oidc
  • OAUTH2_PROXY_OIDC_ISSUER_URL=https://<KEYCLOAK_HOST>/realms/<REALM>
  • OAUTH2_PROXY_CLIENT_ID=<Client ID>
  • OAUTH2_PROXY_CLIENT_SECRET=<Client Secret>
  • OAUTH2_PROXY_OIDC_GROUPS_CLAIM=groups
  • OAUTH2_PROXY_ALLOWED_REDIRECT_DOMAINS=.example.com (set to your domain suffix)
  • OAUTH2_PROXY_SESSION_STORE_TYPE=redis and OAUTH2_PROXY_REDIS_CONNECTION_URL=redis://redis:6379

Testing checklist

  • Hitting https://app.example.com/ redirects to Keycloak login and back to your app.
  • Call https://app.example.com/oauth2/userinfo (if exposed) or decode the ID/Access token — you should see groups: ["…"] containing your test user’s groups/roles.
  • Access to paths protected by your POLICY_JSON works only when the user is in the allowed group(s).

Security notes

  • Never commit client secrets. Use secrets management or env injection.
  • Prefer exact Web Origins over + in production.
  • Keep Standard flow only; avoid implicit/hybrid unless absolutely required.

License

Add your project’s license here (e.g., MIT).

About

A combined nginx-oauth2-proxy with granular authorization support

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published