Refactor plugin configuration examples and improve formatting in documentation
- Replaced `config.yml` with modular configuration examples: `config-basic.yml`, `config-certbot.yml`, `config-deny-pages.yml`, and `config-jwt-validator.yml`. - Improved examples with detailed usage instructions, prerequisites, and testing steps for each configuration. - Fixed indentation and formatting inconsistencies across plugin files and HAProxy configuration generation. - Streamlined README comparison table for static vs. dynamic discovery. - Updated `docker-compose-jwt-validator.yml` to correct audience key formatting.
This commit is contained in:
parent
f75cb8aab2
commit
d3637e737a
11 changed files with 311 additions and 47 deletions
|
|
@ -62,5 +62,5 @@ services:
|
||||||
# JWT validator configuration
|
# JWT validator configuration
|
||||||
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
|
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
|
||||||
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
|
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.audience: https://api.example.com
|
||||||
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,7 @@ See [Using Plugins](../../docs/plugins.md) for more details.
|
||||||
## Comparison: Static vs. Dynamic Discovery
|
## Comparison: Static vs. Dynamic Discovery
|
||||||
|
|
||||||
| Feature | Static Mode | Docker/Swarm/K8s Mode |
|
| Feature | Static Mode | Docker/Swarm/K8s Mode |
|
||||||
|---------|-------------|----------------------|
|
|---------------|-----------------------------|----------------------------------------------|
|
||||||
| Configuration | YAML file | Container labels / Ingress annotations |
|
| Configuration | YAML file | Container labels / Ingress annotations |
|
||||||
| Backend types | Any (containers, VMs, IPs) | Containers only |
|
| Backend types | Any (containers, VMs, IPs) | Containers only |
|
||||||
| Updates | Manual config edit + reload | Automatic discovery |
|
| Updates | Manual config edit + reload | Automatic discovery |
|
||||||
|
|
|
||||||
31
examples/static/conf/config-basic.yml
Normal file
31
examples/static/conf/config-basic.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Basic Static Configuration Example
|
||||||
|
#
|
||||||
|
# This is a minimal configuration without plugins
|
||||||
|
# Demonstrates basic HTTP to HTTPS redirect and SSL setup
|
||||||
|
#
|
||||||
|
# To use:
|
||||||
|
# 1. Update the container name and ports to match your setup
|
||||||
|
# 2. Place SSL certificate at /certs/haproxy/host1.local.pem
|
||||||
|
# 3. Mount this config: -v ./conf/config-basic.yml:/etc/haproxy/static/config.yml
|
||||||
|
|
||||||
|
stats:
|
||||||
|
username: admin
|
||||||
|
password: password
|
||||||
|
port: 1936 # Optional (default 1936)
|
||||||
|
|
||||||
|
customerrors: true # Optional (default false)
|
||||||
|
|
||||||
|
easymapping:
|
||||||
|
# HTTP - Redirect to HTTPS
|
||||||
|
- port: 80
|
||||||
|
redirect:
|
||||||
|
host1.local: https://host1.local
|
||||||
|
www.host1.local: https://host1.local
|
||||||
|
|
||||||
|
# HTTPS - Serve application
|
||||||
|
- port: 443
|
||||||
|
ssl: true
|
||||||
|
hosts:
|
||||||
|
host1.local:
|
||||||
|
containers:
|
||||||
|
- container:8080
|
||||||
88
examples/static/conf/config-certbot.yml
Normal file
88
examples/static/conf/config-certbot.yml
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Certbot/Let's Encrypt Configuration Example
|
||||||
|
#
|
||||||
|
# Demonstrates:
|
||||||
|
# - Automatic SSL certificate generation with Let's Encrypt
|
||||||
|
# - HTTP to HTTPS redirect
|
||||||
|
# - Certificate renewal
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# 1. Public IP address with ports 80 and 443 accessible
|
||||||
|
# 2. DNS records pointing to your server:
|
||||||
|
# example.com -> your-server-ip
|
||||||
|
# www.example.com -> your-server-ip
|
||||||
|
#
|
||||||
|
# 3. Set environment variable:
|
||||||
|
# EASYHAPROXY_CERTBOT_EMAIL=your-email@example.com
|
||||||
|
#
|
||||||
|
# 4. Mount this config:
|
||||||
|
# -v ./conf/config-certbot.yml:/etc/haproxy/static/config.yml
|
||||||
|
#
|
||||||
|
# 5. Persist certificates:
|
||||||
|
# -v ./certs/certbot:/certs/certbot
|
||||||
|
#
|
||||||
|
# How it works:
|
||||||
|
# - EasyHAProxy requests certificates from Let's Encrypt via HTTP-01 challenge
|
||||||
|
# - Certificates are stored in /certs/certbot/
|
||||||
|
# - Certificates auto-renew when needed
|
||||||
|
#
|
||||||
|
# Note: Let's Encrypt has rate limits. Use staging environment for testing:
|
||||||
|
# EASYHAPROXY_CERTBOT_AUTOCONFIG=staging
|
||||||
|
|
||||||
|
stats:
|
||||||
|
username: admin
|
||||||
|
password: password
|
||||||
|
port: 1936
|
||||||
|
|
||||||
|
customerrors: true
|
||||||
|
|
||||||
|
easymapping:
|
||||||
|
# HTTP Port 80
|
||||||
|
# Required for ACME HTTP-01 challenge and redirect
|
||||||
|
- port: 80
|
||||||
|
hosts:
|
||||||
|
# Domain with certbot enabled
|
||||||
|
example.com:
|
||||||
|
containers:
|
||||||
|
- webapp:8080
|
||||||
|
# Enable certbot for this domain
|
||||||
|
certbot: true
|
||||||
|
# Redirect HTTP to HTTPS after cert is issued
|
||||||
|
redirect_ssl: true
|
||||||
|
|
||||||
|
# Additional domain with certbot
|
||||||
|
app.example.com:
|
||||||
|
containers:
|
||||||
|
- app:3000
|
||||||
|
certbot: true
|
||||||
|
redirect_ssl: true
|
||||||
|
|
||||||
|
# Domain without certbot (uses custom certificate)
|
||||||
|
custom.example.com:
|
||||||
|
containers:
|
||||||
|
- custom-app:8080
|
||||||
|
# No certbot - expects certificate at /certs/haproxy/custom.example.com.pem
|
||||||
|
|
||||||
|
# HTTPS Port 443
|
||||||
|
# Serves HTTPS traffic with auto-generated certificates
|
||||||
|
- port: 443
|
||||||
|
ssl: true
|
||||||
|
hosts:
|
||||||
|
example.com:
|
||||||
|
containers:
|
||||||
|
- webapp:8080
|
||||||
|
# Certificate path (auto-generated by certbot)
|
||||||
|
# /certs/certbot/example.com/fullchain.pem
|
||||||
|
|
||||||
|
app.example.com:
|
||||||
|
containers:
|
||||||
|
- app:3000
|
||||||
|
|
||||||
|
# Custom certificate example
|
||||||
|
custom.example.com:
|
||||||
|
containers:
|
||||||
|
- custom-app:8080
|
||||||
|
# Place your certificate at:
|
||||||
|
# /certs/haproxy/custom.example.com.pem
|
||||||
|
|
||||||
|
# Multiple domains with different backends
|
||||||
|
# Certbot will request separate certificates for each domain
|
||||||
78
examples/static/conf/config-deny-pages.yml
Normal file
78
examples/static/conf/config-deny-pages.yml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Deny Pages Plugin Configuration Example
|
||||||
|
#
|
||||||
|
# Demonstrates:
|
||||||
|
# - Global plugin configuration (applies to all domains)
|
||||||
|
# - Per-domain plugin override (custom settings per host)
|
||||||
|
#
|
||||||
|
# To use:
|
||||||
|
# 1. Update container names and ports
|
||||||
|
# 2. Mount this config: -v ./conf/config-deny-pages.yml:/etc/haproxy/static/config.yml
|
||||||
|
# 3. Test blocked paths:
|
||||||
|
# curl http://host1.local/admin # Should return 404
|
||||||
|
# curl http://host2.local/wp-admin # Should return 403 (different config)
|
||||||
|
|
||||||
|
stats:
|
||||||
|
username: admin
|
||||||
|
password: password
|
||||||
|
port: 1936
|
||||||
|
|
||||||
|
customerrors: true
|
||||||
|
|
||||||
|
# Global plugin configuration
|
||||||
|
# This applies to ALL domains unless overridden
|
||||||
|
plugins:
|
||||||
|
enabled:
|
||||||
|
- deny_pages
|
||||||
|
|
||||||
|
config:
|
||||||
|
deny_pages:
|
||||||
|
# Global default: block common admin paths with 404
|
||||||
|
paths:
|
||||||
|
- /admin
|
||||||
|
- /.env
|
||||||
|
- /config
|
||||||
|
status_code: 404 # Hide existence of these paths
|
||||||
|
|
||||||
|
easymapping:
|
||||||
|
- port: 80
|
||||||
|
hosts:
|
||||||
|
# Domain 1: Uses global deny_pages configuration
|
||||||
|
host1.local:
|
||||||
|
containers:
|
||||||
|
- webapp1:8080
|
||||||
|
# No plugins specified = uses global configuration
|
||||||
|
|
||||||
|
# Domain 2: WordPress site with custom blocked paths
|
||||||
|
host2.local:
|
||||||
|
containers:
|
||||||
|
- wordpress:80
|
||||||
|
# Override global plugin configuration for this domain
|
||||||
|
plugins:
|
||||||
|
- deny_pages
|
||||||
|
plugin_config:
|
||||||
|
deny_pages:
|
||||||
|
paths:
|
||||||
|
- /wp-admin
|
||||||
|
- /wp-login.php
|
||||||
|
- /xmlrpc.php
|
||||||
|
- /wp-config.php
|
||||||
|
status_code: 403 # Return forbidden instead of 404
|
||||||
|
|
||||||
|
# Domain 3: Public site with stricter blocking
|
||||||
|
host3.local:
|
||||||
|
containers:
|
||||||
|
- publicsite:3000
|
||||||
|
plugins:
|
||||||
|
- deny_pages
|
||||||
|
plugin_config:
|
||||||
|
deny_pages:
|
||||||
|
paths:
|
||||||
|
- /admin
|
||||||
|
- /administrator
|
||||||
|
- /manager
|
||||||
|
- /phpmyadmin
|
||||||
|
- /.git
|
||||||
|
- /.env
|
||||||
|
- /config
|
||||||
|
- /backup
|
||||||
|
status_code: 404
|
||||||
86
examples/static/conf/config-jwt-validator.yml
Normal file
86
examples/static/conf/config-jwt-validator.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# JWT Validator Plugin Configuration Example
|
||||||
|
#
|
||||||
|
# Demonstrates:
|
||||||
|
# - JWT token validation for API protection
|
||||||
|
# - Different JWT configurations per domain
|
||||||
|
# - Optional issuer/audience validation
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# 1. Generate RSA key pair:
|
||||||
|
# openssl genrsa -out jwt_private.pem 2048
|
||||||
|
# openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
|
||||||
|
#
|
||||||
|
# 2. Mount public keys:
|
||||||
|
# -v ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
# -v ./jwt_pubkey2.pem:/etc/haproxy/jwt_keys/admin_pubkey.pem:ro
|
||||||
|
#
|
||||||
|
# 3. Mount this config:
|
||||||
|
# -v ./conf/config-jwt-validator.yml:/etc/haproxy/static/config.yml
|
||||||
|
#
|
||||||
|
# 4. Test:
|
||||||
|
# # Without token - should fail
|
||||||
|
# curl http://api.local/users
|
||||||
|
# # Response: Missing Authorization HTTP header
|
||||||
|
#
|
||||||
|
# # With valid token - should succeed
|
||||||
|
# curl -H "Authorization: Bearer eyJhbGc..." http://api.local/users
|
||||||
|
|
||||||
|
stats:
|
||||||
|
username: admin
|
||||||
|
password: password
|
||||||
|
port: 1936
|
||||||
|
|
||||||
|
customerrors: true
|
||||||
|
|
||||||
|
easymapping:
|
||||||
|
- port: 80
|
||||||
|
hosts:
|
||||||
|
# Public API with full JWT validation
|
||||||
|
api.local:
|
||||||
|
containers:
|
||||||
|
- api-server: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
|
||||||
|
|
||||||
|
# Internal API - validate signature only (no issuer/audience check)
|
||||||
|
internal-api.local:
|
||||||
|
containers:
|
||||||
|
- internal-api:3000
|
||||||
|
plugins:
|
||||||
|
- jwt_validator
|
||||||
|
plugin_config:
|
||||||
|
jwt_validator:
|
||||||
|
algorithm: RS256
|
||||||
|
# No issuer/audience = skip those validations
|
||||||
|
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
|
||||||
|
# Admin API - different issuer and key
|
||||||
|
admin-api.local:
|
||||||
|
containers:
|
||||||
|
- admin-api:4000
|
||||||
|
plugins:
|
||||||
|
- jwt_validator
|
||||||
|
- deny_pages # Also block internal paths
|
||||||
|
plugin_config:
|
||||||
|
jwt_validator:
|
||||||
|
algorithm: RS256
|
||||||
|
issuer: https://admin-auth.example.com/
|
||||||
|
audience: https://admin.example.com
|
||||||
|
pubkey_path: /etc/haproxy/jwt_keys/admin_pubkey.pem
|
||||||
|
deny_pages:
|
||||||
|
paths:
|
||||||
|
- /internal
|
||||||
|
- /debug
|
||||||
|
status_code: 403
|
||||||
|
|
||||||
|
# Public website - no JWT required
|
||||||
|
website.local:
|
||||||
|
containers:
|
||||||
|
- website:8080
|
||||||
|
# No plugins = public access
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
stats:
|
|
||||||
username: admin
|
|
||||||
password: password
|
|
||||||
port: 1936 # Optional (default 1936)
|
|
||||||
|
|
||||||
customerrors: true # Optional (default false)
|
|
||||||
|
|
||||||
easymapping:
|
|
||||||
- port: 80
|
|
||||||
redirect:
|
|
||||||
host1.local: https://host1.local
|
|
||||||
www.host1.local: https://host1.local
|
|
||||||
|
|
||||||
- port: 443
|
|
||||||
ssl: true
|
|
||||||
hosts:
|
|
||||||
host1.local:
|
|
||||||
containers:
|
|
||||||
- container:8080
|
|
||||||
|
|
@ -76,8 +76,8 @@ class CloudflarePlugin(PluginInterface):
|
||||||
|
|
||||||
# Generate HAProxy config snippet
|
# Generate HAProxy config snippet
|
||||||
haproxy_config = f"""# Cloudflare - Restore original visitor IP
|
haproxy_config = f"""# Cloudflare - Restore original visitor IP
|
||||||
acl from_cloudflare src -f {self.ip_list_path}
|
acl from_cloudflare src -f {self.ip_list_path}
|
||||||
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
|
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
|
||||||
|
|
||||||
return PluginResult(
|
return PluginResult(
|
||||||
haproxy_config=haproxy_config,
|
haproxy_config=haproxy_config,
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ class DenyPagesPlugin(PluginInterface):
|
||||||
|
|
||||||
# Generate HAProxy config snippet
|
# Generate HAProxy config snippet
|
||||||
haproxy_config = f"""# Deny Pages - Block specific paths
|
haproxy_config = f"""# Deny Pages - Block specific paths
|
||||||
acl denied_path path_beg {paths_str}
|
acl denied_path path_beg {paths_str}
|
||||||
http-request deny deny_status {self.status_code} if denied_path"""
|
http-request deny deny_status {self.status_code} if denied_path"""
|
||||||
|
|
||||||
return PluginResult(
|
return PluginResult(
|
||||||
haproxy_config=haproxy_config,
|
haproxy_config=haproxy_config,
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ class IpWhitelistPlugin(PluginInterface):
|
||||||
|
|
||||||
# Generate HAProxy config snippet
|
# Generate HAProxy config snippet
|
||||||
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
|
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
|
||||||
acl whitelisted_ip src {ips_str}
|
acl whitelisted_ip src {ips_str}
|
||||||
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
|
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
|
||||||
|
|
||||||
return PluginResult(
|
return PluginResult(
|
||||||
haproxy_config=haproxy_config,
|
haproxy_config=haproxy_config,
|
||||||
|
|
|
||||||
|
|
@ -144,37 +144,37 @@ class JwtValidatorPlugin(PluginInterface):
|
||||||
lines = ["# JWT Validator - Validate JWT tokens"]
|
lines = ["# JWT Validator - Validate JWT tokens"]
|
||||||
|
|
||||||
# Check for Authorization header
|
# Check for Authorization header
|
||||||
lines.append(" http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }")
|
lines.append("http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }")
|
||||||
|
|
||||||
# Extract JWT parts
|
# Extract JWT parts
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(" # Extract JWT header and payload")
|
lines.append("# Extract JWT header and payload")
|
||||||
lines.append(" http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')")
|
lines.append("http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')")
|
||||||
lines.append(" http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')")
|
lines.append("http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')")
|
||||||
lines.append(" http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')")
|
lines.append("http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')")
|
||||||
lines.append(" http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')")
|
lines.append("http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')")
|
||||||
|
|
||||||
# Validate JWT
|
# Validate JWT
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(" # Validate JWT")
|
lines.append("# Validate JWT")
|
||||||
lines.append(f" http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless {{ var(txn.alg) -m str {self.algorithm} }}")
|
lines.append(f"http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless {{ var(txn.alg) -m str {self.algorithm} }}")
|
||||||
|
|
||||||
# Validate issuer (if configured)
|
# Validate issuer (if configured)
|
||||||
if self.issuer:
|
if self.issuer:
|
||||||
lines.append(f" http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless {{ var(txn.iss) -m str {self.issuer} }}")
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless {{ var(txn.iss) -m str {self.issuer} }}")
|
||||||
|
|
||||||
# Validate audience (if configured)
|
# Validate audience (if configured)
|
||||||
if self.audience:
|
if self.audience:
|
||||||
lines.append(f" http-request deny content-type 'text/html' string 'Invalid JWT audience' unless {{ var(txn.aud) -m str {self.audience} }}")
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT audience' unless {{ var(txn.aud) -m str {self.audience} }}")
|
||||||
|
|
||||||
# Validate signature
|
# Validate signature
|
||||||
lines.append(f" http-request deny content-type 'text/html' string 'Invalid JWT signature' unless {{ http_auth_bearer,jwt_verify(txn.alg,\"{pubkey_file}\") -m int 1 }}")
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT signature' unless {{ http_auth_bearer,jwt_verify(txn.alg,\"{pubkey_file}\") -m int 1 }}")
|
||||||
|
|
||||||
# Validate expiration
|
# Validate expiration
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(" # Validate expiration")
|
lines.append("# Validate expiration")
|
||||||
lines.append(" http-request set-var(txn.now) date()")
|
lines.append("http-request set-var(txn.now) date()")
|
||||||
lines.append(" http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }")
|
lines.append("http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }")
|
||||||
|
|
||||||
haproxy_config = "\n".join(lines)
|
haproxy_config = "\n".join(lines)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue