1
0
Fork 0

Migrate configuration paths to /etc/easyhaproxy and improve health check support in E2E tests

- 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.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-15 14:32:16 -05:00
parent 3e963228f3
commit 045dd3817e
73 changed files with 600 additions and 287 deletions

1
.gitignore vendored
View file

@ -13,6 +13,7 @@ __pycache__
# Build artifacts # Build artifacts
dist/ dist/
build/ build/
!build/assets/
*.egg-info/ *.egg-info/
/tests_e2e/static/conf/config.yml /tests_e2e/static/conf/config.yml

View file

@ -5,16 +5,17 @@
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.12 (docker-easy-haproxy)" /> <option name="SDK_NAME" value="uv (docker-easy-haproxy)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/src" />
<option name="IS_MODULE_SDK" value="false" /> <option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="RUN_TOOL" value="true" />
<option name="_new_keywords" value="&quot;&quot;" /> <option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" /> <option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" /> <option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/src/tests&quot;" /> <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" /> <option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" /> <method v="2" />
</configuration> </configuration>

View file

@ -10,8 +10,17 @@ RUN apk add --no-cache haproxy bash python3 certbot openssl curl \
&& curl -LsSf https://astral.sh/uv/install.sh | sh \ && curl -LsSf https://astral.sh/uv/install.sh | sh \
&& ln -s /root/.local/bin/uv /usr/local/bin/uv && ln -s /root/.local/bin/uv /usr/local/bin/uv
RUN openssl dhparam -out /etc/haproxy/dhparam 2048 \ RUN mkdir -p /etc/easyhaproxy/haproxy \
&& openssl dhparam -out /etc/haproxy/dhparam-1024 1024 && openssl dhparam -out /etc/easyhaproxy/haproxy/dhparam 2048 \
&& openssl dhparam -out /etc/easyhaproxy/haproxy/dhparam-1024 1024 \
&& mkdir -p /etc/easyhaproxy/certs/certbot /etc/easyhaproxy/certs/haproxy \
&& openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout /tmp/placeholder.key \
-out /tmp/placeholder.crt \
-subj "/CN=placeholder" \
&& cat /tmp/placeholder.crt /tmp/placeholder.key > /etc/easyhaproxy/certs/certbot/placeholder.pem \
&& cat /tmp/placeholder.crt /tmp/placeholder.key > /etc/easyhaproxy/certs/haproxy/placeholder.pem \
&& rm /tmp/placeholder.key /tmp/placeholder.crt
WORKDIR /scripts WORKDIR /scripts

View file

@ -0,0 +1,5 @@
# Custom HAProxy Configuration
Put `.cfg` files here to be included in the HAProxy configuration.
These files will be available at `/etc/easyhaproxy/haproxy/conf.d/` inside the container.

View file

@ -4,7 +4,7 @@ Connection: close
Content-Type: text/html Content-Type: text/html
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>Ins
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>400 Bad request</title> <title>400 Bad request</title>

View file

@ -1,3 +0,0 @@
# Custom HAProxy
Put files .cfg to be included in the configuration

View file

@ -3,8 +3,8 @@ services:
image: byjg/easy-haproxy:5.0.0 image: byjg/easy-haproxy:5.0.0
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- certs_certbot:/certs/certbot - certs_certbot:/etc/easyhaproxy/certs/certbot
- certs_haproxy:/certs/haproxy - certs_haproxy:/etc/easyhaproxy/certs/haproxy
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker

View file

@ -28,7 +28,7 @@ Prevents disk space issues by automatically cleaning up temporary files created
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
plugins: plugins:
enabled: [cleanup] enabled: [cleanup]
config: config:
@ -53,7 +53,7 @@ Configure the Cleanup plugin globally:
### Custom Idle Time (1 hour) ### Custom Idle Time (1 hour)
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
plugins: plugins:
enabled: [cleanup] enabled: [cleanup]
config: config:

View file

@ -18,10 +18,10 @@ Cloudflare replaces the visitor's IP with its own. This plugin restores the orig
## Configuration Options ## Configuration Options
| Option | Description | Default | | Option | Description | Default |
|-------------------|------------------------------------------|-----------------------------------| |-------------------|------------------------------------------|---------------------------------------|
| `enabled` | Enable/disable plugin | `true` | | `enabled` | Enable/disable plugin | `true` |
| `use_builtin_ips` | Use built-in Cloudflare IP ranges | `true` | | `use_builtin_ips` | Use built-in Cloudflare IP ranges | `true` |
| `ip_list_path` | Path to Cloudflare IP list | `/etc/haproxy/cloudflare_ips.lst` | | `ip_list_path` | Path to Cloudflare IP list | `/etc/easyhaproxy/cloudflare_ips.lst` |
## Configuration Examples ## Configuration Examples
@ -55,7 +55,7 @@ kind: Ingress
metadata: metadata:
annotations: annotations:
easyhaproxy.plugins: "cloudflare" easyhaproxy.plugins: "cloudflare"
easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst" easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/easyhaproxy/cloudflare_ips.lst"
spec: spec:
rules: rules:
- host: example.com - host: example.com
@ -72,7 +72,7 @@ spec:
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
plugins: plugins:
config: config:
cloudflare: cloudflare:
@ -85,10 +85,10 @@ plugins:
Configure Cloudflare plugin defaults for all domains: Configure Cloudflare plugin defaults for all domains:
| Environment Variable | Config Key | Type | Default | Description | | Environment Variable | Config Key | Type | Default | Description |
|-------------------------------------------------|-------------------|----------|-----------------------------------|---------------------------------------| |-------------------------------------------------|-------------------|----------|---------------------------------------|---------------------------------------|
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains | | `EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_USE_BUILTIN_IPS` | `use_builtin_ips` | boolean | `true` | Use built-in Cloudflare IP ranges | | `EASYHAPROXY_PLUGIN_CLOUDFLARE_USE_BUILTIN_IPS` | `use_builtin_ips` | boolean | `true` | Use built-in Cloudflare IP ranges |
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH` | `ip_list_path` | string | `/etc/haproxy/cloudflare_ips.lst` | Path to Cloudflare IP list file | | `EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH` | `ip_list_path` | string | `/etc/easyhaproxy/cloudflare_ips.lst` | Path to Cloudflare IP list file |
**Note:** Environment variables set defaults for ALL domains. To enable/disable per-domain, use container labels or Kubernetes annotations. **Note:** Environment variables set defaults for ALL domains. To enable/disable per-domain, use container labels or Kubernetes annotations.
@ -96,7 +96,7 @@ Configure Cloudflare plugin defaults for all domains:
```haproxy ```haproxy
# Cloudflare - Restore original visitor IP # Cloudflare - Restore original visitor IP
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst
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
``` ```
@ -114,7 +114,7 @@ The plugin includes the current Cloudflare IP ranges (22 ranges total):
- 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32 - 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32
- 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32 - 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32
These ranges are automatically written to `/etc/haproxy/cloudflare_ips.lst` during each discovery cycle. These ranges are automatically written to `/etc/easyhaproxy/cloudflare_ips.lst` during each discovery cycle.
## Important Notes ## Important Notes

View file

@ -73,7 +73,7 @@ spec:
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
containers: containers:
"example.com:80": "example.com:80":
ip: ["webapp:80"] ip: ["webapp:80"]

View file

@ -20,7 +20,7 @@ Automatically generates HAProxy `fcgi-app` configuration that defines required C
| Option | Description | Default | | Option | Description | Default |
|-------------------|-----------------------------------------|------------------------------------| |-------------------|-----------------------------------------|------------------------------------|
| `enabled` | Enable/disable plugin | `true` | | `enabled` | Enable/disable plugin | `true` |
| `document_root` | Document root path | `/var/www/html` | | `document_root` | Document root path | `/etc/easyhaproxy/www` |
| `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) | | `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) |
| `index_file` | Default index file | `index.php` | | `index_file` | Default index file | `index.php` |
| `path_info` | Enable PATH_INFO support | `true` | | `path_info` | Enable PATH_INFO support | `true` |
@ -40,10 +40,10 @@ services:
easyhaproxy.http.localport: 9000 easyhaproxy.http.localport: 9000
easyhaproxy.http.proto: fcgi easyhaproxy.http.proto: fcgi
easyhaproxy.http.plugins: fastcgi easyhaproxy.http.plugins: fastcgi
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html easyhaproxy.http.plugin.fastcgi.document_root: /etc/easyhaproxy/www
easyhaproxy.http.plugin.fastcgi.index_file: index.php easyhaproxy.http.plugin.fastcgi.index_file: index.php
volumes: volumes:
- ./app:/var/www/html - ./app:/etc/easyhaproxy/www
``` ```
### Docker/Docker Compose (Unix socket) ### Docker/Docker Compose (Unix socket)
@ -57,10 +57,10 @@ services:
easyhaproxy.http.socket: /run/php/php-fpm.sock easyhaproxy.http.socket: /run/php/php-fpm.sock
easyhaproxy.http.proto: fcgi easyhaproxy.http.proto: fcgi
easyhaproxy.http.plugins: fastcgi easyhaproxy.http.plugins: fastcgi
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html easyhaproxy.http.plugin.fastcgi.document_root: /etc/easyhaproxy/www
easyhaproxy.http.plugin.fastcgi.index_file: index.php easyhaproxy.http.plugin.fastcgi.index_file: index.php
volumes: volumes:
- ./app:/var/www/html - ./app:/etc/easyhaproxy/www
- /run/php:/run/php - /run/php:/run/php
``` ```
@ -82,7 +82,7 @@ kind: Ingress
metadata: metadata:
annotations: annotations:
easyhaproxy.plugins: "fastcgi" easyhaproxy.plugins: "fastcgi"
easyhaproxy.plugin.fastcgi.document_root: "/var/www/html" easyhaproxy.plugin.fastcgi.document_root: "/etc/easyhaproxy/www"
easyhaproxy.plugin.fastcgi.index_file: "index.php" easyhaproxy.plugin.fastcgi.index_file: "index.php"
spec: spec:
rules: rules:
@ -100,7 +100,7 @@ spec:
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
easymapping: easymapping:
- host: phpapp.local - host: phpapp.local
port: 80 port: 80
@ -110,7 +110,7 @@ easymapping:
- fastcgi - fastcgi
plugin_config: plugin_config:
fastcgi: fastcgi:
document_root: /var/www/html document_root: /etc/easyhaproxy/www
index_file: index.php index_file: index.php
path_info: true path_info: true
``` ```
@ -122,7 +122,7 @@ Configure FastCGI plugin defaults for all domains:
| Environment Variable | Config Key | Type | Default | Description | | Environment Variable | Config Key | Type | Default | Description |
|----------------------------------------------|-------------------|----------|-----------------|---------------------------------------| |----------------------------------------------|-------------------|----------|-----------------|---------------------------------------|
| `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains | | `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
| `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/var/www/html` | Document root path | | `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/etc/easyhaproxy/www` | Document root path |
| `EASYHAPROXY_PLUGIN_FASTCGI_SCRIPT_FILENAME` | `script_filename` | string | `%[path]` | Custom pattern for SCRIPT_FILENAME | | `EASYHAPROXY_PLUGIN_FASTCGI_SCRIPT_FILENAME` | `script_filename` | string | `%[path]` | Custom pattern for SCRIPT_FILENAME |
| `EASYHAPROXY_PLUGIN_FASTCGI_INDEX_FILE` | `index_file` | string | `index.php` | Default index file | | `EASYHAPROXY_PLUGIN_FASTCGI_INDEX_FILE` | `index_file` | string | `index.php` | Default index file |
| `EASYHAPROXY_PLUGIN_FASTCGI_PATH_INFO` | `path_info` | boolean | `true` | Enable PATH_INFO support | | `EASYHAPROXY_PLUGIN_FASTCGI_PATH_INFO` | `path_info` | boolean | `true` | Enable PATH_INFO support |
@ -136,7 +136,7 @@ The plugin generates a top-level `fcgi-app` section and a `use-fcgi-app` directi
```haproxy ```haproxy
# Top-level fcgi-app definition (added after defaults, before frontends/backends) # Top-level fcgi-app definition (added after defaults, before frontends/backends)
fcgi-app fcgi_phpapp_local fcgi-app fcgi_phpapp_local
docroot /var/www/html docroot /etc/easyhaproxy/www
index index.php index index.php
path-info ^(/.+\.php)(/.*)?$ path-info ^(/.+\.php)(/.*)?$

View file

@ -72,7 +72,7 @@ spec:
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
easymapping: easymapping:
- host: admin.example.com - host: admin.example.com
port: 443 port: 443

View file

@ -77,9 +77,9 @@ services:
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/easyhaproxy/jwt_keys/api_pubkey.pem
volumes: volumes:
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
``` ```
### Protect Specific Paths Only ### Protect Specific Paths Only
@ -87,7 +87,7 @@ services:
```yaml ```yaml
labels: labels:
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem 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.paths: /api/admin,/api/sensitive
easyhaproxy.http.plugin.jwt_validator.only_paths: false easyhaproxy.http.plugin.jwt_validator.only_paths: false
# /api/health, /api/docs, etc. remain publicly accessible # /api/health, /api/docs, etc. remain publicly accessible
@ -98,7 +98,7 @@ labels:
```yaml ```yaml
labels: labels:
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem 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.paths: /api/public,/api/v1
easyhaproxy.http.plugin.jwt_validator.only_paths: true easyhaproxy.http.plugin.jwt_validator.only_paths: true
# All paths except /api/public and /api/v1 are denied # All paths except /api/public and /api/v1 are denied
@ -110,7 +110,7 @@ labels:
labels: labels:
easyhaproxy.http.plugin.jwt_validator.issuer: none easyhaproxy.http.plugin.jwt_validator.issuer: none
easyhaproxy.http.plugin.jwt_validator.audience: none easyhaproxy.http.plugin.jwt_validator.audience: none
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
``` ```
### Allow Anonymous Access (Optional JWT) ### Allow Anonymous Access (Optional JWT)
@ -121,10 +121,10 @@ services:
labels: labels:
easyhaproxy.http.host: api.example.com easyhaproxy.http.host: api.example.com
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true
volumes: volumes:
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
# Requests without Authorization header are allowed # Requests without Authorization header are allowed
# Requests with Authorization header are validated # Requests with Authorization header are validated
# Invalid JWTs are rejected # Invalid JWTs are rejected
@ -204,7 +204,7 @@ metadata:
easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.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.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users" easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
easyhaproxy.plugin.jwt_validator.only_paths: "false" easyhaproxy.plugin.jwt_validator.only_paths: "false"
spec: spec:
@ -227,7 +227,7 @@ spec:
### Static YAML Configuration ### Static YAML Configuration
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
containers: containers:
"api.example.com:443": "api.example.com:443":
ip: ["api-service:8080"] ip: ["api-service:8080"]
@ -238,7 +238,7 @@ containers:
algorithm: RS256 algorithm: RS256
issuer: https://auth.example.com/ issuer: https://auth.example.com/
audience: https://api.example.com audience: https://api.example.com
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
``` ```
### Environment Variables ### Environment Variables
@ -277,7 +277,7 @@ http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
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 '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 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 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 } 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 # Validate expiration
http-request set-var(txn.now) date() http-request set-var(txn.now) date()
@ -303,7 +303,7 @@ http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
# Validate JWT (only on protected paths) # 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 '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 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 # Validate expiration
http-request set-var(txn.now) date() if jwt_protected_path http-request set-var(txn.now) date() if jwt_protected_path
@ -332,7 +332,7 @@ 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) # 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 '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 } 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 # Validate expiration
http-request set-var(txn.now) date() http-request set-var(txn.now) date()
@ -356,7 +356,7 @@ http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
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 '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 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 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 } 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) # Validate expiration (only if Authorization header is present)
http-request set-var(txn.now) date() if { req.hdr(authorization) -m found } http-request set-var(txn.now) date() if { req.hdr(authorization) -m found }

View file

@ -33,7 +33,7 @@ At a high level, ACME with Easy HAProxy works in two stages:
- Manually setting `EASYHAPROXY_CERTBOT_SERVER` (and `EASYHAPROXY_CERTBOT_EAB_KID` / `EASYHAPROXY_CERTBOT_EAB_HMAC_KEY` when your CA requires EAB). - Manually setting `EASYHAPROXY_CERTBOT_SERVER` (and `EASYHAPROXY_CERTBOT_EAB_KID` / `EASYHAPROXY_CERTBOT_EAB_HMAC_KEY` when your CA requires EAB).
- Always set your contact email via `EASYHAPROXY_CERTBOT_EMAIL`. - Always set your contact email via `EASYHAPROXY_CERTBOT_EMAIL`.
- Ensure ports 80 and 443 are publicly reachable on the EasyHAProxy host. - Ensure ports 80 and 443 are publicly reachable on the EasyHAProxy host.
- Persist the folder `/certs/certbot` on a durable volume so issued/renewed certificates survive container restarts and avoid hitting CA rate limits. - Persist the folder `/etc/easyhaproxy/certs/certbot` on a durable volume so issued/renewed certificates survive container restarts and avoid hitting CA rate limits.
- Challenge method is HTTP-01 only; EasyHAProxy configures a standalone Certbot responder internally. - Challenge method is HTTP-01 only; EasyHAProxy configures a standalone Certbot responder internally.
2. Enable ACME per domain (per service/app) 2. Enable ACME per domain (per service/app)
@ -44,7 +44,7 @@ At a high level, ACME with Easy HAProxy works in two stages:
What happens under the hood What happens under the hood
- When a labeled domain is detected and a certificate is needed, EasyHAProxy runs Certbot with `--preferred-challenges http` and a standalone responder bound to internal port 2080. - When a labeled domain is detected and a certificate is needed, EasyHAProxy runs Certbot with `--preferred-challenges http` and a standalone responder bound to internal port 2080.
- HAProxy temporarily routes `/.well-known/acme-challenge/` for that domain to the Certbot responder, allowing the CA to validate via HTTP-01. - HAProxy temporarily routes `/.well-known/acme-challenge/` for that domain to the Certbot responder, allowing the CA to validate via HTTP-01.
- On success, EasyHAProxy merges the issued cert and key and stores them under `/certs/certbot` (one PEM per domain), then reloads HAProxy to serve HTTPS for that domain. - On success, EasyHAProxy merges the issued cert and key and stores them under `/etc/easyhaproxy/certs/certbot` (one PEM per domain), then reloads HAProxy to serve HTTPS for that domain.
- Certificates are monitored and renewed automatically before expiry. - Certificates are monitored and renewed automatically before expiry.
Tips Tips
@ -105,7 +105,7 @@ docker run \
-e EASYHAPROXY_CERTBOT_EMAIL=john@doe.com \ -e EASYHAPROXY_CERTBOT_EMAIL=john@doe.com \
-p 80:80 \ -p 80:80 \
-p 443:443 \ -p 443:443 \
-v /path/to/guest/certbot/certs:/certs/certbot \ -v /path/to/guest/certbot/certs:/etc/easyhaproxy/certs/certbot \
... \ ... \
byjg/easy-haproxy byjg/easy-haproxy
``` ```
@ -118,7 +118,7 @@ docker run \
:::danger Important: Persist Certbot Certificates :::danger Important: Persist Certbot Certificates
To avoid hitting rate limits and certificate issuing problems: To avoid hitting rate limits and certificate issuing problems:
- **You must persist** the container folder `/certs/certbot` outside the container - **You must persist** the container folder `/etc/easyhaproxy/certs/certbot` outside the container
- **Never delete or modify** its contents manually - **Never delete or modify** its contents manually
- If you don't persist this folder, or if you delete/modify its contents, certificate issuing may not work properly and you may hit rate limits - If you don't persist this folder, or if you delete/modify its contents, certificate issuing may not work properly and you may hit rate limits
::: :::

View file

@ -5,9 +5,10 @@ sidebar_position: 12
# Docker environment variables # Docker environment variables
| Environment Variable | Description | Default | | Environment Variable | Description | Default |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** | | EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** |
| EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` | | EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` |
| EASYHAPROXY_BASE_PATH | (Optional) Base directory for all EasyHAProxy files. All paths (config, certs, plugins, www) are constructed relative to this base. | `/etc/easyhaproxy` |
| EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *empty* | | EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *empty* |
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` | | EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` |
| EASYHAPROXY_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 | | EASYHAPROXY_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 |

View file

@ -177,7 +177,7 @@ metadata:
easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.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.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
spec: spec:
ingressClassName: easyhaproxy ingressClassName: easyhaproxy
``` ```

View file

@ -36,18 +36,18 @@ docker run \
## Mapping custom .cfg files ## Mapping custom .cfg files
You can concatenate valid HAProxy `.cfg` files to the dynamically generated `haproxy.cfg` by mapping the folder `/etc/haproxy/conf.d`. You can concatenate valid HAProxy `.cfg` files to the dynamically generated `haproxy.cfg` by mapping the folder `/etc/easyhaproxy/haproxy/conf.d`.
```bash title="Mount custom config directory" ```bash title="Mount custom config directory"
docker run \ docker run \
/* other parameters */ /* other parameters */
-v /your/local/conf.d:/etc/haproxy/conf.d \ -v /your/local/conf.d:/etc/easyhaproxy/haproxy/conf.d \
-d byjg/easy-haproxy -d byjg/easy-haproxy
``` ```
## Setting Custom Errors ## Setting Custom Errors
If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http` If enabled, map the volume : `/etc/easyhaproxy/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http`
where ERROR_NUMBER is the HTTP error code (e.g., `503.http`) where ERROR_NUMBER is the HTTP error code (e.g., `503.http`)
---- ----

