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:
parent
3e963228f3
commit
045dd3817e
73 changed files with 600 additions and 287 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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="""" />
|
<option name="_new_keywords" value="""" />
|
||||||
<option name="_new_parameters" value="""" />
|
<option name="_new_parameters" value="""" />
|
||||||
<option name="_new_additionalArguments" value="""" />
|
<option name="_new_additionalArguments" value="""" />
|
||||||
<option name="_new_target" value=""$PROJECT_DIR$/src/tests"" />
|
<option name="_new_target" value=""$PROJECT_DIR$/tests"" />
|
||||||
<option name="_new_targetType" value=""PATH"" />
|
<option name="_new_targetType" value=""PATH"" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
5
build/assets/etc/easyhaproxy/haproxy/conf.d/README.md
Normal file
5
build/assets/etc/easyhaproxy/haproxy/conf.d/README.md
Normal 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.
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Custom HAProxy
|
|
||||||
|
|
||||||
Put files .cfg to be included in the configuration
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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)(/.*)?$
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
:::
|
:::
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
194
docs/volumes.md
194
docs/volumes.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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] = {}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
43
tests/conftest.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue