- Refactored HAProxy configuration files, templates, and paths to use `/etc/easyhaproxy` instead of `/etc/haproxy`. - Updated Dockerfile to generate DH params and placeholder certificates in the new configuration directory. - Added health check support with timeout to `DockerComposeFixture` in E2E test utilities. - Adjusted tests, templates, and plugins to use the new `Consts`-based configuration paths. - Introduced pytest fixtures for environment isolation and temporary directory management.
17 KiB
| sidebar_position |
|---|
| 16 |
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.
Generating JWT Keys
# Generate RSA key pair (idempotent - skips if exists)
[ -f jwt_private.pem ] || openssl genrsa -out jwt_private.pem 2048
[ -f jwt_pubkey.pem ] || openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
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 (priority 1: explicit file path) | (optional) |
pubkey |
Public key content as base64-encoded string (priority 2: inline content) | (optional) |
k8s_secret.pubkey |
Kubernetes secret containing public key (priority 3: Kubernetes only - see below) | (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 |
Public Key Configuration Priority
When multiple public key options are configured, they are evaluated in this order:
pubkey_path- Direct file path (explicit configuration)pubkey- Base64-encoded key content (inline configuration)k8s_secret.pubkey- Kubernetes secret (recommended for Kubernetes deployments)
The first configured option is used; others are ignored.
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 withoutAuthorizationheader are denied with "Missing Authorization HTTP header"allow_anonymous=true: Requests withoutAuthorizationheader 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)
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/easyhaproxy/jwt_keys/api_pubkey.pem
volumes:
- ./pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
Protect Specific Paths Only
labels:
easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/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
labels:
easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/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
labels:
easyhaproxy.http.plugin.jwt_validator.issuer: none
easyhaproxy.http.plugin.jwt_validator.audience: none
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
Allow Anonymous Access (Optional JWT)
services:
api:
labels:
easyhaproxy.http.host: api.example.com
easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true
volumes:
- ./pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
# Requests without Authorization header are allowed
# Requests with Authorization header are validated
# Invalid JWTs are rejected
Kubernetes with Secrets (Recommended)
The recommended way to configure JWT public keys in Kubernetes is using Kubernetes Secrets with the k8s_secret pattern:
---
# Create a secret with your JWT public key
apiVersion: v1
kind: Secret
metadata:
name: jwt-pubkey-secret
namespace: production
type: Opaque
stringData:
pubkey: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
---
# Reference it in your ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
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"
# Load public key from Kubernetes secret
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret"
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
easyhaproxy.plugin.jwt_validator.only_paths: "false"
spec:
ingressClassName: easyhaproxy
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
With explicit secret key name:
metadata:
annotations:
# Use custom key name from the secret
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret/rsa-public-key"
For complete details about the k8s_secret pattern, including auto-detect vs explicit key names, troubleshooting, and security considerations, see: Loading Plugin Configuration from Kubernetes Secrets
Kubernetes with pubkey_path (Legacy)
You can also mount the public key file using ConfigMaps or volumes:
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/easyhaproxy/jwt_keys/api_pubkey.pem"
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
easyhaproxy.plugin.jwt_validator.only_paths: "false"
spec:
ingressClassName: easyhaproxy
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
Note: This requires mounting the public key file into the EasyHAProxy pod using ConfigMaps or volumes. Using k8s_secret.pubkey is recommended as it's simpler and more secure.
Static YAML Configuration
# /etc/easyhaproxy/static/config.yaml
containers:
"api.example.com:443":
ip: ["api-service:8080"]
ssl: true
plugins: [jwt_validator]
plugin:
jwt_validator:
algorithm: RS256
issuer: https://auth.example.com/
audience: https://api.example.com
pubkey_path: /etc/easyhaproxy/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
# 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/easyhaproxy/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)
# 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/easyhaproxy/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)
# 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/easyhaproxy/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)
# 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/easyhaproxy/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