Files
ouranos/docs/oauth2_proxy.md

11 KiB

OAuth2-Proxy Authentication Gateway

Red Panda Approved

Overview

OAuth2-Proxy provides authentication for services that don't natively support SSO/OIDC. It acts as a reverse proxy that requires users to authenticate via Casdoor before accessing the upstream service.

This document describes the generic approach for adding OAuth2-Proxy authentication to any service in the Ouranos infrastructure.

Architecture

┌──────────────┐     ┌───────────────┐     ┌────────────────┐     ┌───────────────┐
│   Browser    │────▶│    HAProxy    │────▶│  OAuth2-Proxy  │────▶│  Your Service │
│              │     │   (titania)   │     │   (titania)    │     │   (any host)  │
└──────────────┘     └───────┬───────┘     └───────┬────────┘     └───────────────┘
                             │                     │
                             │     ┌───────────────▼───────────────┐
                             └────▶│          Casdoor              │
                                   │    (OIDC Provider - titania)  │
                                   └───────────────────────────────┘

How It Works

  1. User requests https://service.ouranos.helu.ca/
  2. HAProxy routes to OAuth2-Proxy (titania:22082)
  3. OAuth2-Proxy checks for valid session cookie
  4. No session? → Redirect to Casdoor login → After login, redirect back with cookie
  5. Valid session? → Forward request to upstream service

File Structure

ansible/oauth2_proxy/
├── deploy.yml                # Main deployment playbook
├── docker-compose.yml.j2     # Docker Compose template
├── oauth2-proxy.cfg.j2       # OAuth2-Proxy configuration
└── stage.yml                 # Validation/staging playbook

Monitoring configuration is integrated into the host-specific Alloy config:

  • ansible/alloy/titania/config.alloy.j2 - Contains OAuth2-Proxy log collection and metrics scraping

Variable Architecture

The OAuth2-Proxy template uses generic variables (oauth2_proxy_*) that are mapped from service-specific variables in host_vars:

Vault (service-specific)         Host Vars (mapping)              Template (generic)
────────────────────────         ───────────────────              ──────────────────
vault_<service>_oauth2_*    ──►  <service>_oauth2_*    ──►       oauth2_proxy_*

This allows:

  • Multiple services to use the same OAuth2-Proxy template
  • Service-specific credentials in vault
  • Clear naming conventions

Configuration Steps

Step 1: Create Casdoor Application

  1. Login to Casdoor at https://id.ouranos.helu.ca/ (Casdoor SSO)
  2. Navigate to ApplicationsAdd
  3. Configure:
    • Name: <your-service> (e.g., searxng, jupyter)
    • Organization: heluca (or your organization)
    • Redirect URLs: https://<service>.ouranos.helu.ca/oauth2/callback
    • Grant Types: authorization_code, refresh_token
  4. Save and note the Client ID and Client Secret

Step 2: Add Vault Secrets

ansible-vault edit ansible/inventory/group_vars/all/vault.yml

Add service-specific credentials:

# SearXNG OAuth2 credentials
vault_searxng_oauth2_client_id: "abc123..."
vault_searxng_oauth2_client_secret: "secret..."
vault_searxng_oauth2_cookie_secret: "<generate-with-command-below>"

Generate cookie secret:

openssl rand -base64 32

Step 3: Configure Host Variables

Add to the host that will run OAuth2-Proxy (typically titania.incus.yml):

# =============================================================================
# <Service> OAuth2 Configuration (Service-Specific)
# =============================================================================
<service>_oauth2_client_id: "{{ vault_<service>_oauth2_client_id }}"
<service>_oauth2_client_secret: "{{ vault_<service>_oauth2_client_secret }}"
<service>_oauth2_cookie_secret: "{{ vault_<service>_oauth2_cookie_secret }}"

# =============================================================================
# OAuth2-Proxy Configuration (Generic Template Variables)
# =============================================================================
oauth2_proxy_user: oauth2proxy
oauth2_proxy_group: oauth2proxy
oauth2_proxy_uid: 802
oauth2_proxy_gid: 802
oauth2_proxy_directory: /srv/oauth2-proxy
oauth2_proxy_port: 22082

# OIDC Configuration
oauth2_proxy_oidc_issuer_url: "http://titania.incus:{{ casdoor_port }}"

# Map service-specific credentials to generic template variables
oauth2_proxy_client_id: "{{ <service>_oauth2_client_id }}"
oauth2_proxy_client_secret: "{{ <service>_oauth2_client_secret }}"
oauth2_proxy_cookie_secret: "{{ <service>_oauth2_cookie_secret }}"

# Service-specific URLs
oauth2_proxy_redirect_url: "https://<service>.{{ haproxy_domain }}/oauth2/callback"
oauth2_proxy_upstream_url: "http://<service-host>:<service-port>"
oauth2_proxy_cookie_domain: "{{ haproxy_domain }}"

# Access Control
oauth2_proxy_email_domains:
  - "*"  # Or restrict to specific domains

# Session Configuration
oauth2_proxy_cookie_expire: "168h"
oauth2_proxy_cookie_refresh: "1h"

# SSL Verification
oauth2_proxy_skip_ssl_verify: true  # Set false for production

Step 4: Update HAProxy Backend