View file

@ -127,7 +127,7 @@ Execute **once per discovered domain/host**.
### Plugin Loading Order ### Plugin Loading Order
1. **Builtin plugins** - Loaded from `/src/plugins/builtin/` 1. **Builtin plugins** - Loaded from `/src/plugins/builtin/`
2. **External plugins** - Loaded from `/etc/haproxy/plugins/` 2. **External plugins** - Loaded from `/etc/easyhaproxy/plugins/`
Plugins are discovered automatically by filename (`*.py` excluding `__*.py`). Plugins are discovered automatically by filename (`*.py` excluding `__*.py`).
@ -160,10 +160,10 @@ HAProxy Reload
### Step 1: Create Plugin File ### Step 1: Create Plugin File
Create a new Python file in `/etc/haproxy/plugins/` (or builtin location for core plugins): Create a new Python file in `/etc/easyhaproxy/plugins/` (or builtin location for core plugins):
```python ```python
# /etc/haproxy/plugins/my_plugin.py # /etc/easyhaproxy/plugins/my_plugin.py
import os import os
import sys import sys
@ -248,7 +248,7 @@ services:
**Via YAML configuration:** **Via YAML configuration:**
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
plugins: plugins:
enabled: [my_plugin] enabled: [my_plugin]
config: config:
@ -521,13 +521,13 @@ return PluginResult(
return PluginResult( return PluginResult(
haproxy_config="use-fcgi-app fcgi_example_com", haproxy_config="use-fcgi-app fcgi_example_com",
global_configs=[ global_configs=[
"fcgi-app fcgi_example_com\n docroot /var/www/html" "fcgi-app fcgi_example_com\n docroot /etc/easyhaproxy/www"
] ]
) )
# With defaults-level config (new in v2.0) # With defaults-level config (new in v2.0)
return PluginResult( return PluginResult(
haproxy_config="acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst", haproxy_config="acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst",
defaults_configs=[ defaults_configs=[
'log-format "%{+Q}[var(txn.real_ip)]:-/%ci:%cp [%tr] %ft %b/%s"' 'log-format "%{+Q}[var(txn.real_ip)]:-/%ci:%cp [%tr] %ft %b/%s"'
] ]
@ -551,7 +551,7 @@ class PluginManager:
Args: Args:
plugins_dir: Directory containing plugin files (defaults to plugins_dir: Directory containing plugin files (defaults to
EASYHAPROXY_PLUGINS_DIR env var or /etc/haproxy/plugins) EASYHAPROXY_PLUGINS_DIR env var or /etc/easyhaproxy/plugins)
abort_on_error: If True, abort on plugin errors; if False, log and continue abort_on_error: If True, abort on plugin errors; if False, log and continue
""" """
@ -573,7 +573,7 @@ class PluginManager:
**Environment Variables:** **Environment Variables:**
- `EASYHAPROXY_PLUGINS_DIR` - Override plugin directory (default: `/etc/haproxy/plugins`) - `EASYHAPROXY_PLUGINS_DIR` - Override plugin directory (default: `/etc/easyhaproxy/plugins`)
**Note:** You typically don't interact with PluginManager directly when writing plugins. It's used by EasyHAProxy core. **Note:** You typically don't interact with PluginManager directly when writing plugins. It's used by EasyHAProxy core.
@ -712,7 +712,7 @@ The plugin creates:
Configuration: Configuration:
- enabled: Enable/disable the plugin (default: true) - enabled: Enable/disable the plugin (default: true)
- document_root: Document root path (default: /var/www/html) - document_root: Document root path (default: /etc/easyhaproxy/www)
- script_filename: Pattern for SCRIPT_FILENAME (default: %[path]) - script_filename: Pattern for SCRIPT_FILENAME (default: %[path])
- index_file: Default index file (default: index.php) - index_file: Default index file (default: index.php)
- path_info: Enable PATH_INFO support (default: true) - path_info: Enable PATH_INFO support (default: true)
@ -722,7 +722,7 @@ Example YAML config:
plugins: plugins:
fastcgi: fastcgi:
enabled: true enabled: true
document_root: /var/www/html document_root: /etc/easyhaproxy/www
index_file: index.php index_file: index.php
path_info: true path_info: true
@ -748,7 +748,7 @@ class FastcgiPlugin(PluginInterface):
def __init__(self): def __init__(self):
self.enabled = True self.enabled = True
self.document_root = "/var/www/html" self.document_root = "/etc/easyhaproxy/www"
self.script_filename = "%[path]" self.script_filename = "%[path]"
self.index_file = "index.php" self.index_file = "index.php"
self.path_info = True self.path_info = True
@ -885,7 +885,7 @@ Example YAML config:
algorithm: RS256 algorithm: RS256
issuer: https://myaccount.auth0.com/ issuer: https://myaccount.auth0.com/
audience: https://api.mywebsite.com audience: https://api.mywebsite.com
pubkey_path: /etc/haproxy/jwt_keys/pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/pubkey.pem
paths: paths:
- /api/admin - /api/admin
- /api/sensitive - /api/sensitive
@ -896,7 +896,7 @@ Example Container Label:
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/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
easyhaproxy.http.plugin.jwt_validator.only_paths: true easyhaproxy.http.plugin.jwt_validator.only_paths: true
""" """
@ -1267,7 +1267,7 @@ class CleanupPlugin(PluginInterface):
### Core Environment Variables ### Core Environment Variables
- `EASYHAPROXY_PLUGINS_DIR` - Override plugin directory (default: `/etc/haproxy/plugins`) - `EASYHAPROXY_PLUGINS_DIR` - Override plugin directory (default: `/etc/easyhaproxy/plugins`)
- `EASYHAPROXY_PLUGINS_ENABLED` - Comma-separated list of enabled plugins - `EASYHAPROXY_PLUGINS_ENABLED` - Comma-separated list of enabled plugins
- `EASYHAPROXY_PLUGINS_ABORT_ON_ERROR` - Abort on plugin errors (default: `false`) - `EASYHAPROXY_PLUGINS_ABORT_ON_ERROR` - Abort on plugin errors (default: `false`)
@ -1685,7 +1685,7 @@ services:
- "443:443" - "443:443"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./my_plugin.py:/etc/haproxy/plugins/my_plugin.py - ./my_plugin.py:/etc/easyhaproxy/plugins/my_plugin.py
environment: environment:
- EASYHAPROXY_DISCOVER=docker - EASYHAPROXY_DISCOVER=docker
``` ```
@ -1707,7 +1707,7 @@ Expected output:
**Verify generated configuration:** **Verify generated configuration:**
```bash ```bash
docker-compose exec haproxy cat /etc/haproxy/haproxy.cfg | grep -A 5 "My Plugin" docker-compose exec haproxy cat /etc/easyhaproxy/haproxy/haproxy.cfg | grep -A 5 "My Plugin"
``` ```
--- ---
@ -1722,13 +1722,13 @@ docker-compose exec haproxy cat /etc/haproxy/haproxy.cfg | grep -A 5 "My Plugin"
1. **File not in plugins directory** 1. **File not in plugins directory**
```bash ```bash
ls -la /etc/haproxy/plugins/ ls -la /etc/easyhaproxy/plugins/
# Ensure my_plugin.py exists # Ensure my_plugin.py exists
``` ```
2. **Invalid Python syntax** 2. **Invalid Python syntax**
```bash ```bash
python3 -m py_compile /etc/haproxy/plugins/my_plugin.py python3 -m py_compile /etc/easyhaproxy/plugins/my_plugin.py
# Check for syntax errors # Check for syntax errors
``` ```
@ -1814,7 +1814,7 @@ docker-compose exec haproxy cat /etc/haproxy/haproxy.cfg | grep -A 5 "My Plugin"
1. **Invalid HAProxy syntax in generated config** 1. **Invalid HAProxy syntax in generated config**
```bash ```bash
# Test configuration manually: # Test configuration manually:
haproxy -c -f /etc/haproxy/haproxy.cfg haproxy -c -f /etc/easyhaproxy/haproxy/haproxy.cfg
``` ```
2. **Missing quotes or escaping** 2. **Missing quotes or escaping**
@ -1907,7 +1907,7 @@ Share your plugin as a single `.py` file:
```bash ```bash
# Users copy the file to their plugins directory: # Users copy the file to their plugins directory:
cp my_plugin.py /etc/haproxy/plugins/ cp my_plugin.py /etc/easyhaproxy/plugins/
``` ```
**Advantages:** **Advantages:**
@ -1935,7 +1935,7 @@ my-easyhaproxy-plugin/
**Installation:** **Installation:**
```bash ```bash
# Users download and install: # Users download and install:
wget https://raw.githubusercontent.com/user/my-plugin/main/my_plugin.py -O /etc/haproxy/plugins/my_plugin.py wget https://raw.githubusercontent.com/user/my-plugin/main/my_plugin.py -O /etc/easyhaproxy/plugins/my_plugin.py
``` ```
#### Option 3: Docker Image with Plugin #### Option 3: Docker Image with Plugin
@ -1949,7 +1949,7 @@ FROM byjg/easy-haproxy:latest
COPY my_plugin.py /app/src/plugins/builtin/ COPY my_plugin.py /app/src/plugins/builtin/
# Optional: Add default configuration # Optional: Add default configuration
COPY plugin_config.yaml /etc/haproxy/static/config.yaml COPY plugin_config.yaml /etc/easyhaproxy/static/config.yaml
``` ```
**Build and distribute:** **Build and distribute:**
@ -1977,7 +1977,7 @@ Brief description of what your plugin does.
### Docker ### Docker
\`\`\`bash \`\`\`bash
wget https://example.com/my_plugin.py -O /etc/haproxy/plugins/my_plugin.py wget https://example.com/my_plugin.py -O /etc/easyhaproxy/plugins/my_plugin.py
\`\`\` \`\`\`
### Kubernetes ### Kubernetes

View file

@ -71,7 +71,7 @@ metadata:
easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.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.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users" easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
easyhaproxy.plugin.jwt_validator.only_paths: "false" easyhaproxy.plugin.jwt_validator.only_paths: "false"
# Configure deny_pages plugin # Configure deny_pages plugin
@ -124,7 +124,7 @@ services:
### 3. Static YAML Configuration ### 3. Static YAML Configuration
Configure plugins in `/etc/haproxy/static/config.yaml`: Configure plugins in `/etc/easyhaproxy/static/config.yaml`:
```yaml ```yaml
plugins: plugins:
@ -200,9 +200,9 @@ services:
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth0.myapp.com/ easyhaproxy.http.plugin.jwt_validator.issuer: https://auth0.myapp.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/easyhaproxy/jwt_keys/api_pubkey.pem
volumes: volumes:
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./auth_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
``` ```
**Protect only admin/sensitive endpoints:** **Protect only admin/sensitive endpoints:**
@ -213,11 +213,11 @@ services:
labels: labels:
easyhaproxy.http.host: api.example.com easyhaproxy.http.host: api.example.com
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/users,/api/billing easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/users,/api/billing
easyhaproxy.http.plugin.jwt_validator.only_paths: false easyhaproxy.http.plugin.jwt_validator.only_paths: false
volumes: volumes:
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./auth_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
# /api/health, /api/docs, etc. remain publicly accessible # /api/health, /api/docs, etc. remain publicly accessible
``` ```
@ -229,11 +229,11 @@ services:
labels: labels:
easyhaproxy.http.host: api.example.com easyhaproxy.http.host: api.example.com
easyhaproxy.http.plugins: jwt_validator easyhaproxy.http.plugins: jwt_validator
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.paths: /api/v1,/api/v2 easyhaproxy.http.plugin.jwt_validator.paths: /api/v1,/api/v2
easyhaproxy.http.plugin.jwt_validator.only_paths: true easyhaproxy.http.plugin.jwt_validator.only_paths: true
volumes: volumes:
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./auth_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
# All paths except /api/v1 and /api/v2 are denied # All paths except /api/v1 and /api/v2 are denied
``` ```
@ -287,7 +287,7 @@ labels:
Keep your system clean with automatic temp file removal: Keep your system clean with automatic temp file removal:
```yaml ```yaml
# /etc/haproxy/static/config.yaml # /etc/easyhaproxy/static/config.yaml
plugins: plugins:
enabled: [cleanup] enabled: [cleanup]
config: config:
@ -346,13 +346,13 @@ EASYHAPROXY_LOG_LEVEL=DEBUG
INFO: Loaded builtin plugin: cloudflare (domain) INFO: Loaded builtin plugin: cloudflare (domain)
INFO: Loaded builtin plugin: cleanup (global) INFO: Loaded builtin plugin: cleanup (global)
DEBUG: Executing domain plugin: cloudflare for domain: example.com DEBUG: Executing domain plugin: cloudflare for domain: example.com
DEBUG: Plugin cloudflare metadata: {'domain': 'example.com', 'ip_list_path': '/etc/haproxy/cloudflare_ips.lst'} DEBUG: Plugin cloudflare metadata: {'domain': 'example.com', 'ip_list_path': '/etc/easyhaproxy/cloudflare_ips.lst'}
``` ```
### Plugin Not Loading ### Plugin Not Loading
**Check:** **Check:**
1. Plugin file exists in `/etc/haproxy/plugins/` or builtin directory 1. Plugin file exists in `/etc/easyhaproxy/plugins/` or builtin directory
2. Python syntax is valid 2. Python syntax is valid
3. Plugin class inherits from `PluginInterface` 3. Plugin class inherits from `PluginInterface`
4. Check logs for load errors 4. Check logs for load errors

View file

@ -43,7 +43,7 @@ Add the Base64 string you generated before to the label `easyhaproxy.[definition
## Map the certificate as a docker volume ## Map the certificate as a docker volume
EasyHAProxy stores the certificates inside the container folder `/certs/haproxy`. EasyHAProxy stores the certificates inside the container folder `/etc/easyhaproxy/certs/haproxy`.
1. Run EasyHAProxy with the volume for the certificates: 1. Run EasyHAProxy with the volume for the certificates:
@ -52,7 +52,7 @@ docker volume create certs_haproxy
docker run \ docker run \
/* other parameters */ /* other parameters */
-v certs_haproxy:/certs/haproxy \ -v certs_haproxy:/etc/easyhaproxy/certs/haproxy \
-d byjg/easy-haproxy -d byjg/easy-haproxy
``` ```
@ -77,7 +77,7 @@ MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL
3. Copy this certificate to EasyHAProxy volume: 3. Copy this certificate to EasyHAProxy volume:
```bash title="Copy certificate to container" ```bash title="Copy certificate to container"
docker cp single.pem easyhaproxy:/certs/haproxy docker cp single.pem easyhaproxy:/etc/easyhaproxy/certs/haproxy
``` ```
---- ----

View file

@ -66,12 +66,12 @@ The `containers` format simplifies static configuration:
- **Clearer mapping**: Format mirrors internal Docker label structure - **Clearer mapping**: Format mirrors internal Docker label structure
::: :::
Then map this file to `/etc/haproxy/static/config.yml` in your EasyHAProxy container: Then map this file to `/etc/easyhaproxy/static/config.yml` in your EasyHAProxy container:
```bash title="Run EasyHAProxy with static configuration" ```bash title="Run EasyHAProxy with static configuration"
docker run -d \ docker run -d \
--name easy-haproxy-container \ --name easy-haproxy-container \
-v /my/static/:/etc/haproxy/static/ \ -v /my/static/:/etc/easyhaproxy/static/ \
-e EASYHAPROXY_DISCOVER="static" \ -e EASYHAPROXY_DISCOVER="static" \
# + Environment Variables \ # + Environment Variables \
-p 80:80 \ -p 80:80 \

View file

@ -8,15 +8,193 @@ sidebar_position: 13
These volumes allow you to persist certificates, provide custom configurations, and extend EasyHAProxy functionality. These volumes allow you to persist certificates, provide custom configurations, and extend EasyHAProxy functionality.
::: :::
You can map the following volumes: ## Directory Structure
| Volume | Description | :::info Base Path
|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------| All EasyHAProxy files are organized under `/etc/easyhaproxy/`. This can be customized using the `EASYHAPROXY_BASE_PATH` environment variable.
| /etc/haproxy/static/ | The folder that will contain the [config.yml](static.md) file for static configuration | :::
| /certs/haproxy/ | The folder that will contain the certificates (`PEM`) for the [SSL](ssl.md) |
| /certs/certbot/ | The folder that will contain the certificates (`PEM`) processed by Certbot (e.g. Let's Encrypt). More info: [acme](acme.md). | ```plaintext title="/etc/easyhaproxy/ Directory Tree"
| /etc/haproxy/conf.d/ | The folder that will contain the [custom configuration](other.md) files. | /etc/easyhaproxy/
| /etc/haproxy/errors-custom/ | The folder that will contain the [custom error](other.md) html files. | ├── static/ # 🔧 Runtime (static mode only)
│ └── config.yml # Static service configuration
├── haproxy/
│ ├── haproxy.cfg # 🔧 Runtime - Generated HAProxy config
│ ├── conf.d/ # 📦 Base image
│ │ ├── README.md
│ │ └── *.cfg # User-provided custom configs
│ ├── dhparam # 📦 Base image - DH params 2048-bit
│ ├── dhparam-1024 # 📦 Base image - DH params 1024-bit
│ └── errors-custom/ # 📦 Base image
│ ├── 400.http # Bad Request
│ ├── 403.http # Forbidden
│ ├── 408.http # Request Timeout
│ ├── 500.http # Internal Server Error
│ ├── 502.http # Bad Gateway
│ ├── 503.http # Service Unavailable
│ └── 504.http # Gateway Timeout
├── certs/
│ ├── live/ # 🔧 Runtime (Certbot)
│ │ └── {domain}/
│ │ ├── cert.pem # Certificate only
│ │ ├── chain.pem # Certificate chain
│ │ ├── fullchain.pem # cert.pem + chain.pem
│ │ ├── privkey.pem # Private key
│ │ └── README
│ ├── archive/ # 🔧 Runtime (Certbot)
│ │ └── {domain}/
│ │ ├── cert1.pem, cert2.pem... # Versioned certificates
│ │ └── privkey1.pem... # Versioned keys
│ ├── work/ # 🔧 Runtime (Certbot working dir)
│ ├── logs/ # 🔧 Runtime (Certbot logs)
│ │ └── letsencrypt.log
│ ├── certbot/ # 📦 Base image
│ │ ├── {domain}.pem # 🔧 Runtime - Merged cert+key
│ │ └── placeholder.pem # 📦 Base image - Placeholder cert
│ └── haproxy/ # 📦 Base image
│ ├── {domain}.pem # User-provided cert+key (PEM format)
│ └── placeholder.pem # 📦 Base image - Placeholder cert
├── plugins/ # Optional - Custom plugins
│ └── *.py # Python plugin files
├── jwt_keys/ # Optional - JWT validation
│ └── *.pem # RSA public keys
├── cloudflare_ips.lst # Optional - Cloudflare plugin
└── www/ # Optional - FastCGI document root
└── index.php
```
:::tip Legend
- **📦 Base image** - Included in the Docker image
- **🔧 Runtime** - Created/generated when EasyHAProxy runs
- **Optional** - Created only when specific features are used
:::
## Common Volume Mappings
The most commonly mapped volumes for persistence and customization:
| Volume | Purpose | Required |
|-------------------------------------------|-------------------------------------------------------------------------------------------------------|----------|
| `/etc/easyhaproxy/static/` | [Static configuration](static.md) - mount your `config.yml` here | Optional |
| `/etc/easyhaproxy/certs/haproxy/` | [SSL certificates](ssl.md) - user-provided certificates in PEM format | Optional |
| `/etc/easyhaproxy/certs/certbot/` | [ACME/Certbot certificates](acme.md) - auto-generated Let's Encrypt certificates | Optional |
| `/etc/easyhaproxy/certs/live/` | Certbot live certificates - persist across container restarts | Optional |
| `/etc/easyhaproxy/haproxy/conf.d/` | [Custom HAProxy config](other.md) - additional `.cfg` files to include | Optional |
| `/etc/easyhaproxy/haproxy/errors-custom/` | [Custom error pages](other.md) - custom HTTP error pages (400, 403, 500, etc.) | Optional |
| `/etc/easyhaproxy/plugins/` | [Custom plugins](plugins.md) - Python plugin files | Optional |
| `/etc/easyhaproxy/jwt_keys/` | [JWT public keys](Plugins/jwt-validator.md) - RSA public keys for JWT validation | Optional |
| `/etc/easyhaproxy/www/` | [FastCGI document root](Plugins/fastcgi.md) - PHP/FastCGI application files | Optional |
## Directory Details
### Configuration Files
#### Static Configuration
```bash
/etc/easyhaproxy/static/config.yml
```
Static service configuration when not using service discovery (Docker/Kubernetes).
:::note
This directory only exists when `EASYHAPROXY_DISCOVER=static` is set.
:::
#### HAProxy Configuration
```bash
/etc/easyhaproxy/haproxy/haproxy.cfg
```
Auto-generated HAProxy configuration file.
:::warning Do Not Edit
This file is automatically generated by EasyHAProxy. Any manual changes will be overwritten.
:::
#### Custom Configuration Snippets
```bash
/etc/easyhaproxy/haproxy/conf.d/*.cfg
```
Place custom HAProxy configuration snippets here. These files are automatically included in the main configuration.
:::tip Example
```bash
# Mount your custom config
docker run -v ./my-custom.cfg:/etc/easyhaproxy/haproxy/conf.d/my-custom.cfg byjg/easy-haproxy
```
:::
### SSL/TLS Certificates
#### User-Provided Certificates
```bash
/etc/easyhaproxy/certs/haproxy/{domain}.pem
```
Place your SSL certificates here in PEM format (certificate + private key combined).
:::info PEM Format
```bash
cat domain.crt domain.key > /etc/easyhaproxy/certs/haproxy/domain.com.pem
```
:::
#### ACME/Let's Encrypt Certificates
```bash
/etc/easyhaproxy/certs/certbot/{domain}.pem # Merged cert+key for HAProxy
/etc/easyhaproxy/certs/live/{domain}/ # Certbot live certificates (symlinks)
/etc/easyhaproxy/certs/archive/{domain}/ # Versioned certificate archive
```
EasyHAProxy automatically merges Certbot certificates from `/etc/easyhaproxy/certs/live/` into `/etc/easyhaproxy/certs/certbot/` for HAProxy consumption.
:::tip Persist Certbot Certificates
```yaml
volumes:
- certbot-certs:/etc/easyhaproxy/certs/live
- certbot-archive:/etc/easyhaproxy/certs/archive
```
:::
### Plugins & Extensions
#### Custom Plugins
```bash
/etc/easyhaproxy/plugins/*.py
```
Add custom Python plugins to extend EasyHAProxy functionality.
See [Plugin Development](plugin-development.md) for details.
#### JWT Public Keys
```bash
/etc/easyhaproxy/jwt_keys/*.pem
```
RSA public keys for [JWT token validation](Plugins/jwt-validator.md).
#### Cloudflare IP Ranges
```bash
/etc/easyhaproxy/cloudflare_ips.lst
```
Cloudflare IP ranges for the [Cloudflare plugin](Plugins/cloudflare.md) to restore real client IPs.
### Error Pages
```bash
/etc/easyhaproxy/haproxy/errors-custom/{code}.http
```
Custom HTTP error pages (400, 403, 408, 500, 502, 503, 504). Default error pages are included in the base image.
:::tip Customize Error Pages
```bash
# Mount your custom 503 error page
docker run -v ./custom-503.http:/etc/easyhaproxy/haproxy/errors-custom/503.http byjg/easy-haproxy
```
:::
---- ----
[Open source ByJG](http://opensource.byjg.com) [Open source ByJG](http://opensource.byjg.com)

View file

@ -67,6 +67,7 @@ markers = [
"cloudflare: marks tests for Cloudflare IP restoration plugin", "cloudflare: marks tests for Cloudflare IP restoration plugin",
"custom_label: marks tests for custom label prefix functionality", "custom_label: marks tests for custom label prefix functionality",
"static: marks tests for static configuration mode", "static: marks tests for static configuration mode",
"acme: marks tests for certbot/acme",
] ]
[tool.ruff] [tool.ruff]

View file

@ -271,12 +271,55 @@ class Functions:
return [-99, e] return [-99, e]
class classproperty:
"""Decorator for class-level properties."""
def __init__(self, func):
self.func = func
def __get__(self, obj, owner):
return self.func(owner)
class Consts: class Consts:
easyhaproxy_config = "/etc/haproxy/static/config.yml" """Configuration constants with dynamic path resolution based on EASYHAPROXY_BASE_PATH."""
haproxy_config = "/etc/haproxy/haproxy.cfg" _base_path = None
custom_config_folder = "/etc/haproxy/conf.d"
certs_certbot = "/certs/certbot" @classproperty
certs_haproxy = "/certs/haproxy" def base_path(cls):
"""Base directory for all EasyHAProxy files."""
if cls._base_path is None:
cls._base_path = os.getenv("EASYHAPROXY_BASE_PATH", "/etc/easyhaproxy")
return cls._base_path
@classmethod
def reset(cls):
"""Reset cached base path to pick up environment variable changes."""
cls._base_path = None
@classproperty
def easyhaproxy_config(cls):
"""Path to static configuration file."""
return f"{cls.base_path}/static/config.yml"
@classproperty
def haproxy_config(cls):
"""Path to generated HAProxy configuration file."""
return f"{cls.base_path}/haproxy/haproxy.cfg"
@classproperty
def custom_config_folder(cls):
"""Path to custom HAProxy config snippets directory."""
return f"{cls.base_path}/haproxy/conf.d"
@classproperty
def certs_certbot(cls):
"""Path to Certbot/ACME certificates directory."""
return f"{cls.base_path}/certs/certbot"
@classproperty
def certs_haproxy(cls):
"""Path to user-provided certificates directory."""
return f"{cls.base_path}/certs/haproxy"
class DaemonizeHAProxy: class DaemonizeHAProxy:
@ -304,12 +347,12 @@ class DaemonizeHAProxy:
custom_config_files = f"-f {self.custom_config_folder}" custom_config_files = f"-f {self.custom_config_folder}"
if action == DaemonizeHAProxy.HAPROXY_START or not os.path.exists(pid_file): if action == DaemonizeHAProxy.HAPROXY_START or not os.path.exists(pid_file):
return f"/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock" return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -S /var/run/haproxy.sock"
else: else:
return_code, output = Functions().run_bash(logger_haproxy, f"cat {pid_file}", log_output=False) return_code, output = Functions().run_bash(logger_haproxy, f"cat {pid_file}", log_output=False)
pid = "".join(output).rstrip() pid = "".join(output).rstrip()
if psutil.pid_exists(int(pid)): if psutil.pid_exists(int(pid)):
return f"/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}" return f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} {custom_config_files} -p {pid_file} -x /var/run/haproxy.sock -sf {pid}"
else: else:
os.unlink(pid_file) os.unlink(pid_file)
logger_haproxy.warning( logger_haproxy.warning(
@ -440,6 +483,9 @@ class Certbot:
renew_certs.append(host_arg) renew_certs.append(host_arg)
certbot_certonly = ('/usr/bin/certbot certonly {acme_server}' certbot_certonly = ('/usr/bin/certbot certonly {acme_server}'
' --config-dir {base_path}/certs'
' --work-dir {base_path}/certs/work'
' --logs-dir {base_path}/certs/logs'
' --preferred-challenges {challenge}' ' --preferred-challenges {challenge}'
' --agree-tos' ' --agree-tos'
' --issuance-timeout 90' ' --issuance-timeout 90'
@ -452,7 +498,8 @@ class Certbot:
certs=' '.join(request_certs), certs=' '.join(request_certs),
email=self.email, email=self.email,
challenge=self.certbot_preferred_challenges, challenge=self.certbot_preferred_challenges,
acme_server=self.acme_server) acme_server=self.acme_server,
base_path=Consts.base_path)
) )
if 'http' in self.certbot_preferred_challenges: if 'http' in self.certbot_preferred_challenges:
@ -476,7 +523,8 @@ class Certbot:
ret_reload = True ret_reload = True
if len(renew_certs) > 0: if len(renew_certs) > 0:
return_code_renew, output = Functions.run_bash(logger_certbot, "/usr/bin/certbot renew", return_result=False) certbot_renew = f"/usr/bin/certbot renew --config-dir {Consts.base_path}/certs --work-dir {Consts.base_path}/certs/work --logs-dir {Consts.base_path}/certs/logs"
return_code_renew, output = Functions.run_bash(logger_certbot, certbot_renew, return_result=False)
ret_reload = True ret_reload = True
if ret_reload: if ret_reload:
@ -497,7 +545,7 @@ class Certbot:
Functions.save(filename, cert + key) Functions.save(filename, cert + key)
def find_live_certificates(self): def find_live_certificates(self):
certbot_certs = "/etc/letsencrypt/live/" certbot_certs = f"{Consts.base_path}/certs/live/"
if not os.path.exists(certbot_certs): if not os.path.exists(certbot_certs):
return return
for item in os.listdir(certbot_certs): for item in os.listdir(certbot_certs):

View file

@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from functions import logger_easyhaproxy from functions import logger_easyhaproxy, Consts
class PluginType(Enum): class PluginType(Enum):
@ -108,12 +108,12 @@ class PluginManager:
Initialize the plugin manager Initialize the plugin manager
Args: Args:
plugins_dir: Directory containing plugin files (defaults to EASYHAPROXY_PLUGINS_DIR env var or /etc/haproxy/plugins) plugins_dir: Directory containing plugin files (defaults to EASYHAPROXY_PLUGINS_DIR env var or /etc/easyhaproxy/plugins)
abort_on_error: If True, abort on plugin errors; if False, log and continue abort_on_error: If True, abort on plugin errors; if False, log and continue
""" """
self.plugins_dir = plugins_dir or os.getenv( self.plugins_dir = plugins_dir or os.getenv(
"EASYHAPROXY_PLUGINS_DIR", "EASYHAPROXY_PLUGINS_DIR",
"/etc/haproxy/plugins" Consts.base_path + "/plugins"
) )
self.abort_on_error = abort_on_error self.abort_on_error = abort_on_error
self.plugins: dict[str, PluginInterface] = {} self.plugins: dict[str, PluginInterface] = {}

View file

@ -8,7 +8,7 @@ The plugin includes built-in Cloudflare IP ranges that are automatically
updated and written to the IP list file. updated and written to the IP list file.
Configuration: Configuration:
- ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/haproxy/cloudflare_ips.lst) - ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/easyhaproxy/cloudflare_ips.lst)
- ip_list: Base64-encoded list of IP ranges (one per line), takes precedence over ip_list_path - ip_list: Base64-encoded list of IP ranges (one per line), takes precedence over ip_list_path
- use_builtin_ips: Use built-in Cloudflare IP ranges (default: true) - use_builtin_ips: Use built-in Cloudflare IP ranges (default: true)
- update_log_format: Update HAProxy log format to show real visitor IP (default: true) - update_log_format: Update HAProxy log format to show real visitor IP (default: true)
@ -17,7 +17,7 @@ Example YAML config:
plugins: plugins:
cloudflare: cloudflare:
enabled: true enabled: true
ip_list_path: /etc/haproxy/cloudflare_ips.lst ip_list_path: /etc/easyhaproxy/cloudflare_ips.lst
use_builtin_ips: true use_builtin_ips: true
update_log_format: true update_log_format: true
@ -32,7 +32,7 @@ Example Container Label:
HAProxy Config Generated: HAProxy Config Generated:
# Cloudflare - Restore original visitor IP # Cloudflare - Restore original visitor IP
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst
http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP) if from_cloudflare http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP) if from_cloudflare
http-request set-header X-Forwarded-For %[var(txn.real_ip)] if from_cloudflare http-request set-header X-Forwarded-For %[var(txn.real_ip)] if from_cloudflare
@ -48,7 +48,7 @@ import sys
# Add parent directory to path for imports # Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from functions import logger_easyhaproxy from functions import logger_easyhaproxy, Consts
from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest
@ -85,7 +85,7 @@ class CloudflarePlugin(PluginInterface):
] ]
def __init__(self): def __init__(self):
self.ip_list_path = "/etc/haproxy/cloudflare_ips.lst" self.ip_list_path = Consts.base_path + "/cloudflare_ips.lst"
self.enabled = True self.enabled = True
self.use_builtin_ips = True self.use_builtin_ips = True
self.update_log_format = True self.update_log_format = True

View file

@ -10,7 +10,7 @@ The plugin creates:
Configuration: Configuration:
- enabled: Enable/disable the plugin (default: true) - enabled: Enable/disable the plugin (default: true)
- document_root: Document root path (default: /var/www/html) - document_root: Document root path (default: /etc/easyhaproxy/www)
- script_filename: Pattern for SCRIPT_FILENAME (default: %[path]) - script_filename: Pattern for SCRIPT_FILENAME (default: %[path])
- index_file: Default index file (default: index.php) - index_file: Default index file (default: index.php)
- path_info: Enable PATH_INFO support (default: true) - path_info: Enable PATH_INFO support (default: true)
@ -39,6 +39,8 @@ Example Kubernetes Annotation:
import os import os
import sys import sys
from functions import Consts
# Add parent directory to path for imports # Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -50,7 +52,7 @@ class FastcgiPlugin(PluginInterface):
def __init__(self): def __init__(self):
self.enabled = True self.enabled = True
self.document_root = "/var/www/html" self.document_root = Consts.base_path + "/www"
self.script_filename = "%[path]" self.script_filename = "%[path]"
self.index_file = "index.php" self.index_file = "index.php"
self.path_info = True self.path_info = True

View file

@ -47,7 +47,7 @@ Example YAML config:
algorithm: RS256 algorithm: RS256
issuer: https://myaccount.auth0.com/ issuer: https://myaccount.auth0.com/
audience: https://api.mywebsite.com audience: https://api.mywebsite.com
pubkey_path: /etc/haproxy/jwt_keys/pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/pubkey.pem
paths: paths:
- /api/admin - /api/admin
- /api/sensitive - /api/sensitive
@ -58,7 +58,7 @@ Example Container Label:
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/easyhaproxy/jwt_keys/api_pubkey.pem
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
easyhaproxy.http.plugin.jwt_validator.only_paths: true easyhaproxy.http.plugin.jwt_validator.only_paths: true
@ -86,7 +86,7 @@ HAProxy Config Generated:
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 '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 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 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 } 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 # Validate expiration
http-request set-var(txn.now) date() http-request set-var(txn.now) date()
@ -100,7 +100,7 @@ import sys
# Add parent directory to path for imports # Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from functions import Functions, logger_easyhaproxy from functions import Functions, logger_easyhaproxy, Consts
from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest from plugins import InitializationResult, PluginContext, PluginInterface, PluginResult, PluginType, ResourceRequest
@ -118,7 +118,7 @@ class JwtValidatorPlugin(PluginInterface):
self.only_paths = False # If true, only specified paths are accessible self.only_paths = False # If true, only specified paths are accessible
self.allow_anonymous = False # If true, allow requests without Authorization header self.allow_anonymous = False # If true, allow requests without Authorization header
# Make JWT_KEYS_DIR configurable via environment variable (for testing) # Make JWT_KEYS_DIR configurable via environment variable (for testing)
self.jwt_keys_dir = os.getenv("EASYHAPROXY_JWT_KEYS_DIR", "/etc/haproxy/jwt_keys") self.jwt_keys_dir = os.getenv("EASYHAPROXY_JWT_KEYS_DIR", Consts.base_path + "/jwt_keys")
@property @property
def name(self) -> str: def name(self) -> str:

View file

@ -1,5 +1,5 @@
{% if "ssl" in o %} {% if "ssl" in o %}
bind *:{{ o["port"] }} ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:{{ o["port"] }} ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
{% elif "h2" in o and o["h2"] %} {% elif "h2" in o and o["h2"] %}
bind *:{{ o["port"] }} proto h2 bind *:{{ o["port"] }} proto h2
option http-use-htx option http-use-htx

View file

@ -31,13 +31,13 @@ defaults
timeout client 10s timeout client 10s
timeout server 10m timeout server 10m
{% if data["customerrors"] %} {% if data["customerrors"] %}
errorfile 400 /etc/haproxy/errors-custom/400.http errorfile 400 /etc/easyhaproxy/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http errorfile 403 /etc/easyhaproxy/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http errorfile 408 /etc/easyhaproxy/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http errorfile 500 /etc/easyhaproxy/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http errorfile 502 /etc/easyhaproxy/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http errorfile 503 /etc/easyhaproxy/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http errorfile 504 /etc/easyhaproxy/haproxy/errors-custom/504.http
{% endif %} {% endif %}
{% if defaults_plugin_configs %} {% if defaults_plugin_configs %}

View file

@ -9,4 +9,4 @@
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam

View file

@ -6,5 +6,5 @@
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tls-tickets ssl-default-server-options no-sslv3 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam-1024 ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam-1024

43
tests/conftest.py Normal file
View file

@ -0,0 +1,43 @@
"""
Pytest configuration and fixtures for EasyHAProxy tests.
This module provides session-wide and function-level fixtures for testing.
"""
import os
import shutil
import tempfile
import pytest
# Create a session-wide temporary directory for all tests
# Use a different prefix to avoid conflicts with cleanup plugin (which looks for "easyhaproxy_*")
_test_session_dir = tempfile.mkdtemp(prefix="pytest_easyhaproxy_")
os.environ["EASYHAPROXY_BASE_PATH"] = _test_session_dir
@pytest.fixture(scope="function", autouse=True)
def reset_consts():
"""
Reset Consts before and after each test.
This ensures:
1. Each test picks up the EASYHAPROXY_BASE_PATH environment variable
2. Tests don't get permission errors trying to write to /etc/easyhaproxy/
3. Consts path cache is cleared between tests for isolation
"""
from functions import Consts
Consts.reset()
yield
Consts.reset()
def pytest_sessionfinish(session, exitstatus):
"""
Cleanup session temporary directory after all tests complete.
"""
try:
shutil.rmtree(_test_session_dir)
except Exception:
# Ignore cleanup errors
pass

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global
@ -38,7 +38,7 @@ backend srv_stats
server Local 127.0.0.1:1936 server Local 127.0.0.1:1936
frontend http_in_443 frontend http_in_443
bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
mode http mode http
acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global
@ -21,13 +21,13 @@ defaults
timeout connect 3s timeout connect 3s
timeout client 10s timeout client 10s
timeout server 10m timeout server 10m
errorfile 400 /etc/haproxy/errors-custom/400.http errorfile 400 /etc/easyhaproxy/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http errorfile 403 /etc/easyhaproxy/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http errorfile 408 /etc/easyhaproxy/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http errorfile 500 /etc/easyhaproxy/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http errorfile 502 /etc/easyhaproxy/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http errorfile 503 /etc/easyhaproxy/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http errorfile 504 /etc/easyhaproxy/haproxy/errors-custom/504.http
frontend stats frontend stats
@ -76,7 +76,7 @@ backend srv_test2_example_org_80
server srv-0 83d57d592e26:8080 check weight 1 server srv-0 83d57d592e26:8080 check weight 1
frontend http_in_443 frontend http_in_443
bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
mode http mode http
acl is_rule_test_example_org_443_1 hdr(host) -i test.example.org acl is_rule_test_example_org_443_1 hdr(host) -i test.example.org

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global
@ -21,13 +21,13 @@ defaults
timeout connect 3s timeout connect 3s
timeout client 10s timeout client 10s
timeout server 10m timeout server 10m
errorfile 400 /etc/haproxy/errors-custom/400.http errorfile 400 /etc/easyhaproxy/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http errorfile 403 /etc/easyhaproxy/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http errorfile 408 /etc/easyhaproxy/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http errorfile 500 /etc/easyhaproxy/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http errorfile 502 /etc/easyhaproxy/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http errorfile 503 /etc/easyhaproxy/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http errorfile 504 /etc/easyhaproxy/haproxy/errors-custom/504.http
frontend stats frontend stats

View file

@ -9,7 +9,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tls-tickets ssl-default-server-options no-sslv3 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam-1024 ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam-1024
defaults defaults
@ -50,7 +50,7 @@ backend srv_host1_local_80
server srv-0 5b69bc7fea1b:80 check weight 1 server srv-0 5b69bc7fea1b:80 check weight 1
frontend http_in_443 frontend http_in_443
bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
mode http mode http
acl is_rule_host2_local_443_1 hdr(host) -i host2.local acl is_rule_host2_local_443_1 hdr(host) -i host2.local

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global
@ -68,7 +68,7 @@ backend srv_node-exporter_quantum_example_org_31337
server srv-0 my-stack_node-exporter:9100 check weight 1 server srv-0 my-stack_node-exporter:9100 check weight 1
frontend http_in_443 frontend http_in_443
bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
mode http mode http
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com.br } redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com.br }
redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com } redirect prefix https://www.somehost.com.br code 301 if { hdr(host) -i somehost.com }

View file

@ -9,7 +9,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tls-tickets ssl-default-server-options no-sslv3 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam-1024 ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam-1024
defaults defaults

View file

@ -12,7 +12,7 @@ global
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam ssl-dh-param-file /etc/easyhaproxy/haproxy/dhparam
defaults defaults
log global log global
@ -21,13 +21,13 @@ defaults
timeout connect 3s timeout connect 3s
timeout client 10s timeout client 10s
timeout server 10m timeout server 10m
errorfile 400 /etc/haproxy/errors-custom/400.http errorfile 400 /etc/easyhaproxy/haproxy/errors-custom/400.http
errorfile 403 /etc/haproxy/errors-custom/403.http errorfile 403 /etc/easyhaproxy/haproxy/errors-custom/403.http
errorfile 408 /etc/haproxy/errors-custom/408.http errorfile 408 /etc/easyhaproxy/haproxy/errors-custom/408.http
errorfile 500 /etc/haproxy/errors-custom/500.http errorfile 500 /etc/easyhaproxy/haproxy/errors-custom/500.http
errorfile 502 /etc/haproxy/errors-custom/502.http errorfile 502 /etc/easyhaproxy/haproxy/errors-custom/502.http
errorfile 503 /etc/haproxy/errors-custom/503.http errorfile 503 /etc/easyhaproxy/haproxy/errors-custom/503.http
errorfile 504 /etc/haproxy/errors-custom/504.http errorfile 504 /etc/easyhaproxy/haproxy/errors-custom/504.http
frontend stats frontend stats
@ -46,7 +46,7 @@ backend srv_stats
server Local 127.0.0.1:1936 server Local 127.0.0.1:1936
frontend http_in_443 frontend http_in_443
bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 bind *:443 ssl crt /etc/easyhaproxy/certs/certbot/ alpn h2,http/1.1 crt /etc/easyhaproxy/certs/haproxy/ alpn h2,http/1.1
mode http mode http
acl is_rule_host1_com_br_443_1 hdr(host) -i host1.com.br acl is_rule_host1_com_br_443_1 hdr(host) -i host1.com.br

View file

@ -4,7 +4,6 @@
"easyhaproxy.http.port": "80", "easyhaproxy.http.port": "80",
"easyhaproxy.http.localport": "8080", "easyhaproxy.http.localport": "8080",
"easyhaproxy.http.plugins": "cloudflare,deny_pages", "easyhaproxy.http.plugins": "cloudflare,deny_pages",
"easyhaproxy.http.plugin.cloudflare.ip_list_path": "/etc/haproxy/cloudflare_ips.lst",
"easyhaproxy.http.plugin.deny_pages.paths": "/admin,/private", "easyhaproxy.http.plugin.deny_pages.paths": "/admin,/private",
"easyhaproxy.http.plugin.deny_pages.status_code": "403" "easyhaproxy.http.plugin.deny_pages.status_code": "403"
} }

View file

@ -1,4 +1,5 @@
import os import os
from functions import Consts
from functions import DaemonizeHAProxy from functions import DaemonizeHAProxy
@ -15,12 +16,12 @@ def test_daemonize_haproxy_check_config():
def test_daemonize_haproxy_get_haproxy_command_start(): def test_daemonize_haproxy_get_haproxy_command_start():
daemon = DaemonizeHAProxy() daemon = DaemonizeHAProxy()
command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START)
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock" assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock"
def test_daemonize_haproxy_get_haproxy_command_reload_nopid(): def test_daemonize_haproxy_get_haproxy_command_reload_nopid():
daemon = DaemonizeHAProxy() daemon = DaemonizeHAProxy()
command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD) command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD)
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock" assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /run/haproxy.pid -S /var/run/haproxy.sock"
def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid(): def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid():
daemon = DaemonizeHAProxy() daemon = DaemonizeHAProxy()
@ -28,7 +29,7 @@ def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid():
with open("/tmp/temp.pid", 'w') as file: with open("/tmp/temp.pid", 'w') as file:
file.write("-1001") file.write("-1001")
command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /tmp/temp.pid -S /var/run/haproxy.sock" assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /tmp/temp.pid -S /var/run/haproxy.sock"
finally: finally:
assert not os.path.exists("/tmp/temp.pid") assert not os.path.exists("/tmp/temp.pid")
@ -38,7 +39,7 @@ def test_daemonize_haproxy_get_haproxy_command_reload_existing_pin():
with open("/tmp/temp.pid", 'w') as file: with open("/tmp/temp.pid", 'w') as file:
file.write("1") file.write("1")
command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid")
assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /tmp/temp.pid -x /var/run/haproxy.sock -sf 1" assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -p /tmp/temp.pid -x /var/run/haproxy.sock -sf 1"
finally: finally:
assert os.path.exists("/tmp/temp.pid") assert os.path.exists("/tmp/temp.pid")
os.unlink("/tmp/temp.pid") os.unlink("/tmp/temp.pid")
@ -54,4 +55,4 @@ def test_daemonize_haproxy2_check_config():
def test_daemonize_haproxy2_get_haproxy_command_start(): def test_daemonize_haproxy2_get_haproxy_command_start():
daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures') daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures')
command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START)
assert command == f"/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -f {os.path.dirname(__file__)}/fixtures -p /run/haproxy.pid -S /var/run/haproxy.sock" assert command == f"/usr/sbin/haproxy -W -f {Consts.haproxy_config} -f {os.path.dirname(__file__)}/fixtures -p /run/haproxy.pid -S /var/run/haproxy.sock"

View file

@ -17,6 +17,7 @@ import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import easymapping import easymapping
from functions import Consts
from plugins import PluginContext, PluginManager from plugins import PluginContext, PluginManager
from plugins.builtin.cleanup import CleanupPlugin from plugins.builtin.cleanup import CleanupPlugin
from plugins.builtin.cloudflare import CloudflarePlugin from plugins.builtin.cloudflare import CloudflarePlugin
@ -43,7 +44,7 @@ class TestCloudflarePlugin:
assert plugin.name == "cloudflare" assert plugin.name == "cloudflare"
assert plugin.enabled is True assert plugin.enabled is True
assert plugin.use_builtin_ips is True assert plugin.use_builtin_ips is True
assert plugin.ip_list_path == "/etc/haproxy/cloudflare_ips.lst" assert plugin.ip_list_path == f"{Consts.base_path}/cloudflare_ips.lst"
assert len(plugin.CLOUDFLARE_IPS) == 22 # 15 IPv4 + 7 IPv6 assert len(plugin.CLOUDFLARE_IPS) == 22 # 15 IPv4 + 7 IPv6
def test_cloudflare_plugin_configuration(self): def test_cloudflare_plugin_configuration(self):
@ -85,11 +86,11 @@ class TestCloudflarePlugin:
assert result.haproxy_config is not None assert result.haproxy_config is not None
assert "Cloudflare" in result.haproxy_config assert "Cloudflare" in result.haproxy_config
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in result.haproxy_config assert f"acl from_cloudflare src -f {Consts.base_path}/cloudflare_ips.lst" in result.haproxy_config
assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP)" in result.haproxy_config assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP)" in result.haproxy_config
assert "http-request set-header X-Forwarded-For %[var(txn.real_ip)]" in result.haproxy_config assert "http-request set-header X-Forwarded-For %[var(txn.real_ip)]" in result.haproxy_config
assert result.metadata["domain"] == "example.com" assert result.metadata["domain"] == "example.com"
assert result.metadata["ip_list_path"] == "/etc/haproxy/cloudflare_ips.lst" assert result.metadata["ip_list_path"] == f"{Consts.base_path}/cloudflare_ips.lst"
def test_cloudflare_plugin_disabled(self): def test_cloudflare_plugin_disabled(self):
"""Test plugin returns empty config when disabled""" """Test plugin returns empty config when disabled"""
@ -123,7 +124,7 @@ class TestCloudflarePlugin:
# Verify Cloudflare config is in the output # Verify Cloudflare config is in the output
assert "Cloudflare - Restore original visitor IP" in haproxy_config assert "Cloudflare - Restore original visitor IP" in haproxy_config
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in haproxy_config assert f"acl from_cloudflare src -f {Consts.base_path}/cloudflare_ips.lst" in haproxy_config
assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP)" in haproxy_config assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP)" in haproxy_config
assert "http-request set-header X-Forwarded-For %[var(txn.real_ip)]" in haproxy_config assert "http-request set-header X-Forwarded-For %[var(txn.real_ip)]" in haproxy_config
# Verify log-format is in defaults section (from defaults_configs) # Verify log-format is in defaults section (from defaults_configs)
@ -688,7 +689,7 @@ class TestJwtValidatorPlugin:
"algorithm": "RS256", "algorithm": "RS256",
"issuer": "https://auth.example.com/", "issuer": "https://auth.example.com/",
"audience": "https://api.example.com", "audience": "https://api.example.com",
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem" "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem"
}) })
context = PluginContext( context = PluginContext(
@ -712,7 +713,7 @@ class TestJwtValidatorPlugin:
assert "var(txn.alg) -m str RS256" in result.haproxy_config assert "var(txn.alg) -m str RS256" in result.haproxy_config
assert "var(txn.iss) -m str https://auth.example.com/" in result.haproxy_config assert "var(txn.iss) -m str https://auth.example.com/" in result.haproxy_config
assert "var(txn.aud) -m str https://api.example.com" in result.haproxy_config assert "var(txn.aud) -m str https://api.example.com" in result.haproxy_config
assert 'jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem")' in result.haproxy_config assert f'jwt_verify(txn.alg,"{Consts.base_path}/jwt_keys/api_pubkey.pem")' in result.haproxy_config
assert "JWT has expired" in result.haproxy_config assert "JWT has expired" in result.haproxy_config
assert result.metadata["domain"] == "api.example.com" assert result.metadata["domain"] == "api.example.com"
assert result.metadata["algorithm"] == "RS256" assert result.metadata["algorithm"] == "RS256"
@ -742,7 +743,7 @@ class TestJwtValidatorPlugin:
assert result.haproxy_config is not None assert result.haproxy_config is not None
assert "JWT Validator" in result.haproxy_config assert "JWT Validator" in result.haproxy_config
assert "/etc/haproxy/jwt_keys/api_example_com_pubkey.pem" in result.haproxy_config assert f"{Consts.base_path}/jwt_keys/api_example_com_pubkey.pem" in result.haproxy_config
# Verify the decoded content is stored in metadata # Verify the decoded content is stored in metadata
assert result.metadata["pubkey_content"] == "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...\n-----END PUBLIC KEY-----" assert result.metadata["pubkey_content"] == "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh...\n-----END PUBLIC KEY-----"
@ -750,7 +751,7 @@ class TestJwtValidatorPlugin:
"""Test plugin skips issuer/audience validation when not configured""" """Test plugin skips issuer/audience validation when not configured"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem" "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem"
}) })
context = PluginContext( context = PluginContext(
@ -775,7 +776,7 @@ class TestJwtValidatorPlugin:
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"enabled": "false", "enabled": "false",
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem" "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem"
}) })
context = PluginContext( context = PluginContext(
@ -826,7 +827,7 @@ class TestJwtValidatorPlugin:
"""Test plugin with paths configured and only_paths=false""" """Test plugin with paths configured and only_paths=false"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem", "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem",
"paths": ["/api/admin", "/api/sensitive"], "paths": ["/api/admin", "/api/sensitive"],
"only_paths": "false" "only_paths": "false"
}) })
@ -860,7 +861,7 @@ class TestJwtValidatorPlugin:
"""Test plugin with paths configured and only_paths=true""" """Test plugin with paths configured and only_paths=true"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem", "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem",
"paths": ["/api/public"], "paths": ["/api/public"],
"only_paths": "true" "only_paths": "true"
}) })
@ -894,7 +895,7 @@ class TestJwtValidatorPlugin:
"""Test plugin parses comma-separated paths from container labels""" """Test plugin parses comma-separated paths from container labels"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem", "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem",
"paths": "/api/admin,/api/sensitive,/api/protected" "paths": "/api/admin,/api/sensitive,/api/protected"
}) })
@ -904,7 +905,7 @@ class TestJwtValidatorPlugin:
"""Test plugin parses paths from list (YAML config)""" """Test plugin parses paths from list (YAML config)"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem", "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem",
"paths": ["/api/admin", "/api/sensitive"] "paths": ["/api/admin", "/api/sensitive"]
}) })
@ -914,7 +915,7 @@ class TestJwtValidatorPlugin:
"""Test plugin protects all paths when paths is not configured""" """Test plugin protects all paths when paths is not configured"""
plugin = JwtValidatorPlugin() plugin = JwtValidatorPlugin()
plugin.configure({ plugin.configure({
"pubkey_path": "/etc/haproxy/jwt_keys/api_pubkey.pem" "pubkey_path": f"{Consts.base_path}/jwt_keys/api_pubkey.pem"
}) })
context = PluginContext( context = PluginContext(
@ -947,7 +948,7 @@ class TestFastcgiPlugin:
assert plugin.name == "fastcgi" assert plugin.name == "fastcgi"
assert plugin.enabled is True assert plugin.enabled is True
assert plugin.document_root == "/var/www/html" assert plugin.document_root == f"{Consts.base_path}/www"
assert plugin.index_file == "index.php" assert plugin.index_file == "index.php"
assert plugin.path_info is True assert plugin.path_info is True
assert plugin.custom_params == {} assert plugin.custom_params == {}
@ -969,7 +970,7 @@ class TestFastcgiPlugin:
"""Test plugin generates correct HAProxy config""" """Test plugin generates correct HAProxy config"""
plugin = FastcgiPlugin() plugin = FastcgiPlugin()
plugin.configure({ plugin.configure({
"document_root": "/var/www/html", "document_root": f"{Consts.base_path}/www",
"index_file": "index.php" "index_file": "index.php"
}) })
@ -991,9 +992,9 @@ class TestFastcgiPlugin:
assert len(result.global_configs) == 1 assert len(result.global_configs) == 1
fcgi_app_def = result.global_configs[0] fcgi_app_def = result.global_configs[0]
assert "fcgi-app fcgi_phpapp_local" in fcgi_app_def assert "fcgi-app fcgi_phpapp_local" in fcgi_app_def
assert "docroot /var/www/html" in fcgi_app_def assert f"docroot {Consts.base_path}/www" in fcgi_app_def
assert "index index.php" in fcgi_app_def assert "index index.php" in fcgi_app_def
assert result.metadata["document_root"] == "/var/www/html" assert result.metadata["document_root"] == f"{Consts.base_path}/www"
assert result.metadata["index_file"] == "index.php" assert result.metadata["index_file"] == "index.php"
def test_fastcgi_plugin_custom_params(self): def test_fastcgi_plugin_custom_params(self):

View file

@ -5,7 +5,7 @@
# WHAT THIS TESTS: # WHAT THIS TESTS:
# - HAProxy routing of /.well-known/acme-challenge/ to certbot backend # - HAProxy routing of /.well-known/acme-challenge/ to certbot backend
# - Certbot HTTP-01 challenge completion with Pebble ACME server # - Certbot HTTP-01 challenge completion with Pebble ACME server
# - Certificate issuance and storage in /certs/certbot/live/{domain}/ # - Certificate issuance and storage in /etc/easyhaproxy/certs/live/{domain}/
# - HTTPS serving with issued certificate # - HTTPS serving with issued certificate
# - Full end-to-end ACME protocol flow # - Full end-to-end ACME protocol flow
# #
@ -32,7 +32,7 @@
# docker compose -f docker-compose-acme-e2e.yml logs haproxy # docker compose -f docker-compose-acme-e2e.yml logs haproxy
# #
# # Verify certificate was issued # # Verify certificate was issued
# ls -la ../../certs/certbot/live/test.local/ # ls -la ../../certs/live/test.local/
# #
# # Test HTTPS (will show certificate warning - expected for test certs) # # Test HTTPS (will show certificate warning - expected for test certs)
# curl -k https://localhost/ -H "Host: test.local" # curl -k https://localhost/ -H "Host: test.local"
@ -104,7 +104,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Certificate storage (Docker volume for clean test isolation) # Certificate storage (Docker volume for clean test isolation)
- certbot-certs:/certs/certbot - certbot-certs:/etc/easyhaproxy/certs
# Pebble CA certificate (downloaded during test session) # Pebble CA certificate (downloaded during test session)
- ./pebble-ca.pem:/etc/ssl/certs/pebble-ca.pem:ro - ./pebble-ca.pem:/etc/ssl/certs/pebble-ca.pem:ro
ports: ports:

View file

@ -20,7 +20,7 @@
# # - Line 36: easyhaproxy.http.host to your real domain # # - Line 36: easyhaproxy.http.host to your real domain
# #
# # Create certs directory # # Create certs directory
# mkdir -p ./certs/certbot # mkdir -p ./certs
# ``` # ```
# #
# HOW TO START: # HOW TO START:
@ -43,7 +43,7 @@
# # Expected: Issuer: C = US, O = Let's Encrypt # # Expected: Issuer: C = US, O = Let's Encrypt
# #
# # Check certificate files # # Check certificate files
# ls -la ./certs/certbot/ # ls -la ./certs/
# # Expected: Your domain certificate files # # Expected: Your domain certificate files
# ``` # ```
# #
@ -53,7 +53,7 @@
# # Keep certificates: # # Keep certificates:
# # docker compose -f docker-compose-acme.yml down # # docker compose -f docker-compose-acme.yml down
# # Remove certificates too: # # Remove certificates too:
# # docker compose -f docker-compose-acme.yml down && rm -rf ./certs/certbot # # docker compose -f docker-compose-acme.yml down && rm -rf ./certs
# ``` # ```
# #
# ============================================================================== # ==============================================================================
@ -64,7 +64,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Persist the CERTBOT to avoid re-challenge when the server restarts # Persist the CERTBOT to avoid re-challenge when the server restarts
- ./certs/certbot:/certs/certbot - ./certs:/etc/easyhaproxy/certs
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"

View file

@ -53,7 +53,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Mount Cloudflare IP list # Mount Cloudflare IP list
- ./cloudflare_ips.lst:/etc/haproxy/cloudflare_ips.lst:ro - ./cloudflare_ips.lst:/etc/easyhaproxy/cloudflare_ips.lst:ro
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"
@ -77,4 +77,4 @@ services:
# Use custom IP list (disable built-in IPs) # Use custom IP list (disable built-in IPs)
easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false
easyhaproxy.http.plugin.cloudflare.ip_list_path: /etc/haproxy/cloudflare_ips.lst easyhaproxy.http.plugin.cloudflare.ip_list_path: /etc/easyhaproxy/cloudflare_ips.lst

View file

@ -57,7 +57,7 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Mount the public key for JWT verification # Mount the public key for JWT verification
- ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./jwt_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"
@ -85,4 +85,4 @@ services:
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/easyhaproxy/jwt_keys/api_pubkey.pem

View file

@ -68,8 +68,8 @@ services:
image: byjg/easy-haproxy:local image: byjg/easy-haproxy:local
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./cloudflare_ips.lst:/etc/haproxy/cloudflare_ips.lst:ro - ./cloudflare_ips.lst:/etc/easyhaproxy/cloudflare_ips.lst:ro
- ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ./jwt_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true" HAPROXY_CUSTOMERRORS: "true"
@ -114,7 +114,7 @@ services:
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/easyhaproxy/jwt_keys/api_pubkey.pem
# Block internal/debug endpoints # Block internal/debug endpoints
easyhaproxy.http.plugin.deny_pages.paths: /internal,/debug,/metrics easyhaproxy.http.plugin.deny_pages.paths: /internal,/debug,/metrics

View file

@ -62,8 +62,8 @@ services:
image: byjg/easy-haproxy:5.0.0 image: byjg/easy-haproxy:5.0.0
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- certs_certbot:/certs/certbot - certs_certbot:/etc/easyhaproxy/certs/certbot
# - certs_haproxy:/certs/haproxy # - certs_haproxy:/etc/easyhaproxy/certs/haproxy
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker

View file

@ -54,7 +54,7 @@ services:
image: byjg/easy-haproxy:local image: byjg/easy-haproxy:local
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./host2.local.pem:/certs/haproxy/host2.local.pem - ./host2.local.pem:/etc/easyhaproxy/certs/haproxy/host2.local.pem
environment: environment:
EASYHAPROXY_DISCOVER: docker EASYHAPROXY_DISCOVER: docker
EASYHAPROXY_SSL_MODE: "loose" EASYHAPROXY_SSL_MODE: "loose"

View file

@ -27,7 +27,7 @@
# # Edit your EasyHAProxy deployment and add: # # Edit your EasyHAProxy deployment and add:
# # volumeMounts: # # volumeMounts:
# # - name: cloudflare-ips # # - name: cloudflare-ips
# # mountPath: /etc/haproxy/cloudflare_ips.lst # # mountPath: /etc/easyhaproxy/cloudflare_ips.lst
# # subPath: cloudflare_ips.lst # # subPath: cloudflare_ips.lst
# # volumes: # # volumes:
# # - name: cloudflare-ips # # - name: cloudflare-ips
@ -121,7 +121,7 @@ metadata:
# easyhaproxy.plugin.cloudflare.ip_list: "MTAuMC4wLjAvOAoxNzIuMTYuMC4wLzEyCjE5Mi4xNjguMC4wLzE2CjEyNy4wLjAuMQ==" # easyhaproxy.plugin.cloudflare.ip_list: "MTAuMC4wLjAvOAoxNzIuMTYuMC4wLzEyCjE5Mi4xNjguMC4wLzE2CjEyNy4wLjAuMQ=="
# Optional: Specify custom IP list file path (only used if ip_list is not provided) # Optional: Specify custom IP list file path (only used if ip_list is not provided)
# easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst" # easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/easyhaproxy/cloudflare_ips.lst"
name: webapp-ingress-cloudflare name: webapp-ingress-cloudflare
namespace: default namespace: default
spec: spec:

View file

@ -6,7 +6,7 @@
# There are three ways to provide the JWT public key: # There are three ways to provide the JWT public key:
# #
# 1. pubkey_path - Mount a file and reference the path (requires ConfigMap or Volume) # 1. pubkey_path - Mount a file and reference the path (requires ConfigMap or Volume)
# easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem" # easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
# #
# 2. k8s_secret.pubkey - Reference a Kubernetes secret (RECOMMENDED) # 2. k8s_secret.pubkey - Reference a Kubernetes secret (RECOMMENDED)
# Auto-detect key (tries common variations): # Auto-detect key (tries common variations):
@ -141,7 +141,7 @@ metadata:
easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.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.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
name: api-ingress-jwt name: api-ingress-jwt
namespace: default namespace: default
spec: spec:

View file

@ -187,7 +187,7 @@ metadata:
easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.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.pubkey_path: "/etc/easyhaproxy/jwt_keys/api_pubkey.pem"
# Block internal paths # Block internal paths
easyhaproxy.plugin.deny_pages.paths: "/internal,/debug,/metrics" easyhaproxy.plugin.deny_pages.paths: "/internal,/debug,/metrics"
easyhaproxy.plugin.deny_pages.status_code: "403" easyhaproxy.plugin.deny_pages.status_code: "403"

View file

@ -18,7 +18,7 @@ Static mode uses explicit YAML configuration files instead of dynamic service di
## Configuration Files ## Configuration Files
All scenarios use `/etc/haproxy/static/config.yml` mounted from `./conf/config.yml`. All scenarios use `/etc/easyhaproxy/static/config.yml` mounted from `./conf/config.yml`.
Choose one of these pre-made configurations: Choose one of these pre-made configurations:

View file

@ -5,8 +5,8 @@
# #
# To use: # To use:
# 1. Update the container name and ports to match your setup # 1. Update the container name and ports to match your setup
# 2. Place SSL certificate at /certs/haproxy/host1.local.pem # 2. Place SSL certificate at /etc/easyhaproxy/certs/haproxy/host1.local.pem
# 3. Mount this config: -v ./conf/config-basic.yml:/etc/haproxy/static/config.yml # 3. Mount this config: -v ./conf/config-basic.yml:/etc/easyhaproxy/static/config.yml
stats: stats:
username: admin username: admin

View file

@ -15,14 +15,14 @@
# EASYHAPROXY_CERTBOT_EMAIL=your-email@example.com # EASYHAPROXY_CERTBOT_EMAIL=your-email@example.com
# #
# 4. Mount this config: # 4. Mount this config:
# -v ./conf/config-certbot.yml:/etc/haproxy/static/config.yml # -v ./conf/config-certbot.yml:/etc/easyhaproxy/static/config.yml
# #
# 5. Persist certificates: # 5. Persist certificates:
# -v ./certs/certbot:/certs/certbot # -v ./etc/easyhaproxy/certs/certbot:/etc/easyhaproxy/certs/certbot
# #
# How it works: # How it works:
# - EasyHAProxy requests certificates from Let's Encrypt via HTTP-01 challenge # - EasyHAProxy requests certificates from Let's Encrypt via HTTP-01 challenge
# - Certificates are stored in /certs/certbot/ # - Certificates are stored in /etc/easyhaproxy/certs/certbot/
# - Certificates auto-renew when needed # - Certificates auto-renew when needed
# #
# Note: Let's Encrypt has rate limits. Use staging environment for testing: # Note: Let's Encrypt has rate limits. Use staging environment for testing:
@ -56,7 +56,7 @@ containers:
# Domain without certbot (uses custom certificate) # Domain without certbot (uses custom certificate)
"custom.example.com:80": "custom.example.com:80":
ip: ["custom-app:8080"] ip: ["custom-app:8080"]
# No certbot - expects certificate at /certs/haproxy/custom.example.com.pem # No certbot - expects certificate at /etc/easyhaproxy/certs/haproxy/custom.example.com.pem
# HTTPS Port 443 # HTTPS Port 443
# Serves HTTPS traffic with auto-generated certificates # Serves HTTPS traffic with auto-generated certificates
@ -65,7 +65,7 @@ containers:
ip: ["webapp:8080"] ip: ["webapp:8080"]
ssl: true ssl: true
# Certificate path (auto-generated by certbot) # Certificate path (auto-generated by certbot)
# /certs/certbot/example.com/fullchain.pem # /etc/easyhaproxy/certs/certbot/example.com/fullchain.pem
"app.example.com:443": "app.example.com:443":
ip: ["app:3000"] ip: ["app:3000"]
@ -76,7 +76,7 @@ containers:
ip: ["custom-app:8080"] ip: ["custom-app:8080"]
ssl: true ssl: true
# Place your certificate at: # Place your certificate at:
# /certs/haproxy/custom.example.com.pem # /etc/easyhaproxy/certs/haproxy/custom.example.com.pem
# Multiple domains with different backends # Multiple domains with different backends
# Certbot will request separate certificates for each domain # Certbot will request separate certificates for each domain

View file

@ -6,7 +6,7 @@
# #
# To use: # To use:
# 1. Update container names and ports # 1. Update container names and ports
# 2. Mount this config: -v ./conf/config-deny-pages.yml:/etc/haproxy/static/config.yml # 2. Mount this config: -v ./conf/config-deny-pages.yml:/etc/easyhaproxy/static/config.yml
# 3. Test blocked paths: # 3. Test blocked paths:
# curl http://host1.local/admin # Should return 404 # curl http://host1.local/admin # Should return 404
# curl http://host2.local/wp-admin # Should return 403 (different config) # curl http://host2.local/wp-admin # Should return 403 (different config)

View file

@ -11,11 +11,11 @@
# openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem # openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
# #
# 2. Mount public keys: # 2. Mount public keys:
# -v ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro # -v ./jwt_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
# -v ./jwt_pubkey2.pem:/etc/haproxy/jwt_keys/admin_pubkey.pem:ro # -v ./jwt_pubkey2.pem:/etc/easyhaproxy/jwt_keys/admin_pubkey.pem:ro
# #
# 3. Mount this config: # 3. Mount this config:
# -v ./conf/config-jwt-validator.yml:/etc/haproxy/static/config.yml # -v ./conf/config-jwt-validator.yml:/etc/easyhaproxy/static/config.yml
# #
# 4. Test: # 4. Test:
# # Without token - should fail # # Without token - should fail
@ -42,7 +42,7 @@ containers:
algorithm: RS256 algorithm: RS256
issuer: https://auth.example.com/ issuer: https://auth.example.com/
audience: https://api.example.com audience: https://api.example.com
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
# Internal API - validate signature only (no issuer/audience check) # Internal API - validate signature only (no issuer/audience check)
"internal-api.local:80": "internal-api.local:80":
@ -52,7 +52,7 @@ containers:
jwt_validator: jwt_validator:
algorithm: RS256 algorithm: RS256
# No issuer/audience = skip those validations # No issuer/audience = skip those validations
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/api_pubkey.pem
# Admin API - different issuer and key # Admin API - different issuer and key
"admin-api.local:80": "admin-api.local:80":
@ -63,7 +63,7 @@ containers:
algorithm: RS256 algorithm: RS256
issuer: https://admin-auth.example.com/ issuer: https://admin-auth.example.com/
audience: https://admin.example.com audience: https://admin.example.com
pubkey_path: /etc/haproxy/jwt_keys/admin_pubkey.pem pubkey_path: /etc/easyhaproxy/jwt_keys/admin_pubkey.pem
deny_pages: deny_pages:
paths: paths:
- /internal - /internal

View file

@ -5,7 +5,7 @@
# WHAT THIS DEMONSTRATES: # WHAT THIS DEMONSTRATES:
# - EasyHAProxy using static YAML configuration (no service discovery) # - EasyHAProxy using static YAML configuration (no service discovery)
# - Useful for non-containerized backends, VMs, or bare metal servers # - Useful for non-containerized backends, VMs, or bare metal servers
# - Configuration via /etc/haproxy/static/config.yml # - Configuration via /etc/easyhaproxy/static/config.yml
# #
# REQUIREMENTS (run these first): # REQUIREMENTS (run these first):
# ```bash # ```bash
@ -65,10 +65,10 @@ services:
context: ../.. context: ../..
dockerfile: build/Dockerfile dockerfile: build/Dockerfile
volumes: volumes:
- ./conf/:/etc/haproxy/static/ - ./conf/:/etc/easyhaproxy/static/
- ../static/host1.local.pem:/certs/haproxy/host1.local.pem:ro - ../static/host1.local.pem:/etc/easyhaproxy/certs/haproxy/host1.local.pem:ro
- ../docker/jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro - ../docker/jwt_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
- ../docker/jwt_pubkey.pem:/etc/haproxy/jwt_keys/admin_pubkey.pem:ro - ../docker/jwt_pubkey.pem:/etc/easyhaproxy/jwt_keys/admin_pubkey.pem:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
EASYHAPROXY_DISCOVER: static EASYHAPROXY_DISCOVER: static

View file

@ -201,7 +201,7 @@ class TestBasicSSL:
def test_haproxy_config(self, docker_compose_basic_ssl): def test_haproxy_config(self, docker_compose_basic_ssl):
"""Test HAProxy configuration has SSL and redirect configurations""" """Test HAProxy configuration has SSL and redirect configurations"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -280,7 +280,7 @@ class TestJWTValidator:
def test_haproxy_config(self, docker_compose_jwt_validator): def test_haproxy_config(self, docker_compose_jwt_validator):
"""Test HAProxy configuration has JWT validator rules in the correct backend""" """Test HAProxy configuration has JWT validator rules in the correct backend"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -308,7 +308,7 @@ class TestJWTValidator:
assert "var(txn.aud) -m str https://api.example.com" in backend_block assert "var(txn.aud) -m str https://api.example.com" in backend_block
# Verify JWT signature verification # Verify JWT signature verification
assert 'jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem")' in backend_block assert 'jwt_verify(txn.alg,"/etc/easyhaproxy/jwt_keys/api_pubkey.pem")' in backend_block
# Verify expiration check # Verify expiration check
assert "JWT has expired" in backend_block assert "JWT has expired" in backend_block
@ -350,7 +350,7 @@ class TestMultiContainers:
def test_haproxy_config(self, docker_compose_multi_containers): def test_haproxy_config(self, docker_compose_multi_containers):
"""Test HAProxy configuration has multiple backend servers for load balancing""" """Test HAProxy configuration has multiple backend servers for load balancing"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -409,7 +409,7 @@ class TestPHPFPM:
def test_haproxy_config(self, docker_compose_php_fpm): def test_haproxy_config(self, docker_compose_php_fpm):
"""Test HAProxy configuration has FastCGI plugin configuration""" """Test HAProxy configuration has FastCGI plugin configuration"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -495,7 +495,7 @@ class TestPluginsCombined:
def test_haproxy_config(self, docker_compose_plugins_combined): def test_haproxy_config(self, docker_compose_plugins_combined):
"""Test HAProxy configuration has all plugin configurations in correct backends""" """Test HAProxy configuration has all plugin configurations in correct backends"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -506,7 +506,7 @@ class TestPluginsCombined:
website_block = extract_backend_block(config, "srv_website_local_80") website_block = extract_backend_block(config, "srv_website_local_80")
assert website_block, "Backend srv_website_local_80 not found" assert website_block, "Backend srv_website_local_80 not found"
assert "# Cloudflare - Restore original visitor IP" in website_block assert "# Cloudflare - Restore original visitor IP" in website_block
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in website_block assert "acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst" in website_block
assert "# Deny Pages - Block specific paths" in website_block assert "# Deny Pages - Block specific paths" in website_block
assert "acl denied_path path_beg /admin /wp-admin /wp-login.php /.env /config" in website_block assert "acl denied_path path_beg /admin /wp-admin /wp-login.php /.env /config" in website_block
assert "http-request deny deny_status 404 if denied_path" in website_block assert "http-request deny deny_status 404 if denied_path" in website_block
@ -604,7 +604,7 @@ class TestIPWhitelist:
def test_haproxy_config(self, docker_compose_ip_whitelist): def test_haproxy_config(self, docker_compose_ip_whitelist):
"""Test HAProxy configuration has IP whitelist rules in the correct backend""" """Test HAProxy configuration has IP whitelist rules in the correct backend"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -657,7 +657,7 @@ class TestCloudflare:
def test_haproxy_config(self, docker_compose_cloudflare): def test_haproxy_config(self, docker_compose_cloudflare):
"""Test HAProxy configuration has Cloudflare plugin rules in the correct backend""" """Test HAProxy configuration has Cloudflare plugin rules in the correct backend"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -672,7 +672,7 @@ class TestCloudflare:
assert "# Cloudflare - Restore original visitor IP" in backend_block assert "# Cloudflare - Restore original visitor IP" in backend_block
# Verify ACL for Cloudflare IPs # Verify ACL for Cloudflare IPs
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in backend_block assert "acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst" in backend_block
# Verify transaction variable for real IP # Verify transaction variable for real IP
assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP) if from_cloudflare" in backend_block assert "http-request set-var(txn.real_ip) req.hdr(CF-Connecting-IP) if from_cloudflare" in backend_block
@ -767,7 +767,7 @@ class TestChangedLabel:
def test_haproxy_config(self, docker_compose_changed_label): def test_haproxy_config(self, docker_compose_changed_label):
"""Test HAProxy configuration with custom label prefix""" """Test HAProxy configuration with custom label prefix"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -878,7 +878,7 @@ class TestACME:
def test_haproxy_config(self, docker_compose_acme): def test_haproxy_config(self, docker_compose_acme):
"""Test HAProxy configuration has ACME challenge routing""" """Test HAProxy configuration has ACME challenge routing"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "docker-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -952,8 +952,8 @@ class TestACME:
f"HAProxy cannot connect to Pebble ACME server. Check docker network.\nLogs:\n{logs[-2000:]}" f"HAProxy cannot connect to Pebble ACME server. Check docker network.\nLogs:\n{logs[-2000:]}"
# Verify merged certificate file exists # Verify merged certificate file exists
# EasyHAProxy merges cert+key from /etc/letsencrypt/live/ to /certs/certbot/{domain}.pem # EasyHAProxy merges cert+key from /etc/easyhaproxy/certs/live/ to /etc/easyhaproxy/certs/certbot/{domain}.pem
merged_cert_path = "/certs/certbot/test.local.pem" merged_cert_path = "/etc/easyhaproxy/certs/certbot/test.local.pem"
result = subprocess.run( result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "test", "-f", merged_cert_path], ["docker", "exec", "docker-haproxy-1", "test", "-f", merged_cert_path],
capture_output=True capture_output=True

View file

@ -1145,7 +1145,7 @@ class TestIPWhitelist:
# Get HAProxy configuration # Get HAProxy configuration
result = subprocess.run( result = subprocess.run(
[kubectl, "exec", "-n", "easyhaproxy", pod_name, [kubectl, "exec", "-n", "easyhaproxy", pod_name,
"--", "cat", "/etc/haproxy/haproxy.cfg"], "--", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
check=True, check=True,
capture_output=True, capture_output=True,
text=True text=True
@ -1292,7 +1292,7 @@ class TestJWTValidatorSecret:
# Get HAProxy configuration # Get HAProxy configuration
result = subprocess.run( result = subprocess.run(
[kubectl, "exec", "-n", "easyhaproxy", pod_name, [kubectl, "exec", "-n", "easyhaproxy", pod_name,
"--", "cat", "/etc/haproxy/haproxy.cfg"], "--", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
check=True, check=True,
capture_output=True, capture_output=True,
text=True text=True
@ -1327,7 +1327,7 @@ class TestJWTValidatorSecret:
"JWT audience validation not found in API backend" "JWT audience validation not found in API backend"
# Verify JWT keys directory is used in this backend # Verify JWT keys directory is used in this backend
assert "/etc/haproxy/jwt_keys/" in backend_block, \ assert "/etc/easyhaproxy/jwt_keys/" in backend_block, \
"JWT keys directory not found in API backend" "JWT keys directory not found in API backend"
def test_access_without_token_denied(self, k8s_jwt_validator_secret): def test_access_without_token_denied(self, k8s_jwt_validator_secret):
@ -1589,7 +1589,7 @@ class TestCloudflare:
# Get HAProxy configuration # Get HAProxy configuration
result = subprocess.run( result = subprocess.run(
[kubectl, "exec", "-n", "easyhaproxy", pod_name, [kubectl, "exec", "-n", "easyhaproxy", pod_name,
"--", "cat", "/etc/haproxy/haproxy.cfg"], "--", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
check=True, check=True,
capture_output=True, capture_output=True,
text=True text=True
@ -1606,7 +1606,7 @@ class TestCloudflare:
"Cloudflare plugin comment not found in myapp backend" "Cloudflare plugin comment not found in myapp backend"
# Verify ACL for Cloudflare IPs is in this backend # Verify ACL for Cloudflare IPs is in this backend
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in backend_block, \ assert "acl from_cloudflare src -f /etc/easyhaproxy/cloudflare_ips.lst" in backend_block, \
"Cloudflare IP ACL not found in myapp backend" "Cloudflare IP ACL not found in myapp backend"
# Verify real IP extraction from CF-Connecting-IP header is in this backend # Verify real IP extraction from CF-Connecting-IP header is in this backend
@ -1639,7 +1639,7 @@ class TestCloudflare:
# Read the Cloudflare IP list file # Read the Cloudflare IP list file
result = subprocess.run( result = subprocess.run(
[kubectl, "exec", "-n", "easyhaproxy", pod_name, [kubectl, "exec", "-n", "easyhaproxy", pod_name,
"--", "cat", "/etc/haproxy/cloudflare_ips.lst"], "--", "cat", "/etc/easyhaproxy/cloudflare_ips.lst"],
check=True, check=True,
capture_output=True, capture_output=True,
text=True text=True

View file

@ -101,7 +101,7 @@ class TestStaticBasic:
def test_haproxy_config(self, static_basic): def test_haproxy_config(self, static_basic):
"""Test HAProxy configuration has SSL and redirect configurations""" """Test HAProxy configuration has SSL and redirect configurations"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "static-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -155,7 +155,7 @@ class TestStaticDenyPages:
def test_haproxy_config(self, static_deny_pages): def test_haproxy_config(self, static_deny_pages):
"""Test HAProxy configuration has deny pages rules""" """Test HAProxy configuration has deny pages rules"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "static-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True
@ -209,7 +209,7 @@ class TestStaticJWTValidator:
def test_haproxy_config(self, static_jwt_validator): def test_haproxy_config(self, static_jwt_validator):
"""Test HAProxy configuration has JWT validation rules""" """Test HAProxy configuration has JWT validation rules"""
result = subprocess.run( result = subprocess.run(
["docker", "exec", "static-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"], ["docker", "exec", "static-haproxy-1", "cat", "/etc/easyhaproxy/haproxy/haproxy.cfg"],
capture_output=True, capture_output=True,
text=True, text=True,
check=True check=True

View file

@ -10,6 +10,7 @@ import subprocess
import tempfile import tempfile
import time import time
from pathlib import Path from pathlib import Path
import requests
import jwt as jwt_lib import jwt as jwt_lib
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -21,9 +22,12 @@ _docker_image_built = False
class DockerComposeFixture: class DockerComposeFixture:
"""Helper class to manage docker-compose lifecycle""" """Helper class to manage docker-compose lifecycle"""
def __init__(self, compose_file: str, startup_wait: int = 3, build: bool = None): def __init__(self, compose_file: str, startup_wait: int = 3, build: bool = None,
health_check: callable = None, health_check_timeout: int = 60):
self.compose_file = compose_file self.compose_file = compose_file
self.startup_wait = startup_wait self.startup_wait = startup_wait
self.health_check = health_check
self.health_check_timeout = health_check_timeout
# Smart build strategy: build on first call, skip on subsequent calls # Smart build strategy: build on first call, skip on subsequent calls
global _docker_image_built global _docker_image_built
@ -60,6 +64,28 @@ class DockerComposeFixture:
if self.build: if self.build:
_docker_image_built = True _docker_image_built = True
# Use health check if provided, otherwise fall back to fixed delay
if self.health_check:
print(f" ✓ Services started, waiting for health check (timeout: {self.health_check_timeout}s)...")
start_time = time.time()
poll_interval = 1
while time.time() - start_time < self.health_check_timeout:
try:
if self.health_check():
elapsed = time.time() - start_time
print(f" ✓ Services ready (health check passed in {elapsed:.1f}s)")
return
except Exception:
# Health check not ready yet, continue polling
pass
time.sleep(poll_interval)
# Health check timed out
elapsed = time.time() - start_time
raise TimeoutError(f"Health check did not pass within {elapsed:.1f}s")
else:
print(f" ✓ Services started, waiting {self.startup_wait}s for initialization...") print(f" ✓ Services started, waiting {self.startup_wait}s for initialization...")
time.sleep(self.startup_wait) time.sleep(self.startup_wait)
print(f" ✓ Services ready") print(f" ✓ Services ready")