Add environment variable documentation for all plugins, reorganize plugin files for consistency
- Introduced environment variable support in documentation for Cloudflare, Cleanup, Deny Pages, IP Whitelist, JWT Validator, and FastCGI plugins. - Updated plugin files with consistent headings, examples, and tables for easier configuration reference. - Adjusted `README.md` and `plugins.md` to reflect updated plugin paths and environment variable usage.
This commit is contained in:
parent
b4944ac544
commit
50ffb4fe16
8 changed files with 122 additions and 29 deletions
301
docs/Plugins/jwt-validator.md
Normal file
301
docs/Plugins/jwt-validator.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# JWT Validator Plugin
|
||||
|
||||
**Type:** Domain Plugin
|
||||
**Runs:** Once for each discovered domain/host
|
||||
|
||||
## Overview
|
||||
|
||||
The JWT Validator plugin validates JWT (JSON Web Token) authentication tokens using HAProxy's built-in JWT functionality.
|
||||
|
||||
## Why Use It
|
||||
|
||||
Protect APIs and services with JWT authentication without needing application-level code.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|-------------------|----------------------------------------------------------------------------------------------|-------------|
|
||||
| `enabled` | Enable/disable plugin | `true` |
|
||||
| `algorithm` | JWT signing algorithm | `RS256` |
|
||||
| `issuer` | Expected JWT issuer (optional, set to `none`/`null` to skip validation) | (optional) |
|
||||
| `audience` | Expected JWT audience (optional, set to `none`/`null` to skip validation) | (optional) |
|
||||
| `pubkey_path` | Path to public key file (required if `pubkey` not provided) | (required) |
|
||||
| `pubkey` | Public key content as base64-encoded string (required if `pubkey_path` not provided) | (optional) |
|
||||
| `paths` | List of paths that require JWT validation (optional) | (all paths) |
|
||||
| `only_paths` | If `true`, only specified paths are accessible; if `false`, only specified paths require JWT | `false` |
|
||||
| `allow_anonymous` | If `true`, allows requests without Authorization header (validates JWT if present) | `false` |
|
||||
|
||||
## Path Validation Logic
|
||||
|
||||
- **No paths configured:** ALL requests to the domain require JWT validation (default behavior)
|
||||
- **Paths configured + `only_paths=false`:** Only specified paths require JWT validation, other paths pass through without validation
|
||||
- **Paths configured + `only_paths=true`:** Only specified paths are accessible (with JWT validation), all other paths are denied
|
||||
|
||||
## Anonymous Access Logic
|
||||
|
||||
- **`allow_anonymous=false` (default):** Requests without `Authorization` header are denied with "Missing Authorization HTTP header"
|
||||
- **`allow_anonymous=true`:** Requests without `Authorization` header are allowed to pass through, but JWTs are validated if the header is present
|
||||
|
||||
**Use Cases for `allow_anonymous=true`:**
|
||||
- Optional authentication (show different content for authenticated vs anonymous users)
|
||||
- Mixed public/private content where some users have enhanced access with JWT
|
||||
- Gradual JWT authentication rollout
|
||||
- Public APIs that provide additional features to authenticated users
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Docker/Docker Compose (Protect All Paths)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
labels:
|
||||
easyhaproxy.http.host: api.example.com
|
||||
easyhaproxy.http.plugins: jwt_validator
|
||||
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
|
||||
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
|
||||
easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
volumes:
|
||||
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||
```
|
||||
|
||||
### Protect Specific Paths Only
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.plugins: jwt_validator
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
|
||||
easyhaproxy.http.plugin.jwt_validator.only_paths: false
|
||||
# /api/health, /api/docs, etc. remain publicly accessible
|
||||
```
|
||||
|
||||
### Only Allow Specific Paths
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.plugins: jwt_validator
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
easyhaproxy.http.plugin.jwt_validator.paths: /api/public,/api/v1
|
||||
easyhaproxy.http.plugin.jwt_validator.only_paths: true
|
||||
# All paths except /api/public and /api/v1 are denied
|
||||
```
|
||||
|
||||
### Skip Issuer/Audience Validation
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.plugin.jwt_validator.issuer: none
|
||||
easyhaproxy.http.plugin.jwt_validator.audience: none
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
```
|
||||
|
||||
### Allow Anonymous Access (Optional JWT)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
labels:
|
||||
easyhaproxy.http.host: api.example.com
|
||||
easyhaproxy.http.plugins: jwt_validator
|
||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true
|
||||
volumes:
|
||||
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||
# Requests without Authorization header are allowed
|
||||
# Requests with Authorization header are validated
|
||||
# Invalid JWTs are rejected
|
||||
```
|
||||
|
||||
### Kubernetes Annotations
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
easyhaproxy.plugins: "jwt_validator"
|
||||
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
|
||||
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
|
||||
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
|
||||
easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
|
||||
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
|
||||
easyhaproxy.plugin.jwt_validator.only_paths: "false"
|
||||
spec:
|
||||
rules:
|
||||
- host: api.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
service:
|
||||
name: api-service
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
### Static YAML Configuration
|
||||
|
||||
```yaml
|
||||
# /etc/haproxy/static/config.yaml
|
||||
easymapping:
|
||||
- host: api.example.com
|
||||
port: 443
|
||||
container: api-service:8080
|
||||
plugins:
|
||||
- jwt_validator
|
||||
plugin_config:
|
||||
jwt_validator:
|
||||
algorithm: RS256
|
||||
issuer: https://auth.example.com/
|
||||
audience: https://api.example.com
|
||||
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure JWT Validator plugin defaults for all domains:
|
||||
|
||||
| Environment Variable | Config Key | Type | Default | Description |
|
||||
|----------------------------------------------------|-------------------|---------|---------|---------------------------------------------|
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ALGORITHM` | `algorithm` | string | `RS256` | JWT signing algorithm |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ISSUER` | `issuer` | string | - | Expected JWT issuer (optional) |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_AUDIENCE` | `audience` | string | - | Expected JWT audience (optional) |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PUBKEY_PATH` | `pubkey_path` | string | - | Path to public key file |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PUBKEY` | `pubkey` | string | - | Public key as base64-encoded string |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PATHS` | `paths` | string | - | Comma-separated paths requiring JWT |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ONLY_PATHS` | `only_paths` | boolean | `false` | If true, only specified paths accessible |
|
||||
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ALLOW_ANONYMOUS` | `allow_anonymous` | boolean | `false` | Allow requests without Authorization header |
|
||||
|
||||
**Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations.
|
||||
|
||||
## Generated HAProxy Configuration
|
||||
|
||||
### All Paths Protected
|
||||
|
||||
```haproxy
|
||||
# JWT Validator - Validate JWT tokens
|
||||
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
|
||||
|
||||
# Extract JWT header and payload
|
||||
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
|
||||
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
|
||||
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
|
||||
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
|
||||
|
||||
# Validate JWT
|
||||
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||
|
||||
# Validate expiration
|
||||
http-request set-var(txn.now) date()
|
||||
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
|
||||
```
|
||||
|
||||
### Specific Paths Only (only_paths=false)
|
||||
|
||||
```haproxy
|
||||
# JWT Validator - Validate JWT tokens
|
||||
|
||||
# Define paths that require JWT validation
|
||||
acl jwt_protected_path path_beg /api/admin
|
||||
acl jwt_protected_path path_beg /api/sensitive
|
||||
|
||||
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found } if jwt_protected_path
|
||||
|
||||
# Extract JWT header and payload
|
||||
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if jwt_protected_path
|
||||
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if jwt_protected_path
|
||||
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if jwt_protected_path
|
||||
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if jwt_protected_path
|
||||
|
||||
# Validate JWT (only on protected paths)
|
||||
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if jwt_protected_path
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if jwt_protected_path
|
||||
|
||||
# Validate expiration
|
||||
http-request set-var(txn.now) date() if jwt_protected_path
|
||||
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if jwt_protected_path
|
||||
```
|
||||
|
||||
### Specific Paths Only (only_paths=true)
|
||||
|
||||
```haproxy
|
||||
# JWT Validator - Validate JWT tokens
|
||||
|
||||
# Define paths that require JWT validation
|
||||
acl jwt_protected_path path_beg /api/public
|
||||
acl jwt_protected_path path_beg /api/v1
|
||||
|
||||
# Deny access to paths not in the protected list
|
||||
http-request deny content-type 'text/html' string 'Access denied' unless jwt_protected_path
|
||||
|
||||
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
|
||||
|
||||
# Extract JWT header and payload
|
||||
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
|
||||
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
|
||||
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
|
||||
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
|
||||
|
||||
# Validate JWT (all requests at this point are on allowed paths)
|
||||
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||
|
||||
# Validate expiration
|
||||
http-request set-var(txn.now) date()
|
||||
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
|
||||
```
|
||||
|
||||
### Allow Anonymous Access (allow_anonymous=true)
|
||||
|
||||
```haproxy
|
||||
# JWT Validator - Validate JWT tokens
|
||||
|
||||
# Allow anonymous access - validate JWT only if Authorization header is present
|
||||
|
||||
# Extract JWT header and payload
|
||||
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if { req.hdr(authorization) -m found }
|
||||
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if { req.hdr(authorization) -m found }
|
||||
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if { req.hdr(authorization) -m found }
|
||||
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if { req.hdr(authorization) -m found }
|
||||
|
||||
# Validate JWT (only if Authorization header is present)
|
||||
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if { req.hdr(authorization) -m found }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ } if { req.hdr(authorization) -m found }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com } if { req.hdr(authorization) -m found }
|
||||
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if { req.hdr(authorization) -m found }
|
||||
|
||||
# Validate expiration (only if Authorization header is present)
|
||||
http-request set-var(txn.now) date() if { req.hdr(authorization) -m found }
|
||||
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if { req.hdr(authorization) -m found }
|
||||
```
|
||||
|
||||
## What It Validates
|
||||
|
||||
- ✅ Authorization header presence
|
||||
- ✅ JWT signing algorithm (RS256, RS512, etc.)
|
||||
- ✅ JWT issuer (if configured)
|
||||
- ✅ JWT audience (if configured)
|
||||
- ✅ JWT signature using public key
|
||||
- ✅ JWT expiration time
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Required:** HAProxy 2.5+ with JWT support
|
||||
- Mount public key file as read-only volume
|
||||
- The plugin runs once per domain during the discovery cycle
|
||||
- Test thoroughly with your JWT provider before deploying to production
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Plugin System Overview](../plugins.md)
|
||||
- [Container Labels Reference](../container-labels.md)
|
||||
Loading…
Add table
Add a link
Reference in a new issue