docs: rewrite README with structured overview and quick start guide
Replaces the minimal project description with a comprehensive README including a component overview table, quick start instructions, common Ansible operations, and links to detailed documentation. Aligns with Red Panda Approval™ standards.
This commit is contained in:
314
docs/oauth2_proxy.md
Normal file
314
docs/oauth2_proxy.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 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 Agathos 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 **Applications** → **Add**
|
||||
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
|
||||
|
||||
```bash
|
||||
ansible-vault edit ansible/inventory/group_vars/all/vault.yml
|
||||
```
|
||||
|
||||
Add service-specific credentials:
|
||||
```yaml
|
||||
# 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:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
### Step 3: Configure Host Variables
|
||||
|
||||
Add to the host that will run OAuth2-Proxy (typically `titania.incus.yml`):
|
||||
|
||||
```yaml
|
||||
# =============================================================================
|
||||
# <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:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
```yaml
|
||||
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)
|
||||
```yaml
|
||||
# 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
|
||||
```yaml
|
||||
- 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:
|
||||
```yaml
|
||||
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`:
|
||||
```alloy
|
||||
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:
|
||||
```alloy
|
||||
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:
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
ssh titania.incus
|
||||
docker logs oauth2-proxy
|
||||
```
|
||||
|
||||
### Test OIDC Discovery
|
||||
```bash
|
||||
curl http://titania.incus:22081/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
### Verify Cookie Domain
|
||||
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 |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [SearXNG Authentication](services/searxng-auth.md) - Specific implementation details
|
||||
- [Casdoor Documentation](casdoor.md) - Identity provider configuration
|
||||
Reference in New Issue
Block a user