2026-05-05 13:10:16 +01:00
# rsauth2-proxy
2026-05-05 15:41:39 +00:00
Auth proxy for [Traefik ForwardAuth ](https://doc.traefik.io/traefik/middlewares/http/forwardauth/ ) with Keycloak OIDC. Single instance protects all services in a Kubernetes cluster. Replaces oauth2-proxy.
2026-05-05 13:10:16 +01:00
## How it works
```
Browser → Traefik → ForwardAuth (/auth) → rsauth2-proxy
├── no session → 302 to Keycloak login
├── valid session → 200 + user headers
└── expired session → token refresh → 302 back
```
Traefik calls `/auth` for every request to a protected service. The proxy checks the encrypted session cookie, verifies group membership against the route config, and returns 200 (allow), 403 (deny), or 302 (login required).
2026-10-18 00:23:38 +01:00
Machine routes can instead accept HTTP Basic credentials over HTTPS. The proxy
checks the client ID and secret at Keycloak's `client_credentials` token endpoint
and caches successful checks for at most five minutes, never beyond the returned
token lifetime. Invalid credentials return 401, a forbidden route returns 403,
and a Keycloak outage returns 503 so the client can retry. Machine secrets are
never stored in the routes file.
Browser sessions are stored entirely in an AES-256-GCM encrypted cookie. Machine
credential caches are local to each replica; replicas require no coordination.
2026-05-05 13:10:16 +01:00
## Configuration
All configuration is via environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
| `AUTH_PROXY_OIDC_ISSUER` | yes | | OIDC issuer URL |
| `AUTH_PROXY_CLIENT_ID` | yes | | Keycloak client ID |
| `AUTH_PROXY_CLIENT_SECRET` | yes | | Keycloak client secret |
| `AUTH_PROXY_COOKIE_SECRET` | yes | | AES-256 key, 32 bytes, base64-encoded |
| `AUTH_PROXY_COOKIE_DOMAIN` | yes | | Cookie domain (e.g. `.example.com` ) |
| `AUTH_PROXY_CALLBACK_URL` | yes | | Full callback URL (e.g. `https://auth.example.com/callback` ) |
| `AUTH_PROXY_LISTEN` | no | `0.0.0.0:8080` | Listen address |
| `AUTH_PROXY_ROUTES_FILE` | no | `/config/routes.yaml` | Path to routes config |
| `AUTH_PROXY_LOG_LEVEL` | no | `info` | Log level (`debug` , `info` , `warn` , `error` ) |
Generate a cookie secret:
```sh
openssl rand -base64 32
```
## Routes file
Defines which hosts are protected and which groups have access.
```yaml
routes :
grafana.example.com :
allowed_groups : [ "admins" , "developers" ]
wiki.example.com :
allowed_groups : []
# Empty list = any authenticated user
secret.example.com :
allowed_groups : [ "admins" ]
2026-10-18 00:23:38 +01:00
loki.example.com :
browser_access : false
machine_access :
client_ids : [ "tsunagi-vector" ]
method : POST
path : /loki/api/v1/push
2026-05-05 13:10:16 +01:00
```
Rules:
- Host in routes, `allowed_groups` empty — any authenticated user is allowed
- Host in routes, `allowed_groups` set — user must be in at least one listed group
- Host not in routes — denied (403)
2026-10-18 00:23:38 +01:00
- `browser_access: false` — no cookie or OIDC redirect on this host
- `machine_access` — only listed Keycloak clients on the exact method and path
2026-05-05 13:10:16 +01:00
The file is polled every 5 seconds and reloaded on change. This works reliably with Kubernetes ConfigMap volume mounts.
## Endpoints
| Path | Purpose |
|---|---|
| `GET /auth` | ForwardAuth endpoint (called by Traefik) |
| `GET /callback` | OIDC callback (receives authorization code from Keycloak) |
| `GET /refresh` | Token refresh (transparent redirect when session expires) |
| `GET /sign_out` | Logout (clears cookie, redirects to Keycloak end_session) |
| `GET /health` | Health check (returns 200) |
## Keycloak setup
The proxy reads user groups from the `groups` claim in the ID token. Keycloak does not include this by default. Add a group membership mapper to the client:
```hcl
resource "keycloak_openid_group_membership_protocol_mapper" "groups" {
realm_id = keycloak_realm . main . id
client_id = keycloak_openid_client . auth_proxy . id
name = "groups"
claim_name = "groups"
full_path = false
}
```
Or manually: Client Scopes → your client → Mappers → Add mapper → "Group Membership", claim name `groups` , full path off.
## Kubernetes deployment
```yaml
apiVersion : apps/v1
kind : Deployment
metadata :
name : auth-proxy
namespace : auth-proxy
spec :
replicas : 2
selector :
matchLabels :
app : auth-proxy
template :
metadata :
labels :
app : auth-proxy
spec :
containers :
- name : auth-proxy
2026-05-05 15:39:31 +00:00
image : ultradesu/rsauth2-proxy:0.1.0
2026-05-05 13:10:16 +01:00
ports :
- containerPort : 8080
envFrom :
- secretRef :
name : auth-proxy-creds
volumeMounts :
- name : routes
mountPath : /config
readOnly : true
livenessProbe :
httpGet :
path : /health
port : 8080
readinessProbe :
httpGet :
path : /health
port : 8080
volumes :
- name : routes
configMap :
name : auth-proxy-routes
---
apiVersion : v1
kind : Service
metadata :
name : auth-proxy
namespace : auth-proxy
spec :
selector :
app : auth-proxy
ports :
- port : 80
targetPort : 8080
```
### Traefik ForwardAuth middleware
Create in each namespace that has protected services:
```yaml
apiVersion : traefik.io/v1alpha1
kind : Middleware
metadata :
name : auth-proxy
spec :
forwardAuth :
address : http://auth-proxy.auth-proxy.svc:80/auth
trustForwardHeader : true
authResponseHeaders :
- X-Auth-Request-User
- X-Auth-Request-Email
- X-Auth-Request-Groups
```
### Ingress for auth-proxy itself
The `/callback` , `/refresh` , and `/sign_out` endpoints must be reachable by browsers:
```yaml
apiVersion : traefik.io/v1alpha1
kind : IngressRoute
metadata :
name : auth-proxy
namespace : auth-proxy
spec :
entryPoints :
- websecure
routes :
- match : Host(`auth.example.com`) && (Path(`/callback`) || Path(`/refresh`) || Path(`/sign_out`))
kind : Rule
services :
- name : auth-proxy
port : 80
tls :
secretName : auth-proxy-tls
```
## Building
```sh
cargo build --release
```
### Docker
```sh
docker build -t rsauth2-proxy .
```
## Security properties
- **Encrypted cookies** — AES-256-GCM, not just signed. Cookie contents cannot be read or tampered with without the key.
- **PKCE (S256)** — protects the authorization code exchange against interception.
- **Stateless PKCE** — the PKCE verifier is encrypted inside the `state` parameter. No server-side storage needed.
- **No open redirect** — the redirect URL after login is encrypted in `state` , not taken from user input.
- **Deny by default** — any host not listed in routes gets 403.
- **JWT validation** — ID tokens are verified against Keycloak's JWKS (keys refreshed hourly).
- **Cookie flags** — `HttpOnly` , `Secure` , `SameSite=Lax` .
## Response headers
On successful authentication, the following headers are set on the request forwarded to the upstream service:
| Header | Value |
|---|---|
| `X-Auth-Request-User` | `preferred_username` from the ID token |
| `X-Auth-Request-Email` | `email` from the ID token |
| `X-Auth-Request-Groups` | Comma-separated list of groups |
## License
2026-05-05 15:39:31 +00:00
WTFPL