1
0
Fork 0

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:
Joao Gilberto Magalhaes 2025-11-27 19:49:32 -05:00
parent f75cb8aab2
commit d3637e737a
11 changed files with 311 additions and 47 deletions

View file

@ -62,5 +62,5 @@ services:
# JWT validator configuration
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.audience: https://api.example.com
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem

View file

@ -322,7 +322,7 @@ See [Using Plugins](../../docs/plugins.md) for more details.
## Comparison: Static vs. Dynamic Discovery
| Feature | Static Mode | Docker/Swarm/K8s Mode |
|---------|-------------|----------------------|
|---------------|-----------------------------|----------------------------------------------|
| Configuration | YAML file | Container labels / Ingress annotations |
| Backend types | Any (containers, VMs, IPs) | Containers only |
| Updates | Manual config edit + reload | Automatic discovery |

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -76,8 +76,8 @@ class CloudflarePlugin(PluginInterface):
# Generate HAProxy config snippet
haproxy_config = f"""# Cloudflare - Restore original visitor IP
acl from_cloudflare src -f {self.ip_list_path}
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
acl from_cloudflare src -f {self.ip_list_path}
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
return PluginResult(
haproxy_config=haproxy_config,

View file

@ -92,8 +92,8 @@ class DenyPagesPlugin(PluginInterface):
# Generate HAProxy config snippet
haproxy_config = f"""# Deny Pages - Block specific paths
acl denied_path path_beg {paths_str}
http-request deny deny_status {self.status_code} if denied_path"""
acl denied_path path_beg {paths_str}
http-request deny deny_status {self.status_code} if denied_path"""
return PluginResult(
haproxy_config=haproxy_config,

View file

@ -93,8 +93,8 @@ class IpWhitelistPlugin(PluginInterface):
# Generate HAProxy config snippet
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
acl whitelisted_ip src {ips_str}
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
acl whitelisted_ip src {ips_str}
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
return PluginResult(
haproxy_config=haproxy_config,

View file

@ -144,37 +144,37 @@ class JwtValidatorPlugin(PluginInterface):
lines = ["# JWT Validator - Validate JWT tokens"]
# 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
lines.append("")
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.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.exp) http_auth_bearer,jwt_payload_query('$.exp','int')")
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.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.exp) http_auth_bearer,jwt_payload_query('$.exp','int')")
# Validate JWT
lines.append("")
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("# 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} }}")
# Validate issuer (if configured)
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)
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
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
lines.append("")
lines.append(" # Validate expiration")
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("# Validate expiration")
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 }")
haproxy_config = "\n".join(lines)