Change the service backend to route through OAuth2-Proxy:

haproxy_backends:
  - subdomain: "<service>"
    backend_host: "titania.incus"  # OAuth2-Proxy host
    backend_port: 22082            # OAuth2-Proxy port
    health_path: "/ping"           # OAuth2-Proxy health endpoint

Step 5: Deploy

cd ansible

# Validate configuration
ansible-playbook oauth2_proxy/stage.yml

# Deploy OAuth2-Proxy
ansible-playbook oauth2_proxy/deploy.yml

# Update HAProxy routing
ansible-playbook haproxy/deploy.yml

Complete Example: SearXNG

Vault Variables

vault_searxng_oauth2_client_id: "searxng-client-id-from-casdoor"
vault_searxng_oauth2_client_secret: "searxng-client-secret-from-casdoor"
vault_searxng_oauth2_cookie_secret: "ABCdef123..."

Host Variables (titania.incus.yml)

# SearXNG OAuth2 (service-specific)
searxng_oauth2_client_id: "{{ vault_searxng_oauth2_client_id }}"
searxng_oauth2_client_secret: "{{ vault_searxng_oauth2_client_secret }}"
searxng_oauth2_cookie_secret: "{{ vault_searxng_oauth2_cookie_secret }}"

# OAuth2-Proxy (generic mapping)
oauth2_proxy_client_id: "{{ searxng_oauth2_client_id }}"
oauth2_proxy_client_secret: "{{ searxng_oauth2_client_secret }}"
oauth2_proxy_cookie_secret: "{{ searxng_oauth2_cookie_secret }}"
oauth2_proxy_redirect_url: "https://searxng.{{ haproxy_domain }}/oauth2/callback"
oauth2_proxy_upstream_url: "http://oberon.incus:25599"

HAProxy Backend

- subdomain: "searxng"
  backend_host: "titania.incus"
  backend_port: 22082
  health_path: "/ping"

Adding a Second Service (e.g., Jupyter)

When adding authentication to another service, you would:

  1. Create a new Casdoor application for Jupyter
  2. Add vault variables:
    vault_jupyter_oauth2_client_id: "..."
    vault_jupyter_oauth2_client_secret: "..."
    vault_jupyter_oauth2_cookie_secret: "..."
    
  3. Either:
    • Option A: Deploy a second OAuth2-Proxy instance on a different port
    • Option B: Configure the same OAuth2-Proxy with multiple upstreams (more complex)

For multiple services, Option A is recommended for isolation and simplicity.

Monitoring

OAuth2-Proxy monitoring is handled by Grafana Alloy, which runs on each host.

Architecture

OAuth2-Proxy ─────► Grafana Alloy ─────► Prometheus (prospero)
(titania)           (local agent)        (remote_write)
                         │
                         └─────────────► Loki (prospero)
                                         (log forwarding)

Metrics (via Prometheus)

Alloy scrapes OAuth2-Proxy metrics at /metrics and forwards them to Prometheus:

  • oauth2_proxy_requests_total - Total requests processed
  • oauth2_proxy_errors_total - Total errors
  • oauth2_proxy_upstream_latency_seconds - Latency to upstream service

Configuration in ansible/alloy/titania/config.alloy.j2:

prometheus.scrape "oauth2_proxy" {
  targets = [{"__address__" = "127.0.0.1:{{oauth2_proxy_port}}"}]
  scrape_interval = "30s"
  forward_to = [prometheus.remote_write.default.receiver]
  job_name = "oauth2-proxy"
}

Logs (via Loki)

OAuth2-Proxy logs are collected via syslog and forwarded to Loki:

loki.source.syslog "oauth2_proxy_logs" {
  listener {
    address  = "127.0.0.1:{{oauth2_proxy_syslog_port}}"
    protocol = "tcp"
    labels = { job = "oauth2-proxy", hostname = "{{inventory_hostname}}" }
  }
  forward_to = [loki.write.default.receiver]
}

Deploy Alloy After Changes

If you update the Alloy configuration:

ansible-playbook alloy/deploy.yml --limit titania.incus

Security Considerations

  1. Cookie Security:

    • cookie_secure = true - HTTPS only
    • cookie_httponly = true - No JavaScript access
    • cookie_samesite = "lax" - CSRF protection
  2. Access Control:

    • Use oauth2_proxy_email_domains to restrict by email domain
    • Use oauth2_proxy_allowed_groups to restrict by Casdoor groups
  3. SSL Verification:

    • Set oauth2_proxy_skip_ssl_verify: false in production
    • Ensure Casdoor has valid SSL certificates

Troubleshooting

Check OAuth2-Proxy Logs

ssh titania.incus
docker logs oauth2-proxy

Test OIDC Discovery

curl http://titania.incus:22081/.well-known/openid-configuration

Ensure oauth2_proxy_cookie_domain matches your HAProxy domain.

Common Issues

Issue Cause Solution
Redirect loop Cookie domain mismatch Check oauth2_proxy_cookie_domain
403 Forbidden Email domain not allowed Update oauth2_proxy_email_domains
OIDC discovery failed Casdoor not accessible Check network/firewall
Invalid redirect URI Mismatch in Casdoor app Verify redirect URL in Casdoor