From d3637e737ab0ddaf52093ea2dfa81743e2b293a9 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Thu, 27 Nov 2025 19:49:32 -0500 Subject: [PATCH] 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. --- .../docker/docker-compose-jwt-validator.yml | 2 +- examples/static/README.md | 14 +-- examples/static/conf/config-basic.yml | 31 +++++++ examples/static/conf/config-certbot.yml | 88 +++++++++++++++++++ examples/static/conf/config-deny-pages.yml | 78 ++++++++++++++++ examples/static/conf/config-jwt-validator.yml | 86 ++++++++++++++++++ examples/static/conf/config.yml | 19 ---- src/plugins/builtin/cloudflare.py | 4 +- src/plugins/builtin/deny_pages.py | 4 +- src/plugins/builtin/ip_whitelist.py | 4 +- src/plugins/builtin/jwt_validator.py | 28 +++--- 11 files changed, 311 insertions(+), 47 deletions(-) create mode 100644 examples/static/conf/config-basic.yml create mode 100644 examples/static/conf/config-certbot.yml create mode 100644 examples/static/conf/config-deny-pages.yml create mode 100644 examples/static/conf/config-jwt-validator.yml delete mode 100644 examples/static/conf/config.yml diff --git a/examples/docker/docker-compose-jwt-validator.yml b/examples/docker/docker-compose-jwt-validator.yml index 2b50bbf..1a304a9 100644 --- a/examples/docker/docker-compose-jwt-validator.yml +++ b/examples/docker/docker-compose-jwt-validator.yml @@ -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 diff --git a/examples/static/README.md b/examples/static/README.md index aa90cd7..b22d168 100644 --- a/examples/static/README.md +++ b/examples/static/README.md @@ -321,13 +321,13 @@ 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 | -| Use case | Fixed infrastructure | Dynamic container environments | -| Plugin config | Global via YAML/env | Per-container/ingress via labels/annotations | +| 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 | +| Use case | Fixed infrastructure | Dynamic container environments | +| Plugin config | Global via YAML/env | Per-container/ingress via labels/annotations | --- diff --git a/examples/static/conf/config-basic.yml b/examples/static/conf/config-basic.yml new file mode 100644 index 0000000..ea8d4e1 --- /dev/null +++ b/examples/static/conf/config-basic.yml @@ -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 diff --git a/examples/static/conf/config-certbot.yml b/examples/static/conf/config-certbot.yml new file mode 100644 index 0000000..b61c389 --- /dev/null +++ b/examples/static/conf/config-certbot.yml @@ -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 diff --git a/examples/static/conf/config-deny-pages.yml b/examples/static/conf/config-deny-pages.yml new file mode 100644 index 0000000..2eb7d1f --- /dev/null +++ b/examples/static/conf/config-deny-pages.yml @@ -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 diff --git a/examples/static/conf/config-jwt-validator.yml b/examples/static/conf/config-jwt-validator.yml new file mode 100644 index 0000000..76319c0 --- /dev/null +++ b/examples/static/conf/config-jwt-validator.yml @@ -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 diff --git a/examples/static/conf/config.yml b/examples/static/conf/config.yml deleted file mode 100644 index a3e2d18..0000000 --- a/examples/static/conf/config.yml +++ /dev/null @@ -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 diff --git a/src/plugins/builtin/cloudflare.py b/src/plugins/builtin/cloudflare.py index 3a51a52..7433dad 100644 --- a/src/plugins/builtin/cloudflare.py +++ b/src/plugins/builtin/cloudflare.py @@ -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, diff --git a/src/plugins/builtin/deny_pages.py b/src/plugins/builtin/deny_pages.py index f1d3870..5335751 100644 --- a/src/plugins/builtin/deny_pages.py +++ b/src/plugins/builtin/deny_pages.py @@ -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, diff --git a/src/plugins/builtin/ip_whitelist.py b/src/plugins/builtin/ip_whitelist.py index 8a67222..b54265c 100644 --- a/src/plugins/builtin/ip_whitelist.py +++ b/src/plugins/builtin/ip_whitelist.py @@ -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, diff --git a/src/plugins/builtin/jwt_validator.py b/src/plugins/builtin/jwt_validator.py index 3ca1aa4..8fa5a09 100644 --- a/src/plugins/builtin/jwt_validator.py +++ b/src/plugins/builtin/jwt_validator.py @@ -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)