Add IP Whitelist Plugin and corresponding tests
- Introduced `IpWhitelistPlugin` (DOMAIN): Restricts access to specific IP addresses or CIDR ranges. - Added configuration options: `enabled`, `allowed_ips`, and `status_code`. - Updated `plugins.md` and `plugin-development.md` to document `IpWhitelistPlugin`. - Created fixtures for `services-with-ip-whitelist`. - Added extensive test cases for `IpWhitelistPlugin` to validate configuration and HAProxy output generation. - Ensured seamless integration into the plugin framework alongside other domain plugins.
This commit is contained in:
parent
91f8ff5d08
commit
57387f3e32
5 changed files with 1549 additions and 428 deletions
File diff suppressed because it is too large
Load diff
534
docs/plugins.md
534
docs/plugins.md
|
|
@ -2,42 +2,57 @@
|
||||||
sidebar_position: 16
|
sidebar_position: 16
|
||||||
---
|
---
|
||||||
|
|
||||||
# Plugins
|
# Using Plugins
|
||||||
|
|
||||||
EasyHAProxy supports a flexible plugin system that allows you to extend HAProxy configuration with custom functionality. Plugins are automatically invoked during the discovery cycle and can inject HAProxy configuration directives or modify discovery data.
|
EasyHAProxy supports a plugin system that extends HAProxy configuration with custom functionality. This guide explains how to use and configure plugins.
|
||||||
|
|
||||||
|
## What are Plugins?
|
||||||
|
|
||||||
|
Plugins automatically run during the discovery cycle and can:
|
||||||
|
- Add HAProxy configuration directives
|
||||||
|
- Perform maintenance tasks
|
||||||
|
- Modify discovery data
|
||||||
|
- Integrate with external services
|
||||||
|
|
||||||
## Plugin Types
|
## Plugin Types
|
||||||
|
|
||||||
### Global Plugins
|
### Global Plugins
|
||||||
Global plugins execute **once per discovery cycle**, regardless of how many domains are discovered. They're ideal for:
|
|
||||||
|
Execute **once per discovery cycle** regardless of how many domains are discovered.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
- Cleanup tasks
|
- Cleanup tasks
|
||||||
- Global monitoring
|
- Global monitoring
|
||||||
- DNS updates
|
- DNS updates
|
||||||
- Log management
|
- Log management
|
||||||
|
|
||||||
|
**Example:** `cleanup` plugin
|
||||||
|
|
||||||
### Domain Plugins
|
### Domain Plugins
|
||||||
Domain plugins execute **once for each discovered domain/host**. They're ideal for:
|
|
||||||
|
Execute **once for each discovered domain/host**.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
- Domain-specific configuration
|
- Domain-specific configuration
|
||||||
- IP restoration (e.g., Cloudflare)
|
- IP restoration (Cloudflare)
|
||||||
- Path blocking
|
- Path blocking
|
||||||
- Custom headers per domain
|
- Custom headers per domain
|
||||||
|
|
||||||
|
**Examples:** `cloudflare`, `deny_pages`
|
||||||
|
|
||||||
## Built-in Plugins
|
## Built-in Plugins
|
||||||
|
|
||||||
### Cloudflare (Domain Plugin)
|
### Cloudflare Plugin (Domain)
|
||||||
|
|
||||||
Restores the original visitor IP address from Cloudflare's `CF-Connecting-IP` header when requests come through Cloudflare's CDN.
|
Restores the original visitor IP address when requests come through Cloudflare's CDN.
|
||||||
|
|
||||||
**Configuration:**
|
**Why use it:** Cloudflare replaces the visitor's IP with its own. This plugin restores the original IP from the `CF-Connecting-IP` header.
|
||||||
```yaml
|
|
||||||
# /etc/haproxy/static/config.yaml
|
|
||||||
plugins:
|
|
||||||
cloudflare:
|
|
||||||
enabled: true
|
|
||||||
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container Label:**
|
**Configuration options:**
|
||||||
|
- `enabled` - Enable/disable plugin (default: `true`)
|
||||||
|
- `ip_list_path` - Path to Cloudflare IP list (default: `/etc/haproxy/cloudflare_ips.lst`)
|
||||||
|
|
||||||
|
**Enable via container label:**
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
myapp:
|
myapp:
|
||||||
|
|
@ -46,101 +61,117 @@ services:
|
||||||
easyhaproxy.http.plugins: cloudflare
|
easyhaproxy.http.plugins: cloudflare
|
||||||
```
|
```
|
||||||
|
|
||||||
**Environment Variable:**
|
**Custom IP list path:**
|
||||||
```bash
|
```yaml
|
||||||
EASYHAPROXY_PLUGINS_ENABLED=cloudflare
|
labels:
|
||||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH=/etc/haproxy/cloudflare_ips.lst
|
easyhaproxy.http.plugins: cloudflare
|
||||||
|
easyhaproxy.http.plugin.cloudflare.ip_list_path: /custom/path/cf_ips.lst
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cleanup (Global Plugin)
|
**HAProxy config generated:**
|
||||||
|
```
|
||||||
|
# Cloudflare - Restore original visitor IP
|
||||||
|
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
|
||||||
|
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required:** Download Cloudflare IP list from [Cloudflare documentation](https://support.cloudflare.com/hc/en-us/articles/200170786).
|
||||||
|
|
||||||
|
### Cleanup Plugin (Global)
|
||||||
|
|
||||||
Performs cleanup tasks during each discovery cycle, such as removing old temporary files.
|
Performs cleanup tasks during each discovery cycle, such as removing old temporary files.
|
||||||
|
|
||||||
**Configuration:**
|
**Why use it:** Prevents disk space issues by automatically cleaning up temporary files created by EasyHAProxy.
|
||||||
|
|
||||||
|
**Configuration options:**
|
||||||
|
- `enabled` - Enable/disable plugin (default: `true`)
|
||||||
|
- `max_idle_time` - Maximum age in seconds before deleting files (default: `300`)
|
||||||
|
- `cleanup_temp_files` - Enable temp file cleanup (default: `true`)
|
||||||
|
|
||||||
|
**Enable via YAML:**
|
||||||
```yaml
|
```yaml
|
||||||
# /etc/haproxy/static/config.yaml
|
# /etc/haproxy/static/config.yaml
|
||||||
plugins:
|
plugins:
|
||||||
cleanup:
|
enabled: [cleanup]
|
||||||
enabled: true
|
config:
|
||||||
max_idle_time: 300 # seconds
|
cleanup:
|
||||||
cleanup_temp_files: true
|
max_idle_time: 600
|
||||||
|
cleanup_temp_files: true
|
||||||
```
|
```
|
||||||
|
|
||||||
**Environment Variable:**
|
**Enable via environment variable:**
|
||||||
```bash
|
```bash
|
||||||
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
||||||
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deny Pages (Domain Plugin)
|
### Deny Pages Plugin (Domain)
|
||||||
|
|
||||||
Blocks access to specific paths for a domain.
|
Blocks access to specific paths for a domain, returning a configurable HTTP status code.
|
||||||
|
|
||||||
**Configuration:**
|
**Why use it:** Protect admin panels, internal APIs, or debugging endpoints from public access.
|
||||||
```yaml
|
|
||||||
# /etc/haproxy/static/config.yaml
|
|
||||||
plugins:
|
|
||||||
deny_pages:
|
|
||||||
enabled: true
|
|
||||||
paths: /admin,/private,/internal
|
|
||||||
status_code: 403
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container Label:**
|
**Configuration options:**
|
||||||
|
- `enabled` - Enable/disable plugin (default: `true`)
|
||||||
|
- `paths` - Comma-separated list of paths to block (e.g., `/admin,/private`)
|
||||||
|
- `status_code` - HTTP status code to return (default: `403`)
|
||||||
|
|
||||||
|
**Enable via container label:**
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
myapp:
|
webapp:
|
||||||
labels:
|
labels:
|
||||||
easyhaproxy.http.host: example.com
|
easyhaproxy.http.host: example.com
|
||||||
easyhaproxy.http.plugins: deny_pages
|
easyhaproxy.http.plugins: deny_pages
|
||||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/private
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/private,/debug
|
||||||
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
easyhaproxy.http.plugin.deny_pages.status_code: 404
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**HAProxy config generated:**
|
||||||
|
```
|
||||||
|
# Deny Pages - Block specific paths
|
||||||
|
acl denied_path path_beg /admin /private /debug
|
||||||
|
http-request deny deny_status 404 if denied_path
|
||||||
|
```
|
||||||
|
|
||||||
|
### IP Whitelist Plugin (Domain)
|
||||||
|
|
||||||
|
Restricts access to a domain to only specific IP addresses or CIDR ranges.
|
||||||
|
|
||||||
|
**Why use it:** Restrict access to internal tools, admin panels, or staging environments to only trusted IP addresses.
|
||||||
|
|
||||||
|
**Configuration options:**
|
||||||
|
- `enabled` - Enable/disable plugin (default: `true`)
|
||||||
|
- `allowed_ips` - Comma-separated list of IPs/CIDR ranges to allow (e.g., `192.168.1.0/24,10.0.0.1`)
|
||||||
|
- `status_code` - HTTP status code to return for blocked IPs (default: `403`)
|
||||||
|
|
||||||
|
**Enable via container label:**
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
admin:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: admin.example.com
|
||||||
|
easyhaproxy.http.plugins: ip_whitelist
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 192.168.1.0/24,10.0.0.5
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
**HAProxy config generated:**
|
||||||
|
```
|
||||||
|
# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src 192.168.1.0/24 10.0.0.5
|
||||||
|
http-request deny deny_status 403 if !whitelisted_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** This blocks ALL IPs except those in the whitelist. Make sure to include your own IP!
|
||||||
|
|
||||||
## Configuration Methods
|
## Configuration Methods
|
||||||
|
|
||||||
Plugins can be configured using three methods. Configuration from YAML takes precedence over environment variables.
|
Plugins can be configured using three methods, listed in order of precedence (highest to lowest):
|
||||||
|
|
||||||
### 1. Static YAML Configuration
|
### 1. Container Labels (Domain Plugins Only)
|
||||||
|
|
||||||
Configure plugins in `/etc/haproxy/static/config.yaml`:
|
Enable and configure domain plugins for specific containers:
|
||||||
|
|
||||||
```yaml
|
|
||||||
plugins:
|
|
||||||
# Global settings
|
|
||||||
abort_on_error: false # Log and continue on plugin errors (default)
|
|
||||||
|
|
||||||
# Plugin-specific configuration
|
|
||||||
cloudflare:
|
|
||||||
enabled: true
|
|
||||||
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
enabled: true
|
|
||||||
max_idle_time: 300
|
|
||||||
|
|
||||||
deny_pages:
|
|
||||||
enabled: false # Disable globally, can still be enabled per domain
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Environment Variables
|
|
||||||
|
|
||||||
Configure plugins via environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Global settings
|
|
||||||
EASYHAPROXY_PLUGINS_ABORT_ON_ERROR=false
|
|
||||||
EASYHAPROXY_PLUGINS_ENABLED=cloudflare,cleanup
|
|
||||||
|
|
||||||
# Plugin-specific configuration
|
|
||||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED=true
|
|
||||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH=/etc/haproxy/cloudflare_ips.lst
|
|
||||||
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Container Labels (Domain Plugins Only)
|
|
||||||
|
|
||||||
Enable domain plugins for specific containers:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
|
|
@ -148,194 +179,239 @@ services:
|
||||||
image: myapp:latest
|
image: myapp:latest
|
||||||
labels:
|
labels:
|
||||||
easyhaproxy.http.host: example.com
|
easyhaproxy.http.host: example.com
|
||||||
|
easyhaproxy.http.port: 80
|
||||||
|
# Enable multiple plugins
|
||||||
easyhaproxy.http.plugins: cloudflare,deny_pages
|
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||||
|
# Configure deny_pages plugin
|
||||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/api/internal
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/api/internal
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
**Label format:**
|
||||||
|
- Enable plugins: `easyhaproxy.<definition>.plugins: plugin1,plugin2`
|
||||||
|
- Configure plugin: `easyhaproxy.<definition>.plugin.<plugin_name>.<config_key>: value`
|
||||||
|
|
||||||
|
**Where `<definition>` is:** `http`, `https`, `tcp`, etc.
|
||||||
|
|
||||||
|
### 2. Static YAML Configuration
|
||||||
|
|
||||||
|
Configure plugins globally in `/etc/haproxy/static/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
# Global settings
|
||||||
|
abort_on_error: false # Log and continue on errors (recommended)
|
||||||
|
|
||||||
|
# Enable global plugins
|
||||||
|
enabled: [cleanup]
|
||||||
|
|
||||||
|
# Configure individual plugins
|
||||||
|
config:
|
||||||
|
cloudflare:
|
||||||
|
enabled: true
|
||||||
|
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 600
|
||||||
|
|
||||||
|
deny_pages:
|
||||||
|
enabled: false # Disable globally, enable per-container via labels
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Variables
|
||||||
|
|
||||||
|
Configure plugins via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Global settings
|
||||||
|
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
||||||
|
EASYHAPROXY_PLUGINS_ABORT_ON_ERROR=false
|
||||||
|
|
||||||
|
# Plugin-specific configuration
|
||||||
|
EASYHAPROXY_PLUGIN_CLEANUP_ENABLED=true
|
||||||
|
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||||
|
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH=/etc/haproxy/cloudflare_ips.lst
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variable format:**
|
||||||
|
- Enable plugins: `EASYHAPROXY_PLUGINS_ENABLED=plugin1,plugin2`
|
||||||
|
- Configure plugin: `EASYHAPROXY_PLUGIN_<PLUGIN_NAME>_<CONFIG_KEY>=value`
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Restrict Admin Panel to Office IPs
|
||||||
|
|
||||||
|
Protect admin panel by only allowing access from office network:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: admin.example.com
|
||||||
|
easyhaproxy.http.plugins: ip_whitelist
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 203.0.113.0/24,198.51.100.42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect Admin Paths
|
||||||
|
|
||||||
|
Block access to WordPress admin and other sensitive paths:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: wordpress.example.com
|
||||||
|
easyhaproxy.http.plugins: deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /wp-admin,/wp-login.php,/.env
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare IP Restoration
|
||||||
|
|
||||||
|
Restore original visitor IPs for applications behind Cloudflare:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: myapp.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Plugins Together
|
||||||
|
|
||||||
|
Combine multiple plugins for one domain:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: secure-app.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/config
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Cleanup
|
||||||
|
|
||||||
|
Keep your system clean with automatic temp file removal:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
plugins:
|
||||||
|
enabled: [cleanup]
|
||||||
|
config:
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 3600 # 1 hour
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
### Log and Continue (Default)
|
### Log and Continue (Recommended)
|
||||||
|
|
||||||
By default, plugin errors are logged as warnings and the discovery cycle continues:
|
By default, plugin errors are logged as warnings and discovery continues:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plugins:
|
plugins:
|
||||||
abort_on_error: false # Default
|
abort_on_error: false # Default
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures that a failing plugin doesn't prevent HAProxy configuration updates.
|
**When to use:** Most situations. Ensures a failing plugin doesn't prevent HAProxy updates.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Plugin errors logged as warnings
|
||||||
|
- Discovery cycle continues
|
||||||
|
- Other plugins still execute
|
||||||
|
- HAProxy config is generated without the failed plugin
|
||||||
|
|
||||||
### Abort on Error
|
### Abort on Error
|
||||||
|
|
||||||
For critical plugins, you can stop the discovery cycle on errors:
|
Stop discovery cycle if any plugin fails:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plugins:
|
plugins:
|
||||||
abort_on_error: true
|
abort_on_error: true
|
||||||
```
|
```
|
||||||
|
|
||||||
With this setting, any plugin error will halt configuration generation and the previous HAProxy config remains active until the issue is resolved.
|
**When to use:** Critical plugins where failure should halt deployment.
|
||||||
|
|
||||||
## Custom Plugins
|
**Behavior:**
|
||||||
|
- Plugin error stops discovery
|
||||||
|
- Previous HAProxy config remains active
|
||||||
|
- No configuration changes until issue is resolved
|
||||||
|
|
||||||
You can create custom plugins by placing Python files in `/etc/haproxy/plugins/`.
|
## Troubleshooting
|
||||||
|
|
||||||
### Simple Custom Plugin Example
|
### Enable Debug Logging
|
||||||
|
|
||||||
Create `/etc/haproxy/plugins/custom_header.py`:
|
See detailed plugin execution information:
|
||||||
|
|
||||||
```python
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
|
||||||
|
|
||||||
|
|
||||||
class CustomHeaderPlugin(PluginInterface):
|
|
||||||
def __init__(self):
|
|
||||||
self.enabled = True
|
|
||||||
self.header_name = "X-Custom-App"
|
|
||||||
self.header_value = "EasyHAProxy"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return "custom_header"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def plugin_type(self) -> PluginType:
|
|
||||||
return PluginType.DOMAIN
|
|
||||||
|
|
||||||
def configure(self, config: dict) -> None:
|
|
||||||
if "enabled" in config:
|
|
||||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
|
||||||
if "header_name" in config:
|
|
||||||
self.header_name = config["header_name"]
|
|
||||||
if "header_value" in config:
|
|
||||||
self.header_value = config["header_value"]
|
|
||||||
|
|
||||||
def process(self, context: PluginContext) -> PluginResult:
|
|
||||||
if not self.enabled:
|
|
||||||
return PluginResult()
|
|
||||||
|
|
||||||
haproxy_config = f'http-request set-header {self.header_name} "{self.header_value}"'
|
|
||||||
|
|
||||||
return PluginResult(
|
|
||||||
haproxy_config=haproxy_config,
|
|
||||||
metadata={"domain": context.domain}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enable Your Custom Plugin
|
|
||||||
|
|
||||||
**YAML:**
|
|
||||||
```yaml
|
|
||||||
plugins:
|
|
||||||
custom_header:
|
|
||||||
enabled: true
|
|
||||||
header_name: X-My-Header
|
|
||||||
header_value: MyValue
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container Label:**
|
|
||||||
```yaml
|
|
||||||
labels:
|
|
||||||
easyhaproxy.http.plugins: custom_header
|
|
||||||
easyhaproxy.http.plugin.custom_header.header_value: CustomValue
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugin Development
|
|
||||||
|
|
||||||
For detailed information on developing plugins, see the [Plugin Developer Guide](plugin-development.md).
|
|
||||||
|
|
||||||
Key points:
|
|
||||||
- Plugins must inherit from `PluginInterface`
|
|
||||||
- Implement required methods: `name`, `plugin_type`, `configure`, `process`
|
|
||||||
- Return `PluginResult` with HAProxy config snippets
|
|
||||||
- Use `loggerEasyHaproxy` for logging
|
|
||||||
- Handle errors gracefully
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### 1. Cloudflare IP Restoration
|
|
||||||
|
|
||||||
Restore original visitor IPs when using Cloudflare:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
plugins:
|
|
||||||
cloudflare:
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires Cloudflare IP list at `/etc/haproxy/cloudflare_ips.lst`. See [Cloudflare documentation](https://support.cloudflare.com/hc/en-us/articles/200170786-Restoring-original-visitor-IPs).
|
|
||||||
|
|
||||||
### 2. Path Protection
|
|
||||||
|
|
||||||
Block sensitive paths from public access:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
labels:
|
|
||||||
easyhaproxy.http.host: myapp.com
|
|
||||||
easyhaproxy.http.plugins: deny_pages
|
|
||||||
easyhaproxy.http.plugin.deny_pages.paths: /admin,/config,/debug
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Automatic Cleanup
|
|
||||||
|
|
||||||
Keep your system clean:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
plugins:
|
|
||||||
cleanup:
|
|
||||||
enabled: true
|
|
||||||
max_idle_time: 3600 # 1 hour
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Custom Authentication
|
|
||||||
|
|
||||||
Create a plugin to add basic auth:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def process(self, context: PluginContext) -> PluginResult:
|
|
||||||
return PluginResult(
|
|
||||||
haproxy_config="""http-request auth realm MyApp unless { http_auth(user_list) }"""
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
Enable debug logging to see plugin execution:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
EASYHAPROXY_LOG_LEVEL=DEBUG
|
EASYHAPROXY_LOG_LEVEL=DEBUG
|
||||||
```
|
```
|
||||||
|
|
||||||
Look for log messages:
|
**Look for:**
|
||||||
```
|
```
|
||||||
INFO: Loaded builtin plugin: cloudflare (domain)
|
INFO: Loaded builtin plugin: cloudflare (domain)
|
||||||
|
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'}
|
DEBUG: Plugin cloudflare metadata: {'domain': 'example.com', 'ip_list_path': '/etc/haproxy/cloudflare_ips.lst'}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Plugin Not Loading
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. Plugin file exists in `/etc/haproxy/plugins/` or builtin directory
|
||||||
|
2. Python syntax is valid
|
||||||
|
3. Plugin class inherits from `PluginInterface`
|
||||||
|
4. Check logs for load errors
|
||||||
|
|
||||||
|
### Plugin Not Executing
|
||||||
|
|
||||||
|
**For domain plugins:**
|
||||||
|
1. Check container has label: `easyhaproxy.http.plugins: plugin_name`
|
||||||
|
2. Verify plugin name is correct (case-sensitive)
|
||||||
|
3. Enable debug logging
|
||||||
|
|
||||||
|
**For global plugins:**
|
||||||
|
1. Check YAML config: `plugins.enabled: [plugin_name]`
|
||||||
|
2. Or env var: `EASYHAPROXY_PLUGINS_ENABLED=plugin_name`
|
||||||
|
3. Enable debug logging
|
||||||
|
|
||||||
|
### Configuration Not Applied
|
||||||
|
|
||||||
|
**Check precedence order:**
|
||||||
|
1. Container labels (highest)
|
||||||
|
2. YAML configuration
|
||||||
|
3. Environment variables (lowest)
|
||||||
|
|
||||||
|
Container labels override YAML and env vars.
|
||||||
|
|
||||||
|
### Plugin Output Missing
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
1. Plugin is enabled (`enabled: true`)
|
||||||
|
2. Plugin configuration is correct
|
||||||
|
3. Plugin's `process()` method returns valid `PluginResult`
|
||||||
|
4. Check debug logs for plugin execution
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Test plugins independently** before deploying to production
|
1. **Start with log-and-continue mode** - Use `abort_on_error: false` until you're confident plugins are stable
|
||||||
2. **Use log-and-continue mode** unless a plugin is critical
|
2. **Use container labels for domain-specific config** - Easier to manage per-service
|
||||||
3. **Keep plugin logic simple** - one plugin should do one thing well
|
3. **Use YAML/env for global config** - Better for global plugins and defaults
|
||||||
4. **Document configuration options** in plugin docstrings
|
4. **Enable debug logging during testing** - Helps identify configuration issues
|
||||||
5. **Handle errors gracefully** - return empty PluginResult on errors
|
5. **Test plugin changes in staging first** - Avoid production surprises
|
||||||
6. **Use sensible defaults** so plugins work out of the box
|
6. **Keep plugin configurations simple** - Use defaults when possible
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Plugins are Python-only (no shell scripts)
|
- Plugins must be written in Python
|
||||||
- Plugins cannot modify the HAProxy template structure
|
- Domain plugins execute for each domain, so keep them lightweight
|
||||||
- Domain plugins are executed for each domain, so keep them lightweight
|
- Plugins cannot modify the Jinja2 template structure directly
|
||||||
- Plugin errors in abort mode will halt configuration updates
|
- Plugin errors in abort mode prevent all configuration updates
|
||||||
|
|
||||||
|
## Creating Custom Plugins
|
||||||
|
|
||||||
|
To create your own plugins, see the [Plugin Developer Guide](plugin-development.md).
|
||||||
|
|
||||||
## Further Reading
|
## Further Reading
|
||||||
|
|
||||||
- [Plugin Developer Guide](plugin-development.md)
|
- [Plugin Developer Guide](plugin-development.md) - Create custom plugins
|
||||||
- [Container Labels](container-labels.md)
|
- [Container Labels](container-labels.md) - Label configuration reference
|
||||||
- [Environment Variables](environment-variable.md)
|
- [Environment Variables](environment-variable.md) - Environment variable reference
|
||||||
- [Static Configuration](static.md)
|
- [Static Configuration](static.md) - YAML configuration reference
|
||||||
|
|
|
||||||
107
src/plugins/builtin/ip_whitelist.py
Normal file
107
src/plugins/builtin/ip_whitelist.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""
|
||||||
|
IP Whitelist Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin restricts access to a domain to only specific IP addresses or CIDR ranges.
|
||||||
|
It runs as a DOMAIN plugin (once per domain).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- allowed_ips: Comma-separated list of IPs/CIDR ranges to allow
|
||||||
|
- status_code: HTTP status code to return for blocked IPs (default: 403)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
ip_whitelist:
|
||||||
|
enabled: true
|
||||||
|
allowed_ips: "192.168.1.0/24,10.0.0.1,172.16.0.0/16"
|
||||||
|
status_code: 403
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "ip_whitelist"
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.1"
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.status_code: 403
|
||||||
|
|
||||||
|
HAProxy Config Generated:
|
||||||
|
# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src 192.168.1.0/24 10.0.0.1
|
||||||
|
http-request deny deny_status 403 if !whitelisted_ip
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
|
||||||
|
|
||||||
|
class IpWhitelistPlugin(PluginInterface):
|
||||||
|
"""Plugin to restrict access to specific IP addresses"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.allowed_ips = []
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "ip_whitelist"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- allowed_ips: Comma-separated list of IPs/CIDR ranges
|
||||||
|
- status_code: HTTP status code to return for denied requests
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "allowed_ips" in config:
|
||||||
|
ips_str = str(config["allowed_ips"])
|
||||||
|
self.allowed_ips = [ip.strip() for ip in ips_str.split(",") if ip.strip()]
|
||||||
|
|
||||||
|
if "status_code" in config:
|
||||||
|
try:
|
||||||
|
self.status_code = int(config["status_code"])
|
||||||
|
except ValueError:
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Generate HAProxy config to whitelist specific IPs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context with domain information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy configuration snippet
|
||||||
|
"""
|
||||||
|
if not self.enabled or not self.allowed_ips:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Create space-separated list of IPs for ACL
|
||||||
|
ips_str = " ".join(self.allowed_ips)
|
||||||
|
|
||||||
|
# Generate HAProxy config snippet
|
||||||
|
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src {ips_str}
|
||||||
|
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=haproxy_config,
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata={
|
||||||
|
"domain": context.domain,
|
||||||
|
"allowed_ips": self.allowed_ips,
|
||||||
|
"status_code": self.status_code
|
||||||
|
}
|
||||||
|
)
|
||||||
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"192.168.1.40": {
|
||||||
|
"easyhaproxy.http.host": "secure.example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "ip_whitelist",
|
||||||
|
"easyhaproxy.http.plugin.ip_whitelist.allowed_ips": "192.168.1.0/24,10.0.0.5",
|
||||||
|
"easyhaproxy.http.plugin.ip_whitelist.status_code": "403"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ from plugins import PluginManager, PluginContext
|
||||||
from plugins.builtin.cloudflare import CloudflarePlugin
|
from plugins.builtin.cloudflare import CloudflarePlugin
|
||||||
from plugins.builtin.cleanup import CleanupPlugin
|
from plugins.builtin.cleanup import CleanupPlugin
|
||||||
from plugins.builtin.deny_pages import DenyPagesPlugin
|
from plugins.builtin.deny_pages import DenyPagesPlugin
|
||||||
|
from plugins.builtin.ip_whitelist import IpWhitelistPlugin
|
||||||
import easymapping
|
import easymapping
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -320,6 +321,110 @@ class TestDenyPagesPlugin:
|
||||||
assert "http-request deny deny_status 404 if denied_path" in haproxy_config
|
assert "http-request deny deny_status 404 if denied_path" in haproxy_config
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpWhitelistPlugin:
|
||||||
|
"""Test cases for IpWhitelistPlugin (DOMAIN plugin)"""
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_initialization(self):
|
||||||
|
"""Test plugin initializes with correct defaults"""
|
||||||
|
plugin = IpWhitelistPlugin()
|
||||||
|
assert plugin.name == "ip_whitelist"
|
||||||
|
assert plugin.enabled is True
|
||||||
|
assert plugin.allowed_ips == []
|
||||||
|
assert plugin.status_code == 403
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_configuration(self):
|
||||||
|
"""Test plugin configuration"""
|
||||||
|
plugin = IpWhitelistPlugin()
|
||||||
|
|
||||||
|
# Test allowed IPs
|
||||||
|
plugin.configure({"allowed_ips": "192.168.1.0/24,10.0.0.1,172.16.0.0/16"})
|
||||||
|
assert plugin.allowed_ips == ["192.168.1.0/24", "10.0.0.1", "172.16.0.0/16"]
|
||||||
|
|
||||||
|
# Test status code
|
||||||
|
plugin.configure({"status_code": "404"})
|
||||||
|
assert plugin.status_code == 404
|
||||||
|
|
||||||
|
# Test enabled
|
||||||
|
plugin.configure({"enabled": "false"})
|
||||||
|
assert plugin.enabled is False
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_generates_config(self):
|
||||||
|
"""Test plugin generates correct HAProxy config"""
|
||||||
|
plugin = IpWhitelistPlugin()
|
||||||
|
plugin.configure({
|
||||||
|
"allowed_ips": "192.168.1.0/24,10.0.0.1",
|
||||||
|
"status_code": "403"
|
||||||
|
})
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={},
|
||||||
|
easymapping=[],
|
||||||
|
container_env={},
|
||||||
|
domain="example.com",
|
||||||
|
port="80",
|
||||||
|
host_config={}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = plugin.process(context)
|
||||||
|
|
||||||
|
assert result.haproxy_config is not None
|
||||||
|
assert "IP Whitelist" in result.haproxy_config
|
||||||
|
assert "acl whitelisted_ip src 192.168.1.0/24 10.0.0.1" in result.haproxy_config
|
||||||
|
assert "http-request deny deny_status 403 if !whitelisted_ip" in result.haproxy_config
|
||||||
|
assert result.metadata["domain"] == "example.com"
|
||||||
|
assert result.metadata["allowed_ips"] == ["192.168.1.0/24", "10.0.0.1"]
|
||||||
|
assert result.metadata["status_code"] == 403
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_disabled(self):
|
||||||
|
"""Test plugin returns empty config when disabled"""
|
||||||
|
plugin = IpWhitelistPlugin()
|
||||||
|
plugin.configure({"enabled": "false", "allowed_ips": "192.168.1.0/24"})
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={},
|
||||||
|
easymapping=[],
|
||||||
|
container_env={},
|
||||||
|
domain="example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert result.haproxy_config == ""
|
||||||
|
assert result.metadata == {}
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_no_ips(self):
|
||||||
|
"""Test plugin returns empty config when no IPs configured"""
|
||||||
|
plugin = IpWhitelistPlugin()
|
||||||
|
|
||||||
|
context = PluginContext(
|
||||||
|
parsed_object={},
|
||||||
|
easymapping=[],
|
||||||
|
container_env={},
|
||||||
|
domain="example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = plugin.process(context)
|
||||||
|
assert result.haproxy_config == ""
|
||||||
|
|
||||||
|
def test_ip_whitelist_plugin_in_haproxy_config(self):
|
||||||
|
"""Test IP Whitelist plugin integration in full HAProxy config generation"""
|
||||||
|
# Use fixture with ip_whitelist plugin enabled via labels
|
||||||
|
line_list = load_fixture("services-with-ip-whitelist")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"customerrors": False,
|
||||||
|
"certbot": {"email": "test@example.com"},
|
||||||
|
"stats": {"port": 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = easymapping.HaproxyConfigGenerator(result)
|
||||||
|
haproxy_config = cfg.generate(line_list)
|
||||||
|
|
||||||
|
# Verify IP Whitelist config is in the output
|
||||||
|
assert "IP Whitelist - Only allow specific IPs" in haproxy_config
|
||||||
|
assert "acl whitelisted_ip src 192.168.1.0/24 10.0.0.5" in haproxy_config
|
||||||
|
assert "http-request deny deny_status 403 if !whitelisted_ip" in haproxy_config
|
||||||
|
|
||||||
|
|
||||||
class TestPluginManager:
|
class TestPluginManager:
|
||||||
"""Test cases for PluginManager"""
|
"""Test cases for PluginManager"""
|
||||||
|
|
||||||
|
|
@ -332,15 +437,17 @@ class TestPluginManager:
|
||||||
assert "cloudflare" in manager.plugins
|
assert "cloudflare" in manager.plugins
|
||||||
assert "cleanup" in manager.plugins
|
assert "cleanup" in manager.plugins
|
||||||
assert "deny_pages" in manager.plugins
|
assert "deny_pages" in manager.plugins
|
||||||
|
assert "ip_whitelist" in manager.plugins
|
||||||
|
|
||||||
# Verify plugin types
|
# Verify plugin types
|
||||||
assert len(manager.global_plugins) == 1 # cleanup
|
assert len(manager.global_plugins) == 1 # cleanup
|
||||||
assert len(manager.domain_plugins) == 2 # cloudflare, deny_pages
|
assert len(manager.domain_plugins) == 3 # cloudflare, deny_pages, ip_whitelist
|
||||||
|
|
||||||
# Verify plugin instances
|
# Verify plugin instances
|
||||||
assert manager.plugins["cloudflare"].name == "cloudflare"
|
assert manager.plugins["cloudflare"].name == "cloudflare"
|
||||||
assert manager.plugins["cleanup"].name == "cleanup"
|
assert manager.plugins["cleanup"].name == "cleanup"
|
||||||
assert manager.plugins["deny_pages"].name == "deny_pages"
|
assert manager.plugins["deny_pages"].name == "deny_pages"
|
||||||
|
assert manager.plugins["ip_whitelist"].name == "ip_whitelist"
|
||||||
|
|
||||||
def test_plugin_manager_executes_global_plugins(self):
|
def test_plugin_manager_executes_global_plugins(self):
|
||||||
"""Test plugin manager executes global plugins correctly"""
|
"""Test plugin manager executes global plugins correctly"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue