1
0
Fork 0

Migrate examples to tests_e2e directory structure

- Relocated all files and scripts from `examples` to `tests_e2e` for better organization.
- Updated references and paths in configurations, scripts, and test files.
- Adjusted `bump-version.sh` to handle the new `tests_e2e` structure.
- Updated `.gitignore` to reflect path changes.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-12 17:05:04 -05:00
parent b34d7822e4
commit d66c4f8595
51 changed files with 35 additions and 35 deletions

View file

@ -0,0 +1,18 @@
# Instructions for testing
1. Run a docker compose in background for the specified feature e.g. `docker compose -f docker-compose.yml up -d`
2. Check if it is running by running `docker ps` and verifying the container is up
3. If the container is not running, check the logs with `docker logs <container_id>` to diagnose any issues
4. In the top each file, you can find the instructions to test and check if it is working.
5. If everything is working tear down the container with `docker compose -f docker-compose.yml down`
6. To ensure the container is properly shut down, use `docker compose -f docker-compose.yml down --remove-orphans` to remove any orphaned containers.
# In case you find issues
**DONT TEAR DOWN THE CONTAINERS**
1. Investigate the source code in src/*
2. Try to fix it.
3. After the code is changed, build it again: `docker build -t byjg/easy-haproxy:5.0.0 -f build/Dockerfile --no-cache .` and start the tests again.

View file

@ -0,0 +1,52 @@
# Docker Compose Examples
Self-contained examples for EasyHAProxy. **All documentation is in the docker-compose files as header comments.**
## Quick Start
1. Pick an example below
2. Open the docker-compose file
3. Read the header comments for complete instructions
4. Run the commands step-by-step
## Basic Examples
| File | Description |
|----------------------------------------------------------------------------|----------------------------------------------------------------|
| [docker-compose.yml](docker-compose.yml) | Basic SSL setup with two virtual hosts and stats interface |
| [docker-compose-acme.yml](docker-compose-acme.yml) | Let's Encrypt SSL with automatic certificate generation |
| [docker-compose-multi-containers.yml](docker-compose-multi-containers.yml) | Load balancing across multiple container replicas |
| [docker-compose-changed-label.yml](docker-compose-changed-label.yml) | Using custom label prefix (for multiple EasyHAProxy instances) |
## Real-World Application Examples
| File | Description |
|--------------------------------------------------------------------------------------|-----------------------------------------------------|
| [docker-compose-portainer.yml](docker-compose-portainer.yml) | Portainer behind EasyHAProxy with Let's Encrypt |
| [docker-compose-portainer-app-example.yml](docker-compose-portainer-app-example.yml) | Additional app alongside Portainer (shared network) |
## Plugin Examples
| File | Description |
|----------------------------------------------------------------------------|-----------------------------------------------------|
| [docker-compose-php-fpm.yml](docker-compose-php-fpm.yml) | FastCGI plugin with PHP-FPM and PATH_INFO routing |
| [docker-compose-jwt-validator.yml](docker-compose-jwt-validator.yml) | JWT token validation for API protection |
| [docker-compose-ip-whitelist.yml](docker-compose-ip-whitelist.yml) | IP whitelist for admin panels or sensitive services |
| [docker-compose-cloudflare.yml](docker-compose-cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN |
| [docker-compose-plugins-combined.yml](docker-compose-plugins-combined.yml) | Multiple plugins combined for layered security |
## Documentation Structure
Each docker-compose file contains:
- **WHAT THIS DEMONSTRATES** - Key features and concepts
- **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times)
- **HOW TO START** - Command to launch the stack
- **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs
- **CLEAN UP** - Commands to stop and remove resources
## Additional Documentation
- [Container Labels Reference](../../docs/container-labels.md)
- [Docker Configuration Guide](../../docs/docker.md)
- [Environment Variables](../../docs/environment-variable.md)
- [Plugin Documentation](../../docs/plugins/)

View file

@ -0,0 +1,93 @@
# ==============================================================================
# EXAMPLE: Let's Encrypt SSL with ACME/Certbot
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Automatic SSL certificate generation using Let's Encrypt
# - HTTP-01 ACME challenge protocol
# - Certificate persistence across container restarts
# - Auto-renewal of certificates
#
# REQUIREMENTS (run these first):
# ```bash
# # You MUST have:
# # - A public IP address pointing to your machine
# # - Ports 80 and 443 open in your firewall
# # - A valid domain name with DNS configured
#
# # Edit this file and change:
# # - Line 21: EASYHAPROXY_CERTBOT_EMAIL to your email
# # - Line 36: easyhaproxy.http.host to your real domain
#
# # Create certs directory
# mkdir -p ./certs/certbot
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-acme.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check logs for certificate issuance
# docker compose -f docker-compose-acme.yml logs -f haproxy
# # Look for: "Successfully received certificate"
#
# # Test HTTPS with real domain (replace test.xpto.us with your domain)
# curl https://test.xpto.us/
# # Expected: 200 OK with valid SSL certificate
#
# # Verify certificate
# openssl s_client -showcerts -connect test.xpto.us:443 < /dev/null | grep "Issuer:"
# # Expected: Issuer: C = US, O = Let's Encrypt
#
# # Check certificate files
# ls -la ./certs/certbot/
# # Expected: Your domain certificate files
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-acme.yml down
# # Keep certificates:
# # docker compose -f docker-compose-acme.yml down
# # Remove certificates too:
# # docker compose -f docker-compose-acme.yml down && rm -rf ./certs/certbot
# ```
#
# ==============================================================================
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Persist the CERTBOT to avoid re-challenge when the server restarts
- ./certs/certbot:/certs/certbot
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
# SETUP THE EMAIL for CertBot
EASYHAPROXY_CERTBOT_EMAIL: user@example.com
# Let's encrypt don´t need AUTOCONFIG, just email.
# If you want other, please refer to the documentation
# EASYHAPROXY_CERTBOT_AUTOCONFIG: zerossl
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
container:
image: byjg/static-httpserver
labels:
# Setup here the domain will have the SSL issued
easyhaproxy.http.redirect_ssl: true
easyhaproxy.http.host: test.xpto.us
easyhaproxy.http.localport: 8080
easyhaproxy.http.certbot: true

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,80 @@
# ==============================================================================
# EXAMPLE: Cloudflare IP Restoration Plugin
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restoring original visitor IPs when behind Cloudflare CDN
# - Detecting requests from Cloudflare IP ranges
# - Using CF-Connecting-IP header for real client IP
# - Accurate IP logging for applications behind Cloudflare
#
# REQUIREMENTS (run these first):
# ```bash
# # Download Cloudflare IP ranges (idempotent - overwrites if exists)
# curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# echo "" >> cloudflare_ips.lst
# curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
#
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-cloudflare.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test normal request
# curl -H "Host: myapp.local" http://127.0.0.1/
# # Expected: 200 OK
#
# # Test with CF-Connecting-IP header (simulating Cloudflare)
# curl -H "Host: myapp.local" -H "CF-Connecting-IP: 203.0.113.50" http://127.0.0.1/
# # Expected: 200 OK (backend sees 203.0.113.50 as client IP)
#
# # Note: This plugin is most useful when your site is actually behind Cloudflare
# # In production, requests come from Cloudflare IPs and the plugin restores real client IPs
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-cloudflare.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../..
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Mount Cloudflare IP list
- ./cloudflare_ips.lst:/etc/haproxy/cloudflare_ips.lst:ro
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
# Web application behind Cloudflare (header-echo server for testing)
webapp:
build: ../fixtures/header-echo
labels:
easyhaproxy.http.host: myapp.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# Enable Cloudflare plugin
easyhaproxy.http.plugins: cloudflare
# Use custom IP list (disable built-in IPs)
easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false
easyhaproxy.http.plugin.cloudflare.ip_list_path: /etc/haproxy/cloudflare_ips.lst

View file

@ -0,0 +1,79 @@
# ==============================================================================
# EXAMPLE: IP Whitelist Plugin
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restricting access to specific IP addresses or CIDR ranges
# - Single IP, CIDR notation, and multiple IP support
# - Custom HTTP status code for blocked requests
# - Admin panel or sensitive application protection
#
# # IMPORTANT: Update the easyhaproxy.http.plugin.ip_whitelist.allowed_ips with your actual IPs!
# # Default allows localhost and private networks for testing
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-ip-whitelist.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test from localhost (127.0.0.1 is whitelisted)
# curl -k -H "Host: admin.local" http://127.0.0.1/
# # Expected: 200 OK - Access granted
#
# # Test from non-whitelisted IP
# # You'll need to test from another machine or temporarily remove your IP
# # from the allowed_ips list to see the 403 Forbidden response
#
# # View HAProxy stats to see blocked requests
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-ip-whitelist.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../../
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
# Admin panel with IP whitelist
admin:
image: byjg/static-httpserver
environment:
TITLE: "Admin Panel - IP Restricted"
labels:
easyhaproxy.http.host: admin.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# Enable IP whitelist plugin
easyhaproxy.http.plugins: ip_whitelist
# Allow localhost and private networks
# UPDATE THIS with your actual IPs/networks!
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
# Status code to return for blocked IPs
easyhaproxy.http.plugin.ip_whitelist.status_code: 403

View file

@ -0,0 +1,88 @@
# ==============================================================================
# EXAMPLE: JWT Validator Plugin
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - JWT token validation for API protection
# - RS256 algorithm signature verification
# - Issuer and audience validation
# - Public key-based JWT verification
#
# REQUIREMENTS (run these first):
# ```bash
# # Generate SSL certificates and JWT keys (from project root)
# cd ../.. && ./tests_e2e/generate-keys.sh && cd tests_e2e/docker
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-jwt-validator.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test without token (should fail)
# curl -k -H "Host: api.local" http://127.0.0.1/
# # Expected: HTTP 403 - Missing Authorization HTTP header
#
# # Generate test JWT at https://jwt.io with:
# # - Algorithm: RS256
# # - Payload: {"iss":"https://auth.example.com/","aud":"https://api.example.com","exp":9999999999}
# # - Paste contents of jwt_private.pem in private key field
#
# # Test with valid token
# TOKEN="eyJhbGc..." # Replace with your generated token
# curl -k -H "Host: host1.local" -H "Authorization: Bearer $TOKEN" http://api.local/
# # Expected: 200 OK with API response
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-jwt-validator.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../..
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Mount the public key for JWT verification
- ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
# API service protected by JWT
api:
image: byjg/static-httpserver
environment:
TITLE: "Protected API - JWT Required"
labels:
easyhaproxy.http.host: api.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# Enable JWT validator plugin
easyhaproxy.http.plugins: jwt_validator
# JWT validator configuration
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem

View file

@ -0,0 +1,76 @@
# ==============================================================================
# EXAMPLE: Load Balancing with Multiple Container Replicas
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Multiple container replicas behind a single domain
# - Round-robin load balancing across replicas
# - Domain redirect functionality
# - Custom port configuration
#
# REQUIREMENTS (run these first):
# ```bash
# # No special requirements - this example runs on localhost:19901
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-multi-containers.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test load balancing - hostname should alternate between containers
# curl -H "Host: www.helloworld.com" localhost:19901
# # Expected: Container ID (e.g., f6d8d45b7411)
# curl -H "Host: www.helloworld.com" localhost:19901
# # Expected: Different container ID (e.g., 59b213cb8592)
#
# # Test domain redirect
# curl -I -H "Host: google.helloworld.com" localhost:19901
# # Expected: HTTP/1.1 301 Moved Permanently, Location: www.google.com/
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# # You should see 2 backend servers
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-multi-containers.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../../
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- 19901:19901
- 1936:1936
nginx:
#image: nginx
image: stenote/nginx-hostname
deploy:
replicas: 2
labels:
easyhaproxy.http.redirect: '{"google.helloworld.com": "www.google.com"}'
easyhaproxy.http.host: www.helloworld.com
easyhaproxy.http.port: 19901
easyhaproxy.http.localport: 80

View file

@ -0,0 +1,82 @@
# ==============================================================================
# EXAMPLE: FastCGI Plugin with PHP-FPM
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - PHP-FPM configuration with FastCGI protocol (proto: fcgi)
# - FastCGI plugin for PHP environment variable configuration
# - TCP connection to PHP-FPM on port 9000
# - PATH_INFO support for RESTful routing
# - Custom document root and index file configuration
#
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-php-fpm.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test main page
# curl -k -H "Host: phpapp.local" http://127.0.0.1/
# # Expected: 200 OK with PHP environment info
#
# # Test PHP info page
# -k -H "Host: phpapp.local" http://127.0.0.1/info.php
# # Expected: phpinfo() output
#
# # Test PATH_INFO routing
# -k -H "Host: phpapp.local" http://127.0.0.1/test-path-info.php/users/123
# # Expected: PATH_INFO=/users/123
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-php-fpm.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../../
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
# PHP-FPM service using byjg/php image
php-fpm:
image: byjg/php:8.5-fpm
volumes:
# Mount PHP application files
- ./php-app:/var/www/html:ro
labels:
easyhaproxy.http.host: phpapp.local
easyhaproxy.http.port: 80
# PHP-FPM listens on port 9000
easyhaproxy.http.localport: 9000
easyhaproxy.http.proto: fcgi
# Enable FastCGI plugin for PHP environment configuration
easyhaproxy.http.plugins: fastcgi
# FastCGI plugin configuration
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html
easyhaproxy.http.plugin.fastcgi.index_file: index.php
easyhaproxy.http.plugin.fastcgi.path_info: "true"

View file

@ -0,0 +1,138 @@
# ==============================================================================
# EXAMPLE: Multiple Plugins Combined
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Using multiple security plugins together
# - Different plugin combinations for different services
# - Cloudflare + path blocking for public sites
# - JWT validation + path blocking for APIs
# - IP whitelist for admin panels
# - Layered security approach
#
# REQUIREMENTS (run these first):
# ```bash
# # Generate SSL certificates and JWT keys (from project root)
# cd ../.. && ./tests_e2e/generate-keys.sh && cd tests_e2e/docker
#
# # Download Cloudflare IPs (idempotent - overwrites if exists)
# curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# echo "" >> cloudflare_ips.lst
# curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
#
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-plugins-combined.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test public website (Cloudflare + path blocking)
# curl -k -H "Host: website.local" http://127.0.0.1/
# # Expected: 200 OK
# curl -k -H "Host: website.local" http://127.0.0.1/admin
# # Expected: HTTP 404 - Path blocked
#
# # Test protected API (JWT required)
# curl -k -H "Host: api.local" http://127.0.0.1/
# # Expected: HTTP 403 - Missing Authorization header
# # Generate JWT at https://jwt.io (see jwt-validator example for details)
# TOKEN="eyJhbGc..." # Replace with your token
# curl -H "Host: api.local" -H "Authorization: Bearer $TOKEN" http://127.0.0.1/
# # Expected: 200 OK
#
# # Test admin panel (IP whitelist)
# curl -k -H "Host: admin.local" http://127.0.0.1/
# # Expected: 200 OK from localhost
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# # You should see 3 backends with different security configurations
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-plugins-combined.yml down
# ```
#
# ==============================================================================
services:
haproxy:
build:
context: ../../
dockerfile: build/Dockerfile
image: byjg/easy-haproxy:local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./cloudflare_ips.lst:/etc/haproxy/cloudflare_ips.lst:ro
- ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
environment:
EASYHAPROXY_DISCOVER: docker
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
# Public website with Cloudflare + path blocking
website:
image: byjg/static-httpserver
environment:
TITLE: "Public Website"
labels:
easyhaproxy.http.host: website.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# Combine Cloudflare IP restoration + deny pages
easyhaproxy.http.plugins: cloudflare,deny_pages
# Block admin paths, config files, etc.
easyhaproxy.http.plugin.deny_pages.paths: /admin,/wp-admin,/wp-login.php,/.env,/config
easyhaproxy.http.plugin.deny_pages.status_code: 404
# Protected API with JWT validation + path blocking
api:
image: byjg/static-httpserver
environment:
TITLE: "Protected API"
labels:
easyhaproxy.http.host: api.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# JWT validation + block internal endpoints
easyhaproxy.http.plugins: jwt_validator,deny_pages
# JWT configuration
easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
# Block internal/debug endpoints
easyhaproxy.http.plugin.deny_pages.paths: /internal,/debug,/metrics
easyhaproxy.http.plugin.deny_pages.status_code: 403
# Admin panel with strict IP restrictions
admin:
image: byjg/static-httpserver
environment:
TITLE: "Admin Panel"
labels:
easyhaproxy.http.host: admin.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
# IP whitelist only (strictest security)
easyhaproxy.http.plugins: ip_whitelist
# Only allow local and private networks (including Docker bridge)
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
easyhaproxy.http.plugin.ip_whitelist.status_code: 403

View file

@ -0,0 +1,58 @@
# ==============================================================================
# EXAMPLE: Additional Application with Portainer
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Adding more applications to an existing EasyHAProxy setup
# - Using the shared "easyhaproxy" network
# - Multiple applications behind the same HAProxy instance
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. First start the Portainer stack (creates network and HAProxy)
# docker compose -f docker-compose-portainer.yml up -d
#
# # 2. Edit this file and change:
# # - Line 6: easyhaproxy.http.host to your real domain
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-portainer-app-example.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check container is running
# docker compose -f docker-compose-portainer-app-example.yml ps
#
# # Test the application
# curl http://test.xpto.us
# # OR with /etc/hosts: echo "127.0.0.1 test.xpto.us" | sudo tee -a /etc/hosts
#
# # Verify both apps in HAProxy stats (port 1936)
# # You should see backends for both portainer.xpto.us and test.xpto.us
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-portainer-app-example.yml down
# ```
#
# ==============================================================================
services:
container:
image: byjg/static-httpserver
labels:
easyhaproxy.http.redirect_ssl: true
easyhaproxy.http.host: test.xpto.us
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 8080
easyhaproxy.http.certbot: true
networks:
default:
name: easyhaproxy
external: true

View file

@ -0,0 +1,110 @@
# ==============================================================================
# EXAMPLE: Portainer Behind EasyHAProxy
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Running Portainer (Docker management UI) behind EasyHAProxy
# - Using external volumes and networks for shared infrastructure
# - Real-world application example with Let's Encrypt
# - HTTP to HTTPS redirect with Certbot
#
# REQUIREMENTS (run these first):
# ```bash
# # Create required volumes (idempotent)
# docker volume create certs_certbot
# docker volume create certs_haproxy
# docker volume create portainer_data
#
# # Create shared network (idempotent)
# docker network create easyhaproxy
#
# # Edit this file and change:
# # - Line 18: EASYHAPROXY_CERTBOT_EMAIL to your email
# # - Line 38: easyhaproxy.http.host to your real domain
# ```
#
# HOW TO START:
# ```bash
# docker compose -f docker-compose-portainer.yml up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check containers are running
# docker compose -f docker-compose-portainer.yml ps
# # Expected: Both easyhaproxy and portainer containers running
#
# # Access Portainer (replace with your domain or use /etc/hosts)
# # First time: Create admin user
# curl http://portainer.xpto.us
# # OR with /etc/hosts: echo "127.0.0.1 portainer.xpto.us" | sudo tee -a /etc/hosts
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker compose -f docker-compose-portainer.yml down
# # To also remove volumes:
# # docker compose -f docker-compose-portainer.yml down -v
# # docker volume rm certs_certbot certs_haproxy portainer_data
# # docker network rm easyhaproxy
# ```
#
# ==============================================================================
services:
easyhaproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- certs_certbot:/certs/certbot
# - certs_haproxy:/certs/haproxy
environment:
EASYHAPROXY_DISCOVER: docker
EASYHAPROXY_LABEL_PREFIX: easyhaproxy
EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org
EASYHAPROXY_SSL_MODE: "default"
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
portainer:
image: portainer/portainer-ce:latest
volumes:
- portainer_data:/data
- /var/run/docker.sock:/var/run/docker.sock
labels:
easyhaproxy.http.redirect_ssl: true
easyhaproxy.http.certbot: true
easyhaproxy.http.host: portainer.xpto.us
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 9000
volumes:
certs_certbot:
external: true
certs_haproxy:
external: true
portainer_data:
external: true
networks:
default:
name: easyhaproxy
external: true

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,151 @@
# Sample PHP Application for FastCGI Plugin
This directory contains a sample PHP application that demonstrates the FastCGI plugin functionality with EasyHAProxy.
## Files
### index.php
The main page that displays:
- PHP version and configuration
- FastCGI environment variables set by EasyHAProxy
- How the FastCGI plugin works
- Links to test pages
Access: `http://phpapp.local/`
### info.php
Standard `phpinfo()` page showing complete PHP configuration.
Access: `http://phpapp.local/info.php`
### test-path-info.php
Demonstrates PATH_INFO support for RESTful URL routing.
Examples:
- `http://phpapp.local/test-path-info.php/users`
- `http://phpapp.local/test-path-info.php/users/123`
- `http://phpapp.local/test-path-info.php/api/v1/products`
## FastCGI Environment Variables
The FastCGI plugin generates an `fcgi-app` configuration that defines these CGI parameters for HAProxy to use:
| Variable | Description | Example |
|----------|-------------|---------|
| `SCRIPT_FILENAME` | Full path to PHP script | `/var/www/html/index.php` |
| `DOCUMENT_ROOT` | Document root directory | `/var/www/html` |
| `SCRIPT_NAME` | Script path | `/index.php` |
| `REQUEST_URI` | Full request URI with query | `/index.php?page=1` |
| `QUERY_STRING` | Query string parameters | `page=1&limit=10` |
| `REQUEST_METHOD` | HTTP method | `GET`, `POST`, etc. |
| `CONTENT_TYPE` | Request content type | `application/json` |
| `CONTENT_LENGTH` | Request body length | `1024` |
| `SERVER_NAME` | Virtual host name | `phpapp.local` |
| `SERVER_PORT` | Server port | `80` or `443` |
| `HTTPS` | SSL status | `on` or `off` |
| `PATH_INFO` | Extra path info (optional) | `/users/123` |
## How It Works
1. **FastCGI plugin generates configuration** (at startup)
- Creates an `fcgi-app` section with CGI parameter definitions
- Includes `docroot`, `index`, and `path-info` settings
- Adds `use-fcgi-app` directive to the backend
2. **Request arrives at HAProxy** (port 80)
- URL: `http://phpapp.local/index.php`
3. **HAProxy uses the fcgi-app configuration**
- Sets `SCRIPT_FILENAME` to `/var/www/html/index.php`
- Sets `DOCUMENT_ROOT` to `/var/www/html`
- Sets all other CGI variables based on the request
- Handles directory requests (appends `index.php`)
4. **HAProxy forwards to PHP-FPM** via FastCGI protocol
- Host: `php-fpm` (container name)
- Port: `9000` (TCP) or Unix socket
- Protocol: `fcgi`
- Sends CGI parameters in FastCGI format
5. **PHP-FPM executes the script**
- Reads the PHP file from `SCRIPT_FILENAME`
- Processes the PHP code with CGI environment
- Returns HTML/JSON response
6. **HAProxy sends response to client**
## Customizing
You can customize the FastCGI plugin configuration in `docker-compose-php-fpm.yml`:
```yaml
labels:
# Change document root
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/public
# Change default index file
easyhaproxy.http.plugin.fastcgi.index_file: app.php
# Disable PATH_INFO
easyhaproxy.http.plugin.fastcgi.path_info: "false"
# Add custom FastCGI parameters
easyhaproxy.http.plugin.fastcgi.custom_params: '{"PHP_VALUE":"memory_limit=256M","APP_ENV":"production"}'
```
## Adding Your Own PHP Application
Replace the contents of this directory with your own PHP application:
```bash
# Remove sample files
rm -rf php-app/*
# Copy your PHP application
cp -r /path/to/your/app/* php-app/
# Restart the stack
docker compose -f docker-compose-php-fpm.yml restart
```
Make sure your application's entry point matches the `index_file` configuration (default: `index.php`).
## Troubleshooting
### "File not found" error
Check that:
1. The file exists in the `php-app/` directory
2. The `document_root` matches the container path (`/var/www/html`)
3. The volume mount is correct in docker-compose.yml
### PATH_INFO not working
Ensure `path_info` is enabled in the plugin configuration:
```yaml
easyhaproxy.http.plugin.fastcgi.path_info: "true"
```
### PHP-FPM connection error
Verify:
1. The `localport: 9000` is set correctly
2. The `proto: fcgi` parameter is set
3. Both containers are running and can communicate
View logs:
```bash
docker compose -f docker-compose-php-fpm.yml logs php-fpm
docker compose -f docker-compose-php-fpm.yml logs haproxy
```
Check connectivity:
```bash
docker compose -f docker-compose-php-fpm.yml exec haproxy ping php-fpm
```
## Learn More
- [FastCGI Plugin Documentation](../../../docs/plugins.md#fastcgi-plugin)
- [Container Labels Reference](../../../docs/container-labels.md)
- [HAProxy FastCGI Documentation](https://docs.haproxy.org/2.8/configuration.html#5.2-proto)

View file

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP-FPM with EasyHAProxy</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #4CAF50;
padding-bottom: 10px;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.info-table th,
.info-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.info-table th {
background: #f8f9fa;
font-weight: bold;
width: 200px;
}
.info-table tr:hover {
background: #f8f9fa;
}
a {
color: #4CAF50;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>PHP-FPM with EasyHAProxy FastCGI Plugin</h1>
<div class="success">
<strong>Success!</strong> PHP is running via FastCGI protocol through HAProxy.
</div>
<h2>FastCGI Environment</h2>
<table class="info-table">
<tr>
<th>PHP Version</th>
<td><?php echo PHP_VERSION; ?></td>
</tr>
<tr>
<th>Server Software</th>
<td><?php echo $_SERVER['SERVER_SOFTWARE'] ?? 'N/A'; ?></td>
</tr>
<tr>
<th>Document Root</th>
<td><code><?php echo $_SERVER['DOCUMENT_ROOT'] ?? 'N/A'; ?></code></td>
</tr>
<tr>
<th>Script Filename</th>
<td><code><?php echo $_SERVER['SCRIPT_FILENAME'] ?? 'N/A'; ?></code></td>
</tr>
<tr>
<th>Request URI</th>
<td><code><?php echo $_SERVER['REQUEST_URI'] ?? 'N/A'; ?></code></td>
</tr>
<tr>
<th>Request Method</th>
<td><?php echo $_SERVER['REQUEST_METHOD'] ?? 'N/A'; ?></td>
</tr>
<tr>
<th>Server Name</th>
<td><?php echo $_SERVER['SERVER_NAME'] ?? 'N/A'; ?></td>
</tr>
<tr>
<th>Server Port</th>
<td><?php echo $_SERVER['SERVER_PORT'] ?? 'N/A'; ?></td>
</tr>
<tr>
<th>HTTPS</th>
<td><?php echo ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'Yes' : 'No'; ?></td>
</tr>
<tr>
<th>PATH_INFO</th>
<td><code><?php echo $_SERVER['PATH_INFO'] ?? 'Not set'; ?></code></td>
</tr>
<tr>
<th>Gateway Interface</th>
<td><?php echo $_SERVER['GATEWAY_INTERFACE'] ?? 'N/A'; ?></td>
</tr>
</table>
<h2>Test Links</h2>
<ul>
<li><a href="/info.php">View PHP Info</a></li>
<li><a href="/test-path-info.php/extra/path">Test PATH_INFO support</a></li>
</ul>
<h2>How This Works</h2>
<p>
This setup uses HAProxy with EasyHAProxy to proxy requests to PHP-FPM via the FastCGI protocol:
</p>
<ol>
<li>HAProxy receives HTTP request on port 80</li>
<li>The FastCGI plugin generates an <code>fcgi-app</code> configuration that defines CGI parameters (SCRIPT_FILENAME, DOCUMENT_ROOT, etc.)</li>
<li>HAProxy uses this configuration to communicate with PHP-FPM via the FastCGI protocol</li>
<li>HAProxy connects to PHP-FPM (via TCP port 9000 or Unix socket, depending on configuration)</li>
<li>PHP-FPM processes the PHP script and returns the response</li>
<li>HAProxy sends the response back to the client</li>
</ol>
<h2>Configuration</h2>
<p>The FastCGI plugin is configured in <code>docker-compose-php-fpm.yml</code>:</p>
<ul>
<li><strong>document_root:</strong> <code>/var/www/html</code></li>
<li><strong>index_file:</strong> <code>index.php</code></li>
<li><strong>path_info:</strong> <code>true</code> (enables PATH_INFO support)</li>
</ul>
</div>
</body>
</html>

View file

@ -0,0 +1,9 @@
<?php
/**
* PHP Info Page
*
* This page displays comprehensive PHP configuration information
* including FastCGI environment variables set by EasyHAProxy.
*/
phpinfo();

View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PATH_INFO Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #2196F3;
padding-bottom: 10px;
}
.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
pre {
background: #f4f4f4;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
}
a {
color: #2196F3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>PATH_INFO Test</h1>
<?php if (isset($_SERVER['PATH_INFO']) && !empty($_SERVER['PATH_INFO'])): ?>
<div class="success">
<strong>Success!</strong> PATH_INFO is working correctly.
</div>
<h2>PATH_INFO Value</h2>
<pre><?php echo htmlspecialchars($_SERVER['PATH_INFO']); ?></pre>
<h2>Parsed Path Segments</h2>
<pre><?php
$segments = explode('/', trim($_SERVER['PATH_INFO'], '/'));
print_r(array_filter($segments));
?></pre>
<?php else: ?>
<div class="info">
<strong>Note:</strong> PATH_INFO is not set. Try accessing this page with additional path segments.
</div>
<?php endif; ?>
<h2>Request Information</h2>
<pre><?php
echo "SCRIPT_NAME: " . ($_SERVER['SCRIPT_NAME'] ?? 'N/A') . "\n";
echo "REQUEST_URI: " . ($_SERVER['REQUEST_URI'] ?? 'N/A') . "\n";
echo "PATH_INFO: " . ($_SERVER['PATH_INFO'] ?? 'N/A') . "\n";
echo "QUERY_STRING: " . ($_SERVER['QUERY_STRING'] ?? 'N/A') . "\n";
?></pre>
<h2>Example Usage</h2>
<p>PATH_INFO enables RESTful URL routing. Try these URLs:</p>
<ul>
<li><a href="/test-path-info.php/users">/test-path-info.php/users</a></li>
<li><a href="/test-path-info.php/users/123">/test-path-info.php/users/123</a></li>
<li><a href="/test-path-info.php/api/v1/products">/test-path-info.php/api/v1/products</a></li>
</ul>
<p><a href="/"> Back to Home</a></p>
</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
COPY server.py .
RUN chmod +x server.py
EXPOSE 8080
CMD ["python3", "server.py"]

View file

@ -0,0 +1,66 @@
# Header Echo Server - Test Fixture
A lightweight Python HTTP server that echoes all request headers as JSON. Used for testing HAProxy plugins that manipulate headers and client IPs.
## Purpose
This test fixture is used by both Docker Compose and Kubernetes test suites to verify:
- Header manipulation (e.g., X-Forwarded-For, CF-Connecting-IP)
- IP restoration plugins (Cloudflare, custom CDN integrations)
- Request routing and backend visibility
## Usage
### Docker Compose
```yaml
services:
webapp:
build: ../fixtures/header-echo
ports:
- "8080:8080"
```
### Kubernetes
```bash
# Build and load into kind cluster
docker build -t header-echo-server:test .
kind load docker-image header-echo-server:test --name your-cluster
# Use in deployment
spec:
containers:
- name: webapp
image: header-echo-server:test
imagePullPolicy: Never
```
### Manual Testing
```bash
# Start the server
python3 server.py
# Test it
curl http://localhost:8080
# Returns JSON with all headers, client IP, and X-Forwarded-For value
```
## Response Format
```json
{
"headers": {
"Host": "localhost:8080",
"User-Agent": "curl/7.81.0",
"Accept": "*/*"
},
"client_ip": "127.0.0.1",
"x_forwarded_for": "NOT SET"
}
```
## Used By
- `tests_e2e/docker/docker-compose-cloudflare.yml`
- `tests_e2e/test_docker_compose.py::TestCloudflare`
- `tests_e2e/kubernetes/cloudflare.yml`
- `tests_e2e/test_kubernetes.py::TestCloudflare`

View file

@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Simple HTTP server that echoes all request headers"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class HeaderEchoHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
# Collect all headers
headers = dict(self.headers)
# Add the client IP as seen by this server
response = {
'headers': headers,
'client_ip': self.client_address[0],
'x_forwarded_for': self.headers.get('X-Forwarded-For', 'NOT SET')
}
self.wfile.write(json.dumps(response, indent=2).encode())
def log_message(self, format, *args):
# Log to stdout
print(f"{self.address_string()} - {format % args}")
if __name__ == '__main__':
port = 8080
server = HTTPServer(('0.0.0.0', port), HeaderEchoHandler)
print(f'Header echo server running on port {port}...')
server.serve_forever()

100
tests_e2e/generate-keys.sh Executable file
View file

@ -0,0 +1,100 @@
#!/bin/bash
set -e
# Generate SSL Certificates and JWT Keys for EasyHAProxy Examples
# This script creates all .pem files needed for the examples directory
echo "Generating SSL certificates and JWT keys for EasyHAProxy examples..."
echo ""
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Create necessary directories
mkdir -p "$SCRIPT_DIR/static"
mkdir -p "$SCRIPT_DIR/docker"
mkdir -p "$SCRIPT_DIR/docker/certs/haproxy"
mkdir -p "$SCRIPT_DIR/swarm/certs"
# ============================================================================
# Generate SSL Certificate for host1.local (4096-bit RSA, 10-year validity)
# ============================================================================
echo "Generating host1.local certificate (4096-bit RSA, 10-year validity)..."
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
-keyout "$SCRIPT_DIR/static/host1.local.pem" \
-out "$SCRIPT_DIR/static/host1.local.pem" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=host1.local"
# Copy to swarm directory
cp "$SCRIPT_DIR/static/host1.local.pem" "$SCRIPT_DIR/swarm/certs/host1.local.pem"
echo "✓ Created host1.local.pem (4096-bit, 10 years)"
echo " - $SCRIPT_DIR/static/host1.local.pem"
echo " - $SCRIPT_DIR/swarm/certs/host1.local.pem"
echo ""
# ============================================================================
# Generate SSL Certificate for host2.local (2048-bit RSA, 1-year validity)
# ============================================================================
echo "Generating host2.local certificate (2048-bit RSA, 1-year validity)..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$SCRIPT_DIR/docker/host2.local.pem" \
-out "$SCRIPT_DIR/docker/host2.local.pem" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=host2.local"
# Copy to swarm directory
cp "$SCRIPT_DIR/docker/host2.local.pem" "$SCRIPT_DIR/swarm/certs/host2.local.pem"
echo "✓ Created host2.local.pem (2048-bit, 1 year)"
echo " - $SCRIPT_DIR/docker/host2.local.pem"
echo " - $SCRIPT_DIR/swarm/certs/host2.local.pem"
echo ""
# ============================================================================
# Generate JWT RSA Key Pair (2048-bit)
# ============================================================================
echo "Generating JWT RSA key pair (2048-bit)..."
# Generate private key
openssl genrsa -out "$SCRIPT_DIR/docker/jwt_private.pem" 2048
# Extract public key
openssl rsa -in "$SCRIPT_DIR/docker/jwt_private.pem" -pubout -out "$SCRIPT_DIR/docker/jwt_pubkey.pem"
echo "✓ Created JWT key pair (2048-bit)"
echo " - $SCRIPT_DIR/docker/jwt_private.pem (private key)"
echo " - $SCRIPT_DIR/docker/jwt_pubkey.pem (public key)"
echo ""
# ============================================================================
# Generate Placeholder Certificate
# ============================================================================
echo "Generating placeholder certificate..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "$SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem" \
-out "$SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=placeholder"
echo "✓ Created placeholder certificate"
echo " - $SCRIPT_DIR/docker/certs/haproxy/.place_holder_cert.pem"
echo ""
# ============================================================================
# Summary
# ============================================================================
echo "============================================"
echo "All certificates and keys generated successfully!"
echo "============================================"
echo ""
echo "SSL Certificates:"
echo " - host1.local (4096-bit, 10 years)"
echo " - host2.local (2048-bit, 1 year)"
echo ""
echo "JWT Keys:"
echo " - jwt_private.pem (private key for signing)"
echo " - jwt_pubkey.pem (public key for validation)"
echo ""
echo "IMPORTANT NOTES:"
echo " - These are self-signed certificates for TESTING ONLY"
echo " - DO NOT use these certificates in production"
echo " - Browsers will show security warnings for self-signed certificates"
echo " - JWT keys should be kept secure and rotated regularly"
echo ""

9
tests_e2e/kubernetes/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# kind installation directory
.kind/
# kubectl config
kubeconfig
# Test artifacts
*.log
service_tls_generated.yml

View file

@ -0,0 +1,50 @@
# Kubernetes Examples
Self-contained examples for EasyHAProxy ingress controller. **All documentation is in the YAML files as header comments.**
## Quick Start
1. Pick an example below
2. Open the YAML file
3. Read the header comments for complete instructions
4. Run the commands step-by-step
## Prerequisites
All examples require:
- EasyHAProxy installed in your Kubernetes cluster
- Node labeled for EasyHAProxy deployment
See header comments in each file for detailed setup instructions.
## Basic Examples
| File | Description |
|------------------------------------|--------------------------------------------|
| [service.yml](service.yml) | Basic HTTP ingress with multiple domains |
| [service_tls.yml](service_tls.yml) | HTTPS/TLS ingress with custom certificates |
## Plugin Examples
| File | Description |
|----------------------------------------------|-----------------------------------------------------|
| [jwt-validator.yml](jwt-validator.yml) | JWT token validation for API protection |
| [ip-whitelist.yml](ip-whitelist.yml) | IP whitelist for admin panels or sensitive services |
| [cloudflare.yml](cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN |
| [plugins-combined.yml](plugins-combined.yml) | Multiple plugins combined for layered security |
## Documentation Structure
Each YAML file contains:
- **WHAT THIS DEMONSTRATES** - Key features and concepts
- **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times)
- **HOW TO START** - Command to apply the manifest
- **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs
- **CLEAN UP** - Commands to remove resources
## Additional Documentation
- [Kubernetes Installation Guide](../../docs/kubernetes.md)
- [Helm Installation](../../docs/helm.md)
- [Kubernetes Annotations Reference](../../docs/kubernetes.md#kubernetes-annotations)
- [Using Plugins with Kubernetes](../../docs/kubernetes.md#using-plugins-with-kubernetes)

View file

@ -0,0 +1,140 @@
# ==============================================================================
# EXAMPLE: Cloudflare IP Restoration Plugin for Kubernetes
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restoring original visitor IPs when behind Cloudflare CDN
# - Using ConfigMaps to mount Cloudflare IP ranges
# - Detecting requests from Cloudflare IP ranges
# - Accurate client IP logging for applications behind Cloudflare
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. Download Cloudflare IP ranges
# curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
#
# # 3. Create ConfigMap with Cloudflare IPs
# kubectl create configmap cloudflare-ips \
# --from-file=cloudflare_ips.lst=cloudflare_ips.lst \
# -n easyhaproxy
#
# # 4. Mount the ConfigMap in EasyHAProxy deployment:
# # Edit your EasyHAProxy deployment and add:
# # volumeMounts:
# # - name: cloudflare-ips
# # mountPath: /etc/haproxy/cloudflare_ips.lst
# # subPath: cloudflare_ips.lst
# # volumes:
# # - name: cloudflare-ips
# # configMap:
# # name: cloudflare-ips
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f cloudflare.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check resources are created
# kubectl get deployment,service,ingress -l app=webapp
#
# # Test via port-forward
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: myapp.example.local" http://localhost:8080
# # Expected: 200 OK with JSON response containing headers, client_ip, and x_forwarded_for
#
# # Test IP translation with CF-Connecting-IP header
# curl -H "Host: myapp.example.local" -H "CF-Connecting-IP: 1.2.3.4" http://localhost:8080
# # Expected: x_forwarded_for should be "1.2.3.4"
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f cloudflare.yml
# ```
#
# ==============================================================================
---
apiVersion: v1
kind: Service
metadata:
name: webapp-service
namespace: default
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: webapp
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: header-echo-server:test
imagePullPolicy: Never
ports:
- containerPort: 8080
resources:
limits:
cpu: '0.1'
memory: '64Mi'
requests:
cpu: '0.05'
memory: '32Mi'
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# Enable Cloudflare plugin with built-in IPs
easyhaproxy.plugins: "cloudflare"
# Optional: Provide custom IP list as base64-encoded text (takes precedence over built-in IPs)
# This is more Kubernetes-native than mounting ConfigMaps/files
# Example IPs: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.1
# How to create: printf "10.0.0.0/8\n172.16.0.0/12\n192.168.0.0/16\n127.0.0.1" | base64 -w 0
# easyhaproxy.plugin.cloudflare.ip_list: "MTAuMC4wLjAvOAoxNzIuMTYuMC4wLzEyCjE5Mi4xNjguMC4wLzE2CjEyNy4wLjAuMQ=="
# 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"
name: webapp-ingress-cloudflare
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: myapp.example.local
http:
paths:
- backend:
service:
name: webapp-service
port:
number: 8080
pathType: ImplementationSpecific

View file

@ -0,0 +1,124 @@
# ==============================================================================
# EXAMPLE: IP Whitelist Plugin for Kubernetes
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restricting access to specific IP addresses or CIDR ranges
# - Using annotations for IP-based access control
# - Protecting admin panels or sensitive services in Kubernetes
# - Custom HTTP status code for blocked requests
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. IMPORTANT: Edit this file (line 80) and update allowed_ips
# # with your actual office/VPN IP addresses or networks
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f ip-whitelist.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check resources are created
# kubectl get deployment,service,ingress -l app=admin
#
# # Test from allowed IP
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: admin.example.local" http://localhost:8080
# # Expected: 200 OK with "Admin Panel - IP Restricted" (if your IP is in allowed_ips)
#
# # Test from non-allowed IP
# # Expected: HTTP 403 Forbidden
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f ip-whitelist.yml
# ```
#
# ==============================================================================
---
apiVersion: v1
kind: Service
metadata:
name: admin-service
namespace: default
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: admin
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: admin
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: admin
template:
metadata:
labels:
app: admin
spec:
containers:
- name: admin
image: byjg/static-httpserver
ports:
- containerPort: 8080
env:
- name: TITLE
value: "Admin Panel - IP Restricted"
resources:
limits:
cpu: '0.1'
memory: '64Mi'
requests:
cpu: '0.05'
memory: '32Mi'
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# Enable IP whitelist plugin
easyhaproxy.plugins: "ip_whitelist"
# Allow specific IPs and networks
# UPDATE THIS with your actual office/VPN IPs!
# For testing: includes localhost and Docker/Kubernetes private networks
easyhaproxy.plugin.ip_whitelist.allowed_ips: "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,203.0.113.0/24,198.51.100.42"
# Status code to return for blocked IPs
easyhaproxy.plugin.ip_whitelist.status_code: "403"
name: admin-ingress-whitelist
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: admin.example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 8080

View file

@ -0,0 +1,135 @@
# Example demonstrating JWT validator with Kubernetes secret
# This shows the recommended way to provide JWT public keys in Kubernetes
#
# IMPORTANT: Before applying this manifest, generate JWT keys by running:
# cd /path/to/examples && bash generate-keys.sh
#
# Then create the secrets with your generated keys:
# kubectl create secret generic jwt-pubkey-secret \
# --from-file=pubkey=docker/jwt_pubkey.pem -n default
# kubectl create secret generic jwt-custom-secret \
# --from-file=rsa-public-key=docker/jwt_pubkey.pem -n default
#
# TWO ANNOTATION FORMATS:
# 1. Auto-detect key (tries common variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret"
# Tries keys: pubkey, public-key, jwt.pub, tls.crt
#
# 2. Explicit key (no variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret/rsa-public-key"
# Only tries key: rsa-public-key
---
# NOTE: Secrets should be created separately using your generated JWT keys
# See instructions at the top of this file
# The test fixture creates these secrets automatically
---
# Deployment for API service
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: byjg/static-httpserver
ports:
- containerPort: 8080
env:
- name: TITLE
value: "Protected API - JWT Required"
---
# Service to be protected with JWT
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: default
spec:
selector:
app: api
ports:
- port: 8080
targetPort: 8080
---
# Ingress Example 1: Auto-detect key (uses standard key name "pubkey")
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress-jwt-auto
namespace: default
annotations:
# Enable JWT validator plugin
easyhaproxy.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
# Auto-detect: tries pubkey, public-key, jwt.pub, tls.crt
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret"
spec:
ingressClassName: easyhaproxy
rules:
- host: api.example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
---
# Ingress Example 2: Explicit key (uses custom key name "rsa-public-key")
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress-jwt-explicit
namespace: default
annotations:
# Enable JWT validator plugin
easyhaproxy.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
# Explicit key: only tries "rsa-public-key" from the secret
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-custom-secret/rsa-public-key"
# Optional: Protect only specific paths
# easyhaproxy.plugin.jwt_validator.paths: "/api,/admin"
# Optional: Allow anonymous access (JWT validated only if present)
# easyhaproxy.plugin.jwt_validator.allow_anonymous: "true"
spec:
ingressClassName: easyhaproxy
rules:
- host: api-custom.example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080

View file

@ -0,0 +1,160 @@
# ==============================================================================
# EXAMPLE: JWT Validator Plugin for Kubernetes
# ==============================================================================
#
# JWT PUBLIC KEY CONFIGURATION OPTIONS:
# There are three ways to provide the JWT public key:
#
# 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"
#
# 2. k8s_secret.pubkey - Reference a Kubernetes secret (RECOMMENDED)
# Auto-detect key (tries common variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "my-jwt-secret"
# Explicit key (no variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "my-jwt-secret/custom-key-name"
# See jwt-validator-secret-example.yml for full example
#
# 3. pubkey - Inline base64-encoded key (for testing only, not recommended for production)
# easyhaproxy.plugin.jwt_validator.pubkey: "LS0tLS1CRUdJTi..."
#
# This example shows option #1 (pubkey_path) for backward compatibility
#
# WHAT THIS DEMONSTRATES:
# - JWT token validation for API protection in Kubernetes
# - RS256 algorithm signature verification
# - Using ConfigMaps to mount JWT public keys
# - Issuer and audience validation
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. Generate RSA key pair (idempotent - skips if exists)
# [ -f jwt_private.pem ] || openssl genrsa -out jwt_private.pem 2048
# [ -f jwt_pubkey.pem ] || openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
#
# # 3. Create ConfigMap with public key
# kubectl create configmap jwt-keys --from-file=api_pubkey.pem=jwt_pubkey.pem
#
# # 4. Mount the ConfigMap in EasyHAProxy deployment:
# # Edit your EasyHAProxy deployment and add:
# # volumeMounts:
# # - name: jwt-keys
# # mountPath: /etc/haproxy/jwt_keys
# # volumes:
# # - name: jwt-keys
# # configMap:
# # name: jwt-keys
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f jwt-validator.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check resources are created
# kubectl get deployment,service,ingress -l app=api
#
# # Test without token (should fail)
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: api.example.local" http://localhost:8080
# # Expected: HTTP 403 - Missing Authorization HTTP header
#
# # Generate test JWT at https://jwt.io with:
# # - Algorithm: RS256
# # - Payload: {"iss":"https://auth.example.com/","aud":"https://api.example.com","exp":9999999999}
# # - Paste contents of jwt_private.pem in private key field
#
# # Test with valid token
# TOKEN="eyJhbGc..." # Replace with your generated token
# curl -H "Authorization: Bearer $TOKEN" -H "Host: api.example.local" http://localhost:8080
# # Expected: 200 OK with "Protected API - JWT Required"
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f jwt-validator.yml
# ```
#
# ==============================================================================
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: default
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: api
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: byjg/static-httpserver
ports:
- containerPort: 8080
env:
- name: TITLE
value: "Protected API - JWT Required"
resources:
limits:
cpu: '0.1'
memory: '64Mi'
requests:
cpu: '0.05'
memory: '32Mi'
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# Enable JWT validator plugin
easyhaproxy.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
name: api-ingress-jwt
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: api.example.local
http:
paths:
- backend:
service:
name: api-service
port:
number: 8080
pathType: ImplementationSpecific

View file

@ -0,0 +1,278 @@
# ==============================================================================
# EXAMPLE: Multiple Plugins Combined for Kubernetes
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Using multiple security plugins together
# - Different plugin combinations for different services
# - Layered security approach in Kubernetes
# - Three services with different security profiles:
# 1. Public website: Cloudflare + path blocking
# 2. Protected API: JWT validation + path blocking
# 3. Admin panel: Strict IP whitelist
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. Generate JWT keys (idempotent - skips if exists)
# [ -f jwt_private.pem ] || openssl genrsa -out jwt_private.pem 2048
# [ -f jwt_pubkey.pem ] || openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
# kubectl create configmap jwt-keys --from-file=api_pubkey.pem=jwt_pubkey.pem
#
# # 3. Download Cloudflare IPs
# curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
# kubectl create configmap cloudflare-ips \
# --from-file=cloudflare_ips.lst=cloudflare_ips.lst \
# -n easyhaproxy
#
# # 4. Mount ConfigMaps in EasyHAProxy deployment
# # (See individual plugin examples for mount configuration)
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f plugins-combined.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check all resources are created
# kubectl get deployment,service,ingress
#
# # Test public website (Cloudflare + path blocking)
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: website.example.local" http://localhost:8080
# # Expected: 200 OK with "Public Website"
# curl -H "Host: website.example.local" http://localhost:8080/admin
# # Expected: HTTP 404 - Path blocked
#
# # Test protected API (JWT required)
# curl -H "Host: api.example.local" http://localhost:8080
# # Expected: HTTP 403 - Missing Authorization header
#
# # Test admin panel (IP whitelist)
# curl -H "Host: admin.example.local" http://localhost:8080
# # Expected: 200 OK from allowed IP, or HTTP 403 from blocked IP
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f plugins-combined.yml
# ```
#
# ==============================================================================
---
# Public website service
apiVersion: v1
kind: Service
metadata:
name: website-service
namespace: default
spec:
ports:
- port: 8080
selector:
app: website
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: website
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: website
template:
metadata:
labels:
app: website
spec:
containers:
- name: website
image: byjg/static-httpserver
env:
- name: TITLE
value: "Public Website"
resources:
requests:
cpu: '0.05'
memory: '32Mi'
---
# Public website ingress with Cloudflare + path blocking
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# Cloudflare IP restoration + deny pages
easyhaproxy.plugins: "cloudflare,deny_pages"
easyhaproxy.plugin.deny_pages.paths: "/admin,/wp-admin,/wp-login.php,/.env,/config"
easyhaproxy.plugin.deny_pages.status_code: "404"
name: website-ingress
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: website.example.local
http:
paths:
- backend:
service:
name: website-service
port:
number: 8080
pathType: ImplementationSpecific
---
# API service
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: default
spec:
ports:
- port: 8080
selector:
app: api
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: default
spec:
replicas: 5
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: byjg/static-httpserver
env:
- name: TITLE
value: "Protected API"
resources:
requests:
cpu: '0.05'
memory: '32Mi'
---
# API ingress with JWT + path blocking
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# JWT validation + block internal endpoints
easyhaproxy.plugins: "jwt_validator,deny_pages"
# JWT config
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
# Block internal paths
easyhaproxy.plugin.deny_pages.paths: "/internal,/debug,/metrics"
easyhaproxy.plugin.deny_pages.status_code: "403"
name: api-ingress
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: api.example.local
http:
paths:
- backend:
service:
name: api-service
port:
number: 8080
pathType: ImplementationSpecific
---
# Admin service
apiVersion: v1
kind: Service
metadata:
name: admin-service
namespace: default
spec:
ports:
- port: 8080
selector:
app: admin
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: admin
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: admin
template:
metadata:
labels:
app: admin
spec:
containers:
- name: admin
image: byjg/static-httpserver
env:
- name: TITLE
value: "Admin Panel"
resources:
requests:
cpu: '0.05'
memory: '32Mi'
---
# Admin ingress with strict IP whitelist
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# IP whitelist only (strictest security)
easyhaproxy.plugins: "ip_whitelist"
# UPDATE with your office/VPN IPs!
easyhaproxy.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,10.0.0.0/8"
easyhaproxy.plugin.ip_whitelist.status_code: "403"
name: admin-ingress
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: admin.example.local
http:
paths:
- backend:
service:
name: admin-service
port:
number: 8080
pathType: ImplementationSpecific

View file

@ -0,0 +1,137 @@
# ==============================================================================
# EXAMPLE: Basic Kubernetes Ingress
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Basic ingress configuration with EasyHAProxy
# - Multiple domains pointing to the same service
# - Complete deployment + service + ingress setup
# - HTTP ingress without TLS
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. Label the node where EasyHAProxy will run
# kubectl label nodes <node-name> "easyhaproxy/node=master"
#
# # 3. Add to /etc/hosts for local testing (idempotent)
# grep -q "example.org" /etc/hosts || echo "<node-ip> example.org www.example.org" | sudo tee -a /etc/hosts
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f service.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check resources are created
# kubectl get deployment,service,ingress container-example
#
# # Test via node IP
# curl -H "Host: example.org" http://<node-ip>:31080
# # Expected: 200 OK with "My Host Example"
#
# # Or use port-forward for testing
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: example.org" http://localhost:8080
# # Expected: 200 OK with "My Host Example"
#
# # Test second domain
# curl -H "Host: www.example.org" http://<node-ip>:31080
# # Expected: Same response
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f service.yml
# ```
#
# ==============================================================================
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: container-example
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
rules:
- host: example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: container-example
port:
number: 8080
- host: www.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: container-example
port:
number: 8080
---
apiVersion: v1
kind: Service
metadata:
name: container-example
namespace: default
spec:
ports:
- name: http
port: 8080
selector:
app: container-example
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: container-example
namespace: default
spec:
replicas: 1
revisionHistoryLimit: 10
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: container-example
template:
metadata:
labels:
app: container-example
spec:
containers:
- name: container-example
image: byjg/static-httpserver
ports:
- containerPort: 8080
resources:
limits:
cpu: '0.05'
memory: '20Mi'
requests:
cpu: '0.05'
memory: '20Mi'
env:
- name: TITLE
value: "My Host Example"

View file

@ -0,0 +1,143 @@
# ==============================================================================
# EXAMPLE: TLS/SSL Kubernetes Ingress
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - HTTPS/TLS configuration with custom certificates
# - Kubernetes TLS secrets for SSL certificates
# - Complete deployment + service + secret + ingress with TLS
# - Using pre-generated test certificates
#
# REQUIREMENTS (run these first):
# ```bash
# # 1. Ensure EasyHAProxy is installed in your cluster
# kubectl create namespace easyhaproxy
# kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml
#
# # 2. Label the node where EasyHAProxy will run
# kubectl label nodes <node-name> "easyhaproxy/node=master"
#
# # 3. Add to /etc/hosts for local testing (idempotent)
# grep -q "host2.local" /etc/hosts || echo "<node-ip> host2.local" | sudo tee -a /etc/hosts
#
# # Note: This example uses embedded test certificates
# # For production, create your own secret:
# # kubectl create secret tls host2-tls --cert=cert.crt --key=cert.key
# ```
#
# HOW TO START:
# ```bash
# kubectl apply -f service_tls.yml
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check resources are created
# kubectl get deployment,service,ingress,secret tls-example
# kubectl get secret host2-tls
#
# # Test HTTPS (using port-forward)
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8443:443
# curl -k -H "Host: host2.local" https://localhost:8443
# # Expected: 200 OK with "My Host Example"
#
# # Verify certificate
# openssl s_client -showcerts -connect localhost:8443 -servername host2.local < /dev/null
# # Expected: Certificate for host2.local
# ```
#
# CLEAN UP:
# ```bash
# kubectl delete -f service_tls.yml
# ```
#
# ==============================================================================
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-example
namespace: default
spec:
# Use ingressClassName instead of the deprecated annotation
# For backward compatibility, annotation kubernetes.io/ingress.class is still supported
ingressClassName: easyhaproxy
tls:
- hosts:
- host2.local
secretName: host2-tls
rules:
- host: host2.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tls-example
port:
number: 8080
---
apiVersion: v1
kind: Secret
metadata:
name: host2-tls
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxVENDQXBHZ0F3SUJBZ0lVSWQ1Yjl0OXVxSDc4ZzAyRXpiV0Y2RktWdzNnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pERUxNQWtHQTFVRUJoTUNRbEl4RnpBVkJnTlZCQWdNRGxKcGJ5QmtaU0JLWVc1bGFYSnZNUmN3RlFZRApWUVFIREE1U2FXOGdaR1VnU21GdVpXbHliekVOTUFzR0ExVUVDZ3dFUVVOTlJURVVNQklHQTFVRUF3d0xhRzl6CmRESXViRzlqWVd3d0hoY05Nakl3T0RFMU1EUXlOekExV2hjTk1qTXdPREUxTURReU56QTFXakJrTVFzd0NRWUQKVlFRR0V3SkNVakVYTUJVR0ExVUVDQXdPVW1sdklHUmxJRXBoYm1WcGNtOHhGekFWQmdOVkJBY01EbEpwYnlCawpaU0JLWVc1bGFYSnZNUTB3Q3dZRFZRUUtEQVJCUTAxRk1SUXdFZ1lEVlFRRERBdG9iM04wTWk1c2IyTmhiRENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTVNLdnJPYWhhdkNYbnZTRjUxMzFocG8KNms2NUM1N2pnUlE4NEZhRGo1TWJKT1ZsWVFWRnRNRzBYT2s3YStoaDV2MWZlNHdIMFI3STZGRG8wVjlzUytzcwprbzVic0VsYzF4WWxnNUhidUtxODl2UlNLZzZFRGx6dHgzQktiaTkxMlBtdDV2RkdOSjE2emN3NzdEVXJRSVhvCjRJL2I0YTNwbUJpV2o0M05vVElybVNXSHRzR3d3T2ozaUR2U3dlcWRZWEpJcjNocEhINXU2cG9oakRvUXZxRHoKSzZNdThwNm1oQ1VLTnM3S0ZKbk5Jbk5HMjVvUVQ2TzBuNE9HdG1nUmpMV29wZEVuT2hNa0tzZklvSTFYdGxYQgpMQkR2N2h1SUNrM3Q1eXd0ZkNReU8wOWtYN2xGSWdkNXJuNytNandINVdOZXFiUUp4dWFxam9YUW5OWlVnVXNDCkF3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQjhHQTFVZEl3UVkKTUJhQUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSQpodmNOQVFFTEJRQURnZ0VCQUptdWR2eDgrcDVpSVVzVDhmbS9mYlZNMERBNnFXQUxEWVVKblRuM2o2THE0dnBmClBGQytxMUxtdVdmQlFNeXFLckhyUDNlNDkzRWN0WG9pU0taTzZpTjVkVkpJdXIwMk9qR3VpQUVjc1l1WTFuTG4KczlwaWlJK1VFd3hINnV4MU5hSFVuenNXYXVvQnZSaHpqWHZPNlNBVlNaSllhOWRZNW1pelhrbER5RE51RzVVMApsWHY5ZWdNR0JzeTBkRzZlRlhrVTVDUGR4V1U1NDB5STJzQ3RTQWo3eitXUlVENWs3Z0o3dFZvWTMvL2pIUVpHCjVTVFRtbTV0OWtwSVpUV2twdHlKb3M5b1pKRllNSVhxVzJGYzZ0eUxacFJwMzFSNzh0RHM2RVRJa1RvRGMwUlIKano2NnRoNkhJK1psZ0lCUWh3MDkraFlBaEJEZTkrRG1kL1N6UVpjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRREVpcjZ6bW9XcndsNTcKMGhlZGQ5WWFhT3BPdVF1ZTQ0RVVQT0JXZzQrVEd5VGxaV0VGUmJUQnRGenBPMnZvWWViOVgzdU1COUVleU9oUQo2TkZmYkV2ckxKS09XN0JKWE5jV0pZT1IyN2lxdlBiMFVpb09oQTVjN2Nkd1NtNHZkZGo1cmVieFJqU2RlczNNCk8rdzFLMENGNk9DUDIrR3Q2WmdZbG8rTnphRXlLNWtsaDdiQnNNRG85NGc3MHNIcW5XRnlTSzk0YVJ4K2J1cWEKSVl3NkVMNmc4eXVqTHZLZXBvUWxDamJPeWhTWnpTSnpSdHVhRUUranRKK0RoclpvRVl5MXFLWFJKem9USkNySAp5S0NOVjdaVndTd1E3KzRiaUFwTjdlY3NMWHdrTWp0UFpGKzVSU0lIZWE1Ky9qSThCK1ZqWHFtMENjYm1xbzZGCjBKeldWSUZMQWdNQkFBRUNnZ0VCQUtjZWl0VlJPUVE1ZS9teFJSOUNmSzFzTkgvSDNOZTMvMVBrQjZYSXJGYWIKcUIzZXZFYXRaT3VvbjdBNk5LRWVUamwzN1NlK3BkU1ZaT1VYY3FDL0J6YnJhWnJlMytFaHJrcElqNzJBcFYrWQoyaXdaaVdWYWFKUWdJNHVaM21OQXc4UmFXSnNqNVMxYTlJOExET2lRNUlaNDVDbXZBQkRQSmVNU2N2SlN2UlJZCmU1TjBMNnN0cVM3WitJb3lHVktVZnAxaU5PMFl5eXdPVWlTa0lSWGdzY3VSWFpHWXBpR1BvbXNKK0pzMWVqelcKanlTdGxaSkVyNEwxMjg1ckdQcm1IcWpUd0ZkK2hHODBXYzQxNzl4TCtXUkU2SEJFVVpTaXk5NWZlNmtjUEhYWApCZ2lWWXRjRkttaUJpMmRUYnhsNGU5NHV0MjM5aTBIdGxKMVpKdExoK0JFQ2dZRUE4YTI5OEsyelhrSG9zeGhOCnRSckg3WGZNUFRrSEREZDNyeE0yMUxUK2ZJWHFpbkdVcDlMWWFEY2JqdVRQczhlMzN1S01kN1I2UTQweDg5eVcKSVhOa2EvVkwwUFhVZVY2N2FDVkxMcWdYRExHdWRsdUppbkgwWHZtSTBDbUJlY0ZTTXFJRm1RbGdxRVJvR0dzMwpVTWFjMHA4NzZUNFhrR1FRSmRmNjJiRnBFNFVDZ1lFQTBEQkgwUERsT3B3WGNjRGdYck1mYXlyOEhBaEkrRzVSCnlXUS8vOWlpcnRVODNjaHdJV3draDUzZUxMTXpMZ2RxSm5QaVd5YVVXNUJxem1ZdUQyM25oeGNRN1BOZElxT08KSDFzRTZ6cUxOc2h2NDZ0NVFLbGgxUTRxamQ3VXF0Z3JTclk2M1JYSkNNV1R3bk5NZURMdGo4Z2FLYmprckczUgpCTTJpbGR0NlVvOENnWUJYN05EVWxpMVNsb0gxWGxzdkQwNDdTOEZIYU03eWw5OTRGM0owVW1EZnBzemNqMVA0CjlwRjY0TW1xNC8zWXQwbGkwbU11VGIvSmdiM3hyWWdGSlhrY2VjS2FoRVZIM3JvcHVwK3Vtc0xBQUlpclVNUXEKVlNrRndKMFF0bmovZGVEVXdQTnVhT1g4Y2Q2NU81Q0ZWNnpJUjl4QkVERDhmQnNQMlpMT3ptZWZEUUtCZ1FERgptMjR2VnRoZC8xY0pkQ2dEKzBWeE5ZWERIZUlWWExGbzFTMGlMWUNOTG4zdGpabFJRQktVWHpaSmUzYXkwL3JmCnNOTkQ3YVNZSE1Za1R6eWRESmJjMVBvTnp4bXlEVWlUWHBPV3F5VUV4TS9mYkIxVlVQRTVoNDdBeHFkWjJvR04KRXRkZ2pwTVpMbUNJQzJTa0dzTCszTkpvazhVS0hkcHVFcm1tUUlNazVRS0JnUUNnRVdjWXRMWEMzWURZTUZkSQpVZ2NUZWJGcVNzM21MWWd1YjF4ZWtXM0lYUjJ5b200VjVmUVRMaUY3WWZuMmRwRFc0SWNNVTBVSkZZVnNVbGhLCmFHdGV0NFZtNU5uOCtNZ2hvdDV5QWpxTzl5QVVhdWI3d2dpZktJZTk5dFFLZDh1WnlDdkowaGh2bUREU2Z4NG0KQi9URWlGQU85OXlGNDlpU3hFVlNBUzZwcVE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
type: kubernetes.io/tls
---
apiVersion: v1
kind: Service
metadata:
name: tls-example
namespace: default
spec:
ports:
- name: http
port: 8080
selector:
app: tls-example
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tls-example
namespace: default
spec:
replicas: 1
revisionHistoryLimit: 10
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: tls-example
template:
metadata:
labels:
app: tls-example
spec:
containers:
- name: tls-example
image: byjg/static-httpserver
ports:
- containerPort: 8080
resources:
limits:
cpu: '0.05'
memory: '20Mi'
requests:
cpu: '0.05'
memory: '20Mi'
env:
- name: TITLE
value: "My Host Example"

View file

@ -0,0 +1,169 @@
#!/bin/bash
set -e
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
CLUSTER_NAME="easyhaproxy-test"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="${SCRIPT_DIR}/.kind"
KIND_BIN="${BIN_DIR}/kind"
KUBECTL_BIN="${BIN_DIR}/kubectl"
HELM_BIN="${BIN_DIR}/helm"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# Port configuration (matches test_kubernetes.py)
HTTP_PORT=10080
HTTPS_PORT=10443
STATS_PORT=11936
echo -e "${BLUE}[1/9] Setting up kind cluster '${CLUSTER_NAME}'...${NC}"
# Ensure kind is installed
if [ ! -f "${KIND_BIN}" ]; then
echo "Installing kind locally..."
mkdir -p "${BIN_DIR}"
curl -Lo "${KIND_BIN}" "https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64"
chmod +x "${KIND_BIN}"
echo -e "${GREEN}✓ kind installed to ${KIND_BIN}${NC}"
fi
# Ensure kubectl is installed
if ! command -v kubectl &> /dev/null; then
if [ ! -f "${KUBECTL_BIN}" ]; then
echo "Installing kubectl locally..."
mkdir -p "${BIN_DIR}"
VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -Lo "${KUBECTL_BIN}" "https://dl.k8s.io/release/${VERSION}/bin/linux/amd64/kubectl"
chmod +x "${KUBECTL_BIN}"
echo -e "${GREEN}✓ kubectl installed to ${KUBECTL_BIN}${NC}"
fi
KUBECTL="${KUBECTL_BIN}"
else
KUBECTL="kubectl"
fi
# Ensure helm is installed
if ! command -v helm &> /dev/null; then
if [ ! -f "${HELM_BIN}" ]; then
echo "Installing helm locally..."
mkdir -p "${BIN_DIR}"
HELM_VERSION="v3.13.3"
HELM_TAR="${BIN_DIR}/helm.tar.gz"
curl -Lo "${HELM_TAR}" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
tar -xzf "${HELM_TAR}" -C "${BIN_DIR}" --strip-components=1 linux-amd64/helm
rm "${HELM_TAR}"
chmod +x "${HELM_BIN}"
echo -e "${GREEN}✓ helm installed to ${HELM_BIN}${NC}"
fi
HELM="${HELM_BIN}"
else
HELM="helm"
fi
# Check if cluster already exists
echo -e "${BLUE}[1/9] Checking for existing cluster...${NC}"
if ${KIND_BIN} get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo -e "${BLUE}Cluster '${CLUSTER_NAME}' already exists, deleting it first...${NC}"
${KIND_BIN} delete cluster --name "${CLUSTER_NAME}"
fi
# Create cluster config
echo -e "${BLUE}[1/9] Writing cluster config...${NC}"
CLUSTER_CONFIG="${BIN_DIR}/cluster-config.yaml"
mkdir -p "${BIN_DIR}"
cat > "${CLUSTER_CONFIG}" <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 80
hostPort: ${HTTP_PORT}
protocol: TCP
- containerPort: 443
hostPort: ${HTTPS_PORT}
protocol: TCP
- containerPort: 1936
hostPort: ${STATS_PORT}
protocol: TCP
EOF
# Create cluster
echo -e "${BLUE}[2/9] Creating kind cluster (this may take 1-2 minutes)...${NC}"
${KIND_BIN} create cluster --name "${CLUSTER_NAME}" --config "${CLUSTER_CONFIG}"
# Set kubectl context
echo -e "${BLUE}[3/9] Setting kubectl context...${NC}"
${KUBECTL} config use-context "kind-${CLUSTER_NAME}"
# Wait for nodes to be ready
echo -e "${BLUE}[3/9] Waiting for cluster nodes to be ready...${NC}"
${KUBECTL} wait --for=condition=Ready nodes --all --timeout=30s
echo -e "${GREEN}✓ kind cluster '${CLUSTER_NAME}' is ready${NC}"
# Build and load local EasyHAProxy image
echo -e "${BLUE}[4/9] Building local EasyHAProxy image (may take 30-60s)...${NC}"
docker build -t byjg/easy-haproxy:local \
-f "${PROJECT_ROOT}/build/Dockerfile" \
"${PROJECT_ROOT}"
echo -e "${BLUE}[5/9] Loading image into kind cluster (may take 10-20s)...${NC}"
${KIND_BIN} load docker-image byjg/easy-haproxy:local --name "${CLUSTER_NAME}"
# Generate EasyHAProxy manifest using Helm
echo -e "${BLUE}[6/9] Generating EasyHAProxy manifest from Helm...${NC}"
HELM_DIR="${PROJECT_ROOT}/helm"
MANIFEST_PATH="${BIN_DIR}/easyhaproxy-local.yml"
${HELM} template ingress "${HELM_DIR}/easyhaproxy" \
--namespace easyhaproxy \
--set service.create=false \
--set image.tag=local \
--set image.pullPolicy=Never \
> "${MANIFEST_PATH}"
# Install EasyHAProxy
echo -e "${BLUE}[7/9] Creating easyhaproxy namespace...${NC}"
${KUBECTL} create namespace easyhaproxy
echo -e "${BLUE}[7/9] Applying EasyHAProxy manifest...${NC}"
${KUBECTL} apply -f "${MANIFEST_PATH}"
# Label the control-plane node
echo -e "${BLUE}[8/9] Labeling control-plane node...${NC}"
${KUBECTL} label nodes "${CLUSTER_NAME}-control-plane" \
"easyhaproxy/node=master" --overwrite
# Wait for EasyHAProxy to be ready
echo -e "${BLUE}[9/9] Waiting for EasyHAProxy pods to be ready...${NC}"
if ${KUBECTL} wait --for=condition=Ready pods \
-n easyhaproxy -l "app.kubernetes.io/name=easyhaproxy" \
--timeout=30s 2>/dev/null; then
echo -e "${GREEN}✓ EasyHAProxy pods are ready${NC}"
else
echo -e "${RED}✗ Pods not ready within 30s. Checking status...${NC}"
${KUBECTL} get pods -n easyhaproxy -o wide
echo -e "\n${BLUE}Events:${NC}"
${KUBECTL} get events -n easyhaproxy --sort-by=.lastTimestamp
exit 1
fi
echo -e "${GREEN}✓ All setup complete! Cluster is ready.${NC}"
echo ""
echo -e "${BLUE}Cluster Information:${NC}"
echo -e " Cluster name: ${CLUSTER_NAME}"
echo -e " HTTP port: localhost:${HTTP_PORT}"
echo -e " HTTPS port: localhost:${HTTPS_PORT}"
echo -e " Stats port: localhost:${STATS_PORT}"
echo ""
echo -e "${BLUE}Useful commands:${NC}"
echo -e " Apply example ingress: ${KUBECTL} apply -f ${SCRIPT_DIR}/service.yml"
echo -e " Check EasyHAProxy logs: ${KUBECTL} logs -n easyhaproxy -l app.kubernetes.io/name=easyhaproxy -f"
echo -e " Test with curl: curl -H 'Host: example.org' http://localhost:${HTTP_PORT}"
echo -e " Delete cluster: ${SCRIPT_DIR}/teardown-cluster.sh"
echo ""

View file

@ -0,0 +1,39 @@
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
CLUSTER_NAME="easyhaproxy-test"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="${SCRIPT_DIR}/.kind"
KIND_BIN="${BIN_DIR}/kind"
# Check if kind binary exists
if [ ! -f "${KIND_BIN}" ]; then
# Try to use system kind
if command -v kind &> /dev/null; then
KIND_BIN="kind"
else
echo -e "${RED}✗ kind binary not found. Cannot delete cluster.${NC}"
echo " Cluster may not exist or kind is not installed."
exit 1
fi
fi
# Check if cluster exists
if ! ${KIND_BIN} get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo -e "${BLUE}Cluster '${CLUSTER_NAME}' does not exist. Nothing to delete.${NC}"
exit 0
fi
echo -e "${BLUE}Deleting kind cluster '${CLUSTER_NAME}'...${NC}"
if ${KIND_BIN} delete cluster --name "${CLUSTER_NAME}"; then
echo -e "${GREEN}✓ Cluster deleted successfully${NC}"
else
echo -e "${RED}✗ Failed to delete cluster${NC}"
exit 1
fi

View file

@ -0,0 +1,97 @@
# Static Configuration Example
Self-contained example for EasyHAProxy using static YAML configuration. **All documentation is in the docker-compose file as header comments.**
## Quick Start
1. Open [docker-compose.yml](docker-compose.yml)
2. Read the header comments for complete instructions
3. Choose a configuration scenario from `conf/` directory
4. Run the commands step-by-step
## What is Static Mode?
Static mode uses explicit YAML configuration files instead of dynamic service discovery. This is useful for:
- **Non-containerized backends** - VMs, bare metal servers, external APIs
- **Fixed infrastructure** - When your backend IPs/ports don't change
- **Explicit routing control** - Precise control over HAProxy configuration
## Configuration Files
All scenarios use `/etc/haproxy/static/config.yml` mounted from `./conf/config.yml`.
Choose one of these pre-made configurations:
| Configuration File | Description |
|----------------------------|-------------------------------------------------|
| `config-basic.yml` | Simple HTTP→HTTPS redirect with SSL termination |
| `config-certbot.yml` | Let's Encrypt SSL (requires public domain) |
| `config-deny-pages.yml` | Block specific paths (e.g., `/admin`, `/.env`) |
| `config-jwt-validator.yml` | JWT token validation for API authentication |
## Prerequisites
- SSL certificates generated (`./tests_e2e/generate-keys.sh`)
- `/etc/hosts` entry for `host1.local`
- Backend container running on port 8080
See header comments in [docker-compose.yml](docker-compose.yml) for detailed setup.
## Documentation Structure
The docker-compose.yml file contains:
- **WHAT THIS DEMONSTRATES** - Key features and concepts
- **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times)
- **HOW TO START** - Commands to start backend and EasyHAProxy
- **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs
- **CLEAN UP** - Commands to stop and remove resources
## Example Workflow
```bash
# 1. Generate certificates
cd ../.. && ./tests_e2e/generate-keys.sh && cd tests_e2e/static
# 2. Choose a configuration
cp conf/config-basic.yml conf/config.yml
# 3. Start backend
docker run -d --name container -p 8080:8080 byjg/static-httpserver
# 4. Start EasyHAProxy
docker compose up -d
# 5. Test
curl -k https://host1.local/
# 6. Clean up
docker compose down
docker stop container && docker rm container
```
## Configuration File Reference
Basic structure of `config.yml`:
```yaml
stats:
username: admin
password: password
port: 1936
easymapping:
- port: 443
ssl: true
hosts:
host1.local:
containers:
- container:8080 # Can also be IP:PORT for external backends
```
See `conf/` directory for complete examples.
## Additional Documentation
- [Static Configuration Guide](../../docs/static.md)
- [Using Plugins](../../docs/plugins/)
- [Environment Variables](../../docs/environment-variable.md)

View file

@ -0,0 +1,31 @@
# Basic Static Configuration Example
#
# This is a minimal configuration without plugins
# Demonstrates basic HTTP to HTTPS redirect and SSL setup
#
# To use:
# 1. Update the container name and ports to match your setup
# 2. Place SSL certificate at /certs/haproxy/host1.local.pem
# 3. Mount this config: -v ./conf/config-basic.yml:/etc/haproxy/static/config.yml
stats:
username: admin
password: password
port: 1936 # Optional (default 1936)
customerrors: true # Optional (default false)
easymapping:
# HTTP - Redirect to HTTPS
- port: 80
redirect:
host1.local: https://host1.local
www.host1.local: https://host1.local
# HTTPS - Serve application
- port: 443
ssl: true
hosts:
host1.local:
containers:
- container:8080

View file

@ -0,0 +1,88 @@
# Certbot/Let's Encrypt Configuration Example
#
# Demonstrates:
# - Automatic SSL certificate generation with Let's Encrypt
# - HTTP to HTTPS redirect
# - Certificate renewal
#
# Prerequisites:
# 1. Public IP address with ports 80 and 443 accessible
# 2. DNS records pointing to your server:
# example.com -> your-server-ip
# www.example.com -> your-server-ip
#
# 3. Set environment variable:
# EASYHAPROXY_CERTBOT_EMAIL=your-email@example.com
#
# 4. Mount this config:
# -v ./conf/config-certbot.yml:/etc/haproxy/static/config.yml
#
# 5. Persist certificates:
# -v ./certs/certbot:/certs/certbot
#
# How it works:
# - EasyHAProxy requests certificates from Let's Encrypt via HTTP-01 challenge
# - Certificates are stored in /certs/certbot/
# - Certificates auto-renew when needed
#
# Note: Let's Encrypt has rate limits. Use staging environment for testing:
# EASYHAPROXY_CERTBOT_AUTOCONFIG=staging
stats:
username: admin
password: password
port: 1936
customerrors: true
easymapping:
# HTTP Port 80
# Required for ACME HTTP-01 challenge and redirect
- port: 80
hosts:
# Domain with certbot enabled
example.com:
containers:
- webapp:8080
# Enable certbot for this domain
certbot: true
# Redirect HTTP to HTTPS after cert is issued
redirect_ssl: true
# Additional domain with certbot
app.example.com:
containers:
- app:3000
certbot: true
redirect_ssl: true
# Domain without certbot (uses custom certificate)
custom.example.com:
containers:
- custom-app:8080
# No certbot - expects certificate at /certs/haproxy/custom.example.com.pem
# HTTPS Port 443
# Serves HTTPS traffic with auto-generated certificates
- port: 443
ssl: true
hosts:
example.com:
containers:
- webapp:8080
# Certificate path (auto-generated by certbot)
# /certs/certbot/example.com/fullchain.pem
app.example.com:
containers:
- app:3000
# Custom certificate example
custom.example.com:
containers:
- custom-app:8080
# Place your certificate at:
# /certs/haproxy/custom.example.com.pem
# Multiple domains with different backends
# Certbot will request separate certificates for each domain

View file

@ -0,0 +1,78 @@
# Deny Pages Plugin Configuration Example
#
# Demonstrates:
# - Global plugin configuration (applies to all domains)
# - Per-domain plugin override (custom settings per host)
#
# To use:
# 1. Update container names and ports
# 2. Mount this config: -v ./conf/config-deny-pages.yml:/etc/haproxy/static/config.yml
# 3. Test blocked paths:
# curl http://host1.local/admin # Should return 404
# curl http://host2.local/wp-admin # Should return 403 (different config)
stats:
username: admin
password: password
port: 1936
customerrors: true
# Global plugin configuration
# This applies to ALL domains unless overridden
plugins:
enabled:
- deny_pages
config:
deny_pages:
# Global default: block common admin paths with 404
paths:
- /admin
- /.env
- /config
status_code: 404 # Hide existence of these paths
easymapping:
- port: 80
hosts:
# Domain 1: Uses global deny_pages configuration
host1.local:
containers:
- webapp1:8080
# No plugins specified = uses global configuration
# Domain 2: WordPress site with custom blocked paths
host2.local:
containers:
- wordpress:80
# Override global plugin configuration for this domain
plugins:
- deny_pages
plugin_config:
deny_pages:
paths:
- /wp-admin
- /wp-login.php
- /xmlrpc.php
- /wp-config.php
status_code: 403 # Return forbidden instead of 404
# Domain 3: Public site with stricter blocking
host3.local:
containers:
- publicsite:3000
plugins:
- deny_pages
plugin_config:
deny_pages:
paths:
- /admin
- /administrator
- /manager
- /phpmyadmin
- /.git
- /.env
- /config
- /backup
status_code: 404

View file

@ -0,0 +1,86 @@
# JWT Validator Plugin Configuration Example
#
# Demonstrates:
# - JWT token validation for API protection
# - Different JWT configurations per domain
# - Optional issuer/audience validation
#
# Prerequisites:
# 1. Generate RSA key pair:
# openssl genrsa -out jwt_private.pem 2048
# openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
#
# 2. Mount public keys:
# -v ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
# -v ./jwt_pubkey2.pem:/etc/haproxy/jwt_keys/admin_pubkey.pem:ro
#
# 3. Mount this config:
# -v ./conf/config-jwt-validator.yml:/etc/haproxy/static/config.yml
#
# 4. Test:
# # Without token - should fail
# curl http://api.local/users
# # Response: Missing Authorization HTTP header
#
# # With valid token - should succeed
# curl -H "Authorization: Bearer eyJhbGc..." http://api.local/users
stats:
username: admin
password: password
port: 1936
customerrors: true
easymapping:
- port: 80
hosts:
# Public API with full JWT validation
api.local:
containers:
- api-server:8080
plugins:
- jwt_validator
plugin_config:
jwt_validator:
algorithm: RS256
issuer: https://auth.example.com/
audience: https://api.example.com
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
# Internal API - validate signature only (no issuer/audience check)
internal-api.local:
containers:
- internal-api:3000
plugins:
- jwt_validator
plugin_config:
jwt_validator:
algorithm: RS256
# No issuer/audience = skip those validations
pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
# Admin API - different issuer and key
admin-api.local:
containers:
- admin-api:4000
plugins:
- jwt_validator
- deny_pages # Also block internal paths
plugin_config:
jwt_validator:
algorithm: RS256
issuer: https://admin-auth.example.com/
audience: https://admin.example.com
pubkey_path: /etc/haproxy/jwt_keys/admin_pubkey.pem
deny_pages:
paths:
- /internal
- /debug
status_code: 403
# Public website - no JWT required
website.local:
containers:
- website:8080
# No plugins = public access

View file

@ -0,0 +1,76 @@
# ==============================================================================
# EXAMPLE: Static Configuration Mode
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - EasyHAProxy using static YAML configuration (no service discovery)
# - Useful for non-containerized backends, VMs, or bare metal servers
# - Configuration via /etc/haproxy/static/config.yml
#
# REQUIREMENTS (run these first):
# ```bash
# # Generate SSL certificates
# cd ../.. && ./tests_e2e/generate-keys.sh && cd tests_e2e/static
#
# # Add to /etc/hosts (idempotent)
# grep -q "host1.local" /etc/hosts || echo "127.0.0.1 host1.local" | sudo tee -a /etc/hosts
#
# # Copy a configuration file (choose one):
# cp conf/config-basic.yml conf/config.yml # Basic HTTP→HTTPS redirect
# # OR
# cp conf/config-certbot.yml conf/config.yml # Let's Encrypt (requires public domain)
# # OR
# cp conf/config-deny-pages.yml conf/config.yml # Block specific paths
# # OR
# cp conf/config-jwt-validator.yml conf/config.yml # JWT authentication
# ```
#
# HOW TO START:
# ```bash
# # Start backend container
# docker run -d --name container -p 8080:8080 byjg/static-httpserver
#
# # Start EasyHAProxy
# docker compose up -d
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Test HTTPS
# curl -k -H "Host: host1.local" https://127.0.0.1/
# # Expected: 200 OK with "Hello from Static HTTP Server!"
#
# # Test HTTP redirect (if using basic config)
# curl -I -H "Host: host1.local" http://127.0.0.1
# # Expected: HTTP/1.1 301 Moved Permanently
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker compose down
# docker stop container && docker rm container
# ```
#
# ==============================================================================
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- ./conf/:/etc/haproxy/static/
- ./host1.local.pem:/certs/haproxy/host1.local.pem
- /var/run/docker.sock:/var/run/docker.sock
environment:
EASYHAPROXY_DISCOVER: static
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
container:
image: byjg/static-httpserver

67
tests_e2e/swarm/README.md Normal file
View file

@ -0,0 +1,67 @@
# Docker Swarm Examples
Self-contained examples for EasyHAProxy in Docker Swarm mode. **All documentation is in the YAML files as header comments.**
## Quick Start
1. Pick an example below
2. Open the YAML file
3. Read the header comments for complete instructions
4. Run the commands step-by-step
## Prerequisites
All examples require:
- Docker Swarm initialized (`docker swarm init`)
- Overlay network created (`docker network create --driver overlay --attachable easyhaproxy`)
- EasyHAProxy deployed (`docker stack deploy -c easyhaproxy.yml easyhaproxy`)
See header comments in each file for detailed setup instructions.
## Basic Examples
| File | Description |
|------|-------------|
| [easyhaproxy.yml](easyhaproxy.yml) | EasyHAProxy service for Swarm with stats and certbot |
| [services.yml](services.yml) | Basic services with SSL (embedded cert and file-based) |
| [portainer.yml](portainer.yml) | Portainer management UI behind EasyHAProxy |
## Plugin Examples
| File | Description |
|------|-------------|
| [jwt-validator.yml](jwt-validator.yml) | JWT token validation for API protection |
| [ip-whitelist.yml](ip-whitelist.yml) | IP whitelist for admin panels or sensitive services |
| [cloudflare.yml](cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN |
| [plugins-combined.yml](plugins-combined.yml) | Multiple plugins combined for layered security |
## Documentation Structure
Each YAML file contains:
- **WHAT THIS DEMONSTRATES** - Key features and concepts
- **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times)
- **HOW TO START** - Command to deploy the stack
- **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs
- **CLEAN UP** - Commands to remove resources
## Important: Service Labels
In Swarm mode, labels must be under `deploy.labels`, NOT top-level `labels`:
```yaml
# ✅ CORRECT - Service labels
deploy:
labels:
easyhaproxy.http.host: example.com
# ❌ WRONG - Container labels (ignored in Swarm)
labels:
easyhaproxy.http.host: example.com
```
## Additional Documentation
- [Docker Swarm Guide](../../docs/swarm.md)
- [Container Labels Reference](../../docs/container-labels.md)
- [Using Plugins](../../docs/plugins/)
- [ACME/Let's Encrypt](../../docs/acme.md)

View file

@ -0,0 +1,122 @@
# ==============================================================================
# EXAMPLE: Cloudflare IP Restoration (Swarm)
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restoring original visitor IPs when behind Cloudflare CDN
# - Using Docker configs to manage Cloudflare IP lists
# - Service discovery in Swarm mode with plugins
# - Load balancing across multiple replicas
#
# REQUIREMENTS (run these first):
# ```bash
# # Initialize Docker Swarm (if not already initialized)
# docker swarm init
#
# # Create overlay network (idempotent)
# docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy
#
# # Ensure EasyHAProxy is deployed
# docker stack deploy -c easyhaproxy.yml easyhaproxy
#
# # Download Cloudflare IP ranges and create Docker config
# curl https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# echo "" >> cloudflare_ips.lst
# curl https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
# docker config create cloudflare_ips cloudflare_ips.lst
# rm cloudflare_ips.lst
#
# # Add to /etc/hosts for local testing (idempotent)
# grep -q "myapp.example.com" /etc/hosts || echo "127.0.0.1 myapp.example.com" | sudo tee -a /etc/hosts
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c cloudflare.yml webapp
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check stack is deployed
# docker stack ls | grep webapp
# # Expected: webapp stack listed
#
# # Check service is running
# docker service ls | grep webapp_webapp
# # Expected: webapp_webapp with 4/4 replicas
#
# # Test the application
# curl -H "Host: myapp.example.com" http://localhost/
# # Expected: 200 OK with "App Behind Cloudflare"
#
# # Check HAProxy config includes Cloudflare IPs
# docker exec $(docker ps -q -f name=easyhaproxy_haproxy) cat /etc/haproxy/haproxy.cfg | grep -A 5 "cloudflare"
# # Expected: ACL rules for Cloudflare IP ranges
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm webapp
# # To also remove the Cloudflare IPs config:
# # docker config rm cloudflare_ips
# ```
#
# NOTE: This plugin is most useful when your site is actually behind Cloudflare CDN.
# The plugin uses the CF-Connecting-IP header to restore the original visitor IP.
#
# ==============================================================================
version: "3.7"
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
configs:
- source: cloudflare_ips
target: /etc/haproxy/cloudflare_ips.lst
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
environment:
EASYHAPROXY_DISCOVER: swarm
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
networks:
- easyhaproxy
# Web application behind Cloudflare
webapp:
image: byjg/static-httpserver
environment:
TITLE: "App Behind Cloudflare"
deploy:
replicas: 4
labels:
easyhaproxy.http.host: "myapp.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# Enable Cloudflare plugin
easyhaproxy.http.plugins: "cloudflare"
# Optional: Specify custom IP list path
# easyhaproxy.http.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst"
networks:
- easyhaproxy
networks:
easyhaproxy:
external: true
configs:
cloudflare_ips:
external: true

View file

@ -0,0 +1,87 @@
# ==============================================================================
# EXAMPLE: EasyHAProxy for Docker Swarm
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - EasyHAProxy running in Swarm mode with service discovery
# - HAProxy stats interface
# - Let's Encrypt/Certbot support
# - Shared overlay network for services
#
# REQUIREMENTS (run these first):
# ```bash
# # Initialize Docker Swarm (if not already initialized)
# docker swarm init
#
# # Create overlay network (idempotent)
# docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy
#
# # Edit this file and change:
# # Line 18: EASYHAPROXY_CERTBOT_EMAIL to your email
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c easyhaproxy.yml easyhaproxy
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check stack is deployed
# docker stack ls
# # Expected: easyhaproxy stack listed
#
# # Check service is running
# docker service ls
# # Expected: easyhaproxy_haproxy with 1/1 replicas
#
# # View HAProxy stats
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
#
# # Check logs
# docker service logs -f easyhaproxy_haproxy
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm easyhaproxy
# ```
#
# ==============================================================================
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./certs:/certs/haproxy
- certs_certbot:/certs/certbot
deploy:
replicas: 1
environment:
EASYHAPROXY_DISCOVER: swarm
EASYHAPROXY_SSL_MODE: "loose"
EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
networks:
- easyhaproxy
networks:
easyhaproxy:
external: true
volumes:
certs_certbot:
# external: true
# certs_haproxy:
# external: true

View file

@ -0,0 +1,113 @@
# ==============================================================================
# EXAMPLE: IP Whitelist Plugin (Swarm)
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Restricting access to specific IP addresses/networks
# - IP-based access control for admin panels or sensitive services
# - Returning custom status codes for blocked IPs
# - Service discovery in Swarm mode with plugins
#
# REQUIREMENTS (run these first):
# ```bash
# # Initialize Docker Swarm (if not already initialized)
# docker swarm init
#
# # Create overlay network (idempotent)
# docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy
#
# # Ensure EasyHAProxy is deployed
# docker stack deploy -c easyhaproxy.yml easyhaproxy
#
# # Add to /etc/hosts for local testing (idempotent)
# grep -q "admin.example.com" /etc/hosts || echo "127.0.0.1 admin.example.com" | sudo tee -a /etc/hosts
#
# # IMPORTANT: Edit this file (ip-whitelist.yml) line 64 to add your actual IP addresses!
# # Get your current IP: curl ifconfig.me
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c ip-whitelist.yml admin
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check stack is deployed
# docker stack ls | grep admin
# # Expected: admin stack listed
#
# # Check service is running
# docker service ls | grep admin_admin
# # Expected: admin_admin with 3/3 replicas
#
# # Test from allowed IP (assumes 127.0.0.1 or your IP is in the whitelist)
# curl -H "Host: admin.example.com" http://localhost/
# # Expected: 200 OK with "Admin Panel - IP Restricted"
#
# # Test from blocked IP (using a different IP via proxy or VPN)
# # Expected: HTTP 403 Forbidden
#
# # View HAProxy stats to see IP whitelist rules
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm admin
# ```
#
# ==============================================================================
version: "3.7"
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
environment:
EASYHAPROXY_DISCOVER: swarm
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
networks:
- easyhaproxy
# Admin panel with IP restrictions
admin:
image: byjg/static-httpserver
environment:
TITLE: "Admin Panel - IP Restricted"
deploy:
replicas: 3
labels:
easyhaproxy.http.host: "admin.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# Enable IP whitelist plugin
easyhaproxy.http.plugins: "ip_whitelist"
# Allow specific IPs and networks
# UPDATE THIS with your actual office/VPN IPs!
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,198.51.100.0/24,10.0.0.0/8"
# Status code to return for blocked IPs
easyhaproxy.http.plugin.ip_whitelist.status_code: "403"
networks:
- easyhaproxy
networks:
easyhaproxy:
external: true

View file

@ -0,0 +1,132 @@
# ==============================================================================
# EXAMPLE: JWT Validator Plugin (Swarm)
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - JWT token validation for API authentication
# - Using Docker configs to manage JWT public keys
# - Validating issuer, audience, and expiration claims
# - Service discovery in Swarm mode with plugins
#
# REQUIREMENTS (run these first):
# ```bash
# # Initialize Docker Swarm (if not already initialized)
# docker swarm init
#
# # Create overlay network (idempotent)
# docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy
#
# # Ensure EasyHAProxy is deployed
# docker stack deploy -c easyhaproxy.yml easyhaproxy
#
# # Generate JWT key pair (RS256 algorithm)
# openssl genrsa -out jwt_private.pem 2048
# openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
#
# # Create Docker config with public key
# docker config create jwt_api_pubkey jwt_pubkey.pem
#
# # Add to /etc/hosts for local testing (idempotent)
# grep -q "api.example.com" /etc/hosts || echo "127.0.0.1 api.example.com" | sudo tee -a /etc/hosts
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c jwt-validator.yml api
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check stack is deployed
# docker stack ls | grep api
# # Expected: api stack listed
#
# # Check service is running
# docker service ls | grep api_api
# # Expected: api_api with 5/5 replicas
#
# # Test without token (should fail)
# curl -H "Host: api.example.com" http://localhost/
# # Expected: HTTP 401 with "Missing Authorization HTTP header"
#
# # Generate test JWT at https://jwt.io with:
# # - Algorithm: RS256
# # - Payload: {"iss":"https://auth.example.com/","aud":"https://api.example.com","exp":9999999999}
# # - Use your jwt_private.pem content in "Verify Signature" section
#
# # Test with valid token (replace TOKEN with your JWT)
# TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# curl -H "Host: api.example.com" -H "Authorization: Bearer $TOKEN" http://localhost/
# # Expected: 200 OK with "Protected API - JWT Required"
#
# # Test with invalid/expired token
# curl -H "Host: api.example.com" -H "Authorization: Bearer invalid_token" http://localhost/
# # Expected: HTTP 401 with error message
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm api
# # To also remove the JWT public key config and generated keys:
# # docker config rm jwt_api_pubkey
# # rm jwt_private.pem jwt_pubkey.pem
# ```
#
# ==============================================================================
version: "3.7"
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
configs:
- source: jwt_api_pubkey
target: /etc/haproxy/jwt_keys/api_pubkey.pem
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
environment:
EASYHAPROXY_DISCOVER: swarm
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "1936:1936/tcp"
networks:
- easyhaproxy
# Protected API service
api:
image: byjg/static-httpserver
environment:
TITLE: "Protected API - JWT Required"
deploy:
replicas: 5
labels:
easyhaproxy.http.host: "api.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# Enable JWT validator plugin
easyhaproxy.http.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.http.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.http.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.http.plugin.jwt_validator.audience: "https://api.example.com"
easyhaproxy.http.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
networks:
- easyhaproxy
networks:
easyhaproxy:
external: true
configs:
jwt_api_pubkey:
external: true

View file

@ -0,0 +1,200 @@
# ==============================================================================
# EXAMPLE: Multiple Plugins Combined (Swarm)
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Using multiple plugins together for layered security
# - Three different security profiles for different service types:
# * Public website: Cloudflare IP restoration + path blocking
# * Protected API: JWT authentication + path blocking
# * Admin panel: Strict IP whitelist
# - Complex production-ready security configuration
#
# REQUIREMENTS (run these first):
# ```bash
# # Initialize Docker Swarm (if not already initialized)
# docker swarm init
#
# # Create overlay network (idempotent)
# docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy
#
# # Ensure EasyHAProxy is deployed
# docker stack deploy -c easyhaproxy.yml easyhaproxy
#
# # Generate JWT key pair (RS256 algorithm)
# openssl genrsa -out jwt_private.pem 2048
# openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem
# docker config create jwt_api_pubkey jwt_pubkey.pem
#
# # Download Cloudflare IP ranges and create Docker config
# curl https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst
# echo "" >> cloudflare_ips.lst
# curl https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst
# docker config create cloudflare_ips cloudflare_ips.lst
# rm cloudflare_ips.lst jwt_private.pem jwt_pubkey.pem
#
# # Add to /etc/hosts for local testing (idempotent)
# grep -q "website.example.com" /etc/hosts || echo "127.0.0.1 website.example.com api.example.com admin.example.com" | sudo tee -a /etc/hosts
#
# # IMPORTANT: Edit this file (plugins-combined.yml) line 124 to add your actual IP!
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c plugins-combined.yml production
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check stack is deployed
# docker stack ls | grep production
# # Expected: production stack listed
#
# # Check all services are running
# docker service ls | grep production
# # Expected: 3 services (website, api, admin) with all replicas running
#
# # Test public website (Cloudflare + deny_pages)
# curl -H "Host: website.example.com" http://localhost/
# # Expected: 200 OK with "Public Website"
# curl -H "Host: website.example.com" http://localhost/admin
# # Expected: HTTP 404 (blocked by deny_pages)
#
# # Test protected API (JWT + deny_pages)
# curl -H "Host: api.example.com" http://localhost/
# # Expected: HTTP 401 with "Missing Authorization HTTP header"
#
# # Generate JWT at https://jwt.io (see jwt-validator.yml for details)
# TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# curl -H "Host: api.example.com" -H "Authorization: Bearer $TOKEN" http://localhost/
# # Expected: 200 OK with "Protected API"
# curl -H "Host: api.example.com" -H "Authorization: Bearer $TOKEN" http://localhost/internal
# # Expected: HTTP 403 (blocked by deny_pages)
#
# # Test admin panel (IP whitelist)
# curl -H "Host: admin.example.com" http://localhost/
# # Expected: 200 OK if your IP is whitelisted, 403 otherwise
#
# # View HAProxy stats to see all plugin configurations
# # URL: http://localhost:1936
# # Username: admin
# # Password: password
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm production
# # To also remove Docker configs:
# # docker config rm cloudflare_ips jwt_api_pubkey
# ```
#
# ==============================================================================
version: "3.7"
services:
haproxy:
image: byjg/easy-haproxy:5.0.0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
configs:
- source: cloudflare_ips
target: /etc/haproxy/cloudflare_ips.lst
- source: jwt_api_pubkey
target: /etc/haproxy/jwt_keys/api_pubkey.pem
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
environment:
EASYHAPROXY_DISCOVER: swarm
EASYHAPROXY_SSL_MODE: "loose"
HAPROXY_CUSTOMERRORS: "true"
HAPROXY_USERNAME: admin
HAPROXY_PASSWORD: password
HAPROXY_STATS_PORT: 1936
ports:
- "80:80/tcp"
- "443:443/tcp"
- "1936:1936/tcp"
networks:
- easyhaproxy
# Public website with Cloudflare + path blocking
website:
image: byjg/static-httpserver
environment:
TITLE: "Public Website"
deploy:
replicas: 4
labels:
easyhaproxy.http.host: "website.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# Cloudflare IP restoration + block sensitive paths
easyhaproxy.http.plugins: "cloudflare,deny_pages"
easyhaproxy.http.plugin.deny_pages.paths: "/admin,/wp-admin,/wp-login.php,/.env,/config"
easyhaproxy.http.plugin.deny_pages.status_code: "404"
networks:
- easyhaproxy
# Protected API with JWT + path blocking
api:
image: byjg/static-httpserver
environment:
TITLE: "Protected API"
deploy:
replicas: 6
labels:
easyhaproxy.http.host: "api.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# JWT validation + block internal endpoints
easyhaproxy.http.plugins: "jwt_validator,deny_pages"
# JWT configuration
easyhaproxy.http.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.http.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.http.plugin.jwt_validator.audience: "https://api.example.com"
easyhaproxy.http.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
# Block internal/debug paths
easyhaproxy.http.plugin.deny_pages.paths: "/internal,/debug,/metrics"
easyhaproxy.http.plugin.deny_pages.status_code: "403"
networks:
- easyhaproxy
# Admin panel with strict IP whitelist
admin:
image: byjg/static-httpserver
environment:
TITLE: "Admin Panel"
deploy:
replicas: 2
labels:
easyhaproxy.http.host: "admin.example.com"
easyhaproxy.http.port: "80"
easyhaproxy.http.localport: "8080"
# IP whitelist only (strictest security)
easyhaproxy.http.plugins: "ip_whitelist"
# Only allow office network
# UPDATE with your actual office/VPN IPs!
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,10.0.0.0/8"
easyhaproxy.http.plugin.ip_whitelist.status_code: "403"
networks:
- easyhaproxy
networks:
easyhaproxy:
external: true
configs:
cloudflare_ips:
external: true
jwt_api_pubkey:
external: true

View file

@ -0,0 +1,67 @@
# ==============================================================================
# EXAMPLE: Portainer Behind EasyHAProxy (Swarm)
# ==============================================================================
#
# WHAT THIS DEMONSTRATES:
# - Running Portainer management UI in Swarm mode
# - Service discovery with EasyHAProxy
# - Using persistent volumes for Portainer data
#
# REQUIREMENTS (run these first):
# ```bash
# # Ensure EasyHAProxy is deployed
# docker stack deploy -c easyhaproxy.yml easyhaproxy
#
# # Add to /etc/hosts (idempotent)
# grep -q "portainer.local" /etc/hosts || echo "127.0.0.1 portainer.local" | sudo tee -a /etc/hosts
# ```
#
# HOW TO START:
# ```bash
# docker stack deploy -c portainer.yml portainer
# ```
#
# HOW TO VERIFY IT'S WORKING:
# ```bash
# # Check service is running
# docker service ls | grep portainer
# # Expected: portainer_portainer with 1/1 replicas
#
# # Access Portainer
# curl http://portainer.local
# # Or open in browser: http://portainer.local
# # First time: Create admin user
# ```
#
# CLEAN UP:
# ```bash
# docker stack rm portainer
# # To also remove data volume:
# # docker volume rm portainer_portainer_data
# ```
#
# ==============================================================================
services:
portainer:
image: portainer/portainer-ce:latest
volumes:
- portainer_data:/data portainer
- /var/run/docker.sock:/var/run/docker.sock
deploy:
replicas: 1
labels:
# easyhaproxy.http.redirect_ssl: true
# easyhaproxy.http.certbot: true
easyhaproxy.http.host: portainer.local
easyhaproxy.http.port: 80
easyhaproxy.http.localport: 9000
volumes:
certs_certbot:
external: true
# certs_haproxy:
# external: true
portainer_data:
# external: true

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,933 @@
"""
Pytest test suite for EasyHAProxy Docker Compose examples
These tests verify the functionality of various docker-compose configurations.
Tests are organized by compose file and can be run individually or as a suite.
Requirements:
- pytest
- requests
- PyJWT
- cryptography
- docker-compose
Usage:
# Run all tests
pytest test_docker_compose.py -v
# Run specific test class
pytest test_docker_compose.py::TestBasicSSL -v
# Run specific test
pytest test_docker_compose.py::TestBasicSSL::test_https_host1 -v
# Run with markers
pytest test_docker_compose.py -m ssl -v
"""
import subprocess
import time
import os
from pathlib import Path
import pytest
import requests
import jwt as jwt_lib
from typing import Generator
# Base directory for docker-compose files
BASE_DIR = Path(__file__).parent.absolute()
@pytest.fixture(scope="session", autouse=True)
def generate_ssl_certificates():
"""
Generate SSL certificates once for all tests that require them.
This runs automatically at the start of the test session.
"""
script_path = BASE_DIR / "generate-keys.sh"
# Check if script exists
if not script_path.exists():
pytest.skip(f"SSL certificate generation script not found: {script_path}")
# Run the script from the tests_e2e directory
result = subprocess.run(
["bash", str(script_path)],
cwd=BASE_DIR,
capture_output=True,
text=True
)
if result.returncode != 0:
pytest.fail(f"Failed to generate SSL certificates:\n{result.stderr}")
yield
# No cleanup needed - certificates can be reused
class DockerComposeFixture:
"""Helper class to manage docker-compose lifecycle"""
def __init__(self, compose_file: str, startup_wait: int = 3, build: bool = True):
self.compose_file = str(BASE_DIR / "docker" / compose_file)
self.startup_wait = startup_wait
self.build = build
def up(self):
"""Start docker-compose services"""
cmd = ["docker", "compose", "-f", self.compose_file, "up", "-d"]
if self.build:
cmd.append("--build")
subprocess.run(
cmd,
check=True,
capture_output=True
)
time.sleep(self.startup_wait)
def down(self):
"""Stop and remove docker-compose services"""
subprocess.run(
["docker", "compose", "-f", self.compose_file, "down", "--remove-orphans"],
check=True,
capture_output=True
)
@pytest.fixture
def docker_compose_basic_ssl() -> Generator[None, None, None]:
"""Fixture for docker-compose.yml (Basic SSL)"""
fixture = DockerComposeFixture("docker-compose.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_jwt_validator() -> Generator[None, None, None]:
"""Fixture for docker-compose-jwt-validator.yml"""
fixture = DockerComposeFixture("docker-compose-jwt-validator.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_multi_containers() -> Generator[None, None, None]:
"""Fixture for docker-compose-multi-containers.yml"""
fixture = DockerComposeFixture("docker-compose-multi-containers.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_php_fpm() -> Generator[None, None, None]:
"""Fixture for docker-compose-php-fpm.yml"""
fixture = DockerComposeFixture("docker-compose-php-fpm.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_plugins_combined() -> Generator[None, None, None]:
"""Fixture for docker-compose-plugins-combined.yml"""
fixture = DockerComposeFixture("docker-compose-plugins-combined.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_ip_whitelist() -> Generator[None, None, None]:
"""Fixture for docker-compose-ip-whitelist.yml"""
fixture = DockerComposeFixture("docker-compose-ip-whitelist.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def docker_compose_cloudflare() -> Generator[None, None, None]:
"""Fixture for docker-compose-cloudflare.yml"""
# Set up cloudflare_ips.lst with Docker network for testing
cloudflare_ips_path = BASE_DIR / "docker" / "cloudflare_ips.lst"
# Download Cloudflare IPs
subprocess.run(
["curl", "-s", "https://www.cloudflare.com/ips-v4"],
stdout=open(cloudflare_ips_path, 'w'),
check=True
)
with open(cloudflare_ips_path, 'a') as f:
f.write("\n")
subprocess.run(
["curl", "-s", "https://www.cloudflare.com/ips-v6"],
stdout=open(cloudflare_ips_path, 'a'),
check=True
)
# Add Docker private network range so HAProxy treats test requests as from Cloudflare
# Docker bridge networks are typically in 172.16.0.0/12 range
with open(cloudflare_ips_path, 'a') as f:
f.write("\n")
f.write("172.16.0.0/12\n") # Docker private network range
fixture = DockerComposeFixture("docker-compose-cloudflare.yml")
fixture.up()
yield
fixture.down()
@pytest.fixture
def jwt_token() -> str:
"""Generate a valid JWT token for testing"""
private_key_path = BASE_DIR / "docker" / "jwt_private.pem"
with open(private_key_path, 'r') as f:
private_key = f.read()
payload = {
'iss': 'https://auth.example.com/',
'aud': 'https://api.example.com',
'exp': 9999999999
}
token = jwt_lib.encode(payload, private_key, algorithm='RS256')
return token
# =============================================================================
# Test: docker-compose.yml - Basic SSL Setup
# =============================================================================
@pytest.mark.ssl
class TestBasicSSL:
"""Tests for basic SSL setup with two virtual hosts"""
def test_haproxy_config(self, docker_compose_basic_ssl):
"""Test HAProxy configuration has SSL and redirect configurations"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Test HTTPS backend for host1
https_host1_block = extract_backend_block(config, "srv_host1_local_443")
assert https_host1_block, "Backend srv_host1_local_443 not found"
assert "mode http" in https_host1_block
# Test HTTPS backend for host2
https_host2_block = extract_backend_block(config, "srv_host2_local_443")
assert https_host2_block, "Backend srv_host2_local_443 not found"
assert "mode http" in https_host2_block
# Verify SSL frontend exists and binds to port 443
assert "frontend https_in_443" in config or "bind *:443" in config
# Verify HTTP to HTTPS redirect
# Check for redirect rules in HTTP frontend or backends
assert "redirect scheme https" in config or "location: https://" in config
def test_https_host1(self, docker_compose_basic_ssl):
"""Test HTTPS access to host1.local"""
response = requests.get(
"https://127.0.0.1/",
headers={"Host": "host1.local"},
verify=False
)
assert response.status_code == 200
def test_https_host2(self, docker_compose_basic_ssl):
"""Test HTTPS access to host2.local"""
response = requests.get(
"https://127.0.0.1/",
headers={"Host": "host2.local"},
verify=False
)
assert response.status_code == 200
def test_http_redirect_host1(self, docker_compose_basic_ssl):
"""Test HTTP to HTTPS redirect for host1.local"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "host1.local"},
allow_redirects=False
)
assert response.status_code == 301
assert response.headers.get("location") == "https://host1.local/"
def test_http_redirect_host2(self, docker_compose_basic_ssl):
"""Test HTTP to HTTPS redirect for host2.local"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "host2.local"},
allow_redirects=False
)
assert response.status_code == 301
assert response.headers.get("location") == "https://host2.local/"
def test_haproxy_stats(self, docker_compose_basic_ssl):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-jwt-validator.yml - JWT Validator Plugin
# =============================================================================
@pytest.mark.jwt
class TestJWTValidator:
"""Tests for JWT validator plugin"""
def test_haproxy_config(self, docker_compose_jwt_validator):
"""Test HAProxy configuration has JWT validator rules in the correct backend"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Extract the specific backend block
backend_block = extract_backend_block(config, "srv_api_local_80")
assert backend_block, "Backend srv_api_local_80 not found"
# Verify JWT validator plugin comment
assert "# JWT Validator - Validate JWT tokens" in backend_block
# Verify JWT validation rules
assert "http-request deny content-type 'text/html' string 'Missing Authorization HTTP header'" in backend_block
assert "http_auth_bearer,jwt_header_query('$.alg')" in backend_block
assert "http_auth_bearer,jwt_payload_query('$.iss')" in backend_block
assert "http_auth_bearer,jwt_payload_query('$.aud')" in backend_block
# Verify algorithm check
assert "var(txn.alg) -m str RS256" in backend_block
# Verify issuer and audience checks
assert "var(txn.iss) -m str https://auth.example.com/" in backend_block
assert "var(txn.aud) -m str https://api.example.com" in backend_block
# Verify JWT signature verification
assert 'jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem")' in backend_block
# Verify expiration check
assert "JWT has expired" in backend_block
def test_without_token(self, docker_compose_jwt_validator):
"""Test API access without JWT token (should fail)"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "api.local"}
)
assert response.status_code == 403
assert "Missing Authorization HTTP header" in response.text
def test_with_valid_token(self, docker_compose_jwt_validator, jwt_token):
"""Test API access with valid JWT token (should succeed)"""
response = requests.get(
"http://127.0.0.1/",
headers={
"Host": "api.local",
"Authorization": f"Bearer {jwt_token}"
}
)
assert response.status_code == 200
def test_haproxy_stats(self, docker_compose_jwt_validator):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-multi-containers.yml - Load Balancing
# =============================================================================
@pytest.mark.loadbalancing
class TestMultiContainers:
"""Tests for load balancing with multiple container replicas"""
def test_haproxy_config(self, docker_compose_multi_containers):
"""Test HAProxy configuration has multiple backend servers for load balancing"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Extract the specific backend block
backend_block = extract_backend_block(config, "srv_www_helloworld_com_19901")
assert backend_block, "Backend srv_www_helloworld_com_19901 not found"
# Verify round-robin load balancing
assert "balance roundrobin" in backend_block
# Verify multiple servers are configured
server_lines = [line for line in backend_block.split('\n') if line.strip().startswith('server srv-')]
assert len(server_lines) >= 2, f"Expected at least 2 servers, found {len(server_lines)}"
# Verify both servers have check and weight
for server_line in server_lines:
assert "check" in server_line
assert "weight" in server_line
def test_load_balancing(self, docker_compose_multi_containers):
"""Test round-robin load balancing across replicas"""
container_ids = set()
for _ in range(6):
response = requests.get(
"http://localhost:19901/",
headers={"Host": "www.helloworld.com"}
)
assert response.status_code == 200
container_ids.add(response.text.strip())
# Should see at least 2 different container IDs
assert len(container_ids) >= 2
def test_domain_redirect(self, docker_compose_multi_containers):
"""Test domain redirect functionality"""
response = requests.get(
"http://localhost:19901/",
headers={"Host": "google.helloworld.com"},
allow_redirects=False
)
assert response.status_code == 301
assert response.headers.get("location") == "www.google.com/"
# =============================================================================
# Test: docker-compose-php-fpm.yml - PHP-FPM FastCGI Plugin
# =============================================================================
@pytest.mark.php
class TestPHPFPM:
"""Tests for PHP-FPM FastCGI plugin"""
def test_haproxy_config(self, docker_compose_php_fpm):
"""Test HAProxy configuration has FastCGI plugin configuration"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Extract the specific backend block
backend_block = extract_backend_block(config, "srv_phpapp_local_80")
assert backend_block, "Backend srv_phpapp_local_80 not found"
# Verify FastCGI app is used
assert "use-fcgi-app fcgi_phpapp_local" in backend_block
# Verify server uses fcgi protocol
assert "proto fcgi" in backend_block
# Verify port 9000 (PHP-FPM default)
assert ":9000" in backend_block
# Now check for fcgi-app configuration (not in backend, but in global config)
assert "fcgi-app fcgi_phpapp_local" in config
# Extract fcgi-app block
fcgi_lines = []
in_fcgi = False
for line in config.split('\n'):
if line.startswith('fcgi-app fcgi_phpapp_local'):
in_fcgi = True
elif in_fcgi:
if line.startswith(('fcgi-app ', 'frontend ', 'backend ', 'listen ')):
break
fcgi_lines.append(line)
fcgi_block = '\n'.join(fcgi_lines)
# Verify FastCGI plugin configuration
assert "docroot /var/www/html" in fcgi_block
assert "index index.php" in fcgi_block
assert "path-info" in fcgi_block
def test_main_page(self, docker_compose_php_fpm):
"""Test main PHP page"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "phpapp.local"}
)
assert response.status_code == 200
assert "PHP-FPM with EasyHAProxy" in response.text
def test_phpinfo(self, docker_compose_php_fpm):
"""Test PHP info page"""
response = requests.get(
"http://127.0.0.1/info.php",
headers={"Host": "phpapp.local"}
)
assert response.status_code == 200
assert "phpinfo()" in response.text
def test_path_info_routing(self, docker_compose_php_fpm):
"""Test PATH_INFO routing for RESTful URLs"""
response = requests.get(
"http://127.0.0.1/test-path-info.php/users/123",
headers={"Host": "phpapp.local"}
)
assert response.status_code == 200
assert "PATH_INFO" in response.text
assert "/users/123" in response.text
def test_haproxy_stats(self, docker_compose_php_fpm):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-plugins-combined.yml - Multiple Plugins Combined
# =============================================================================
@pytest.mark.plugins
class TestPluginsCombined:
"""Tests for multiple plugins combined"""
def test_haproxy_config(self, docker_compose_plugins_combined):
"""Test HAProxy configuration has all plugin configurations in correct backends"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Test website backend (Cloudflare + deny_pages)
website_block = extract_backend_block(config, "srv_website_local_80")
assert website_block, "Backend srv_website_local_80 not found"
assert "# Cloudflare - Restore original visitor IP" in website_block
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" 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 "http-request deny deny_status 404 if denied_path" in website_block
# Test API backend (JWT validator + deny_pages)
api_block = extract_backend_block(config, "srv_api_local_80")
assert api_block, "Backend srv_api_local_80 not found"
assert "# JWT Validator - Validate JWT tokens" in api_block
assert "Missing Authorization HTTP header" in api_block
assert "jwt_verify" in api_block
assert "# Deny Pages - Block specific paths" in api_block
assert "acl denied_path path_beg /internal /debug /metrics" in api_block
assert "http-request deny deny_status 403 if denied_path" in api_block
# Test admin backend (IP whitelist)
admin_block = extract_backend_block(config, "srv_admin_local_80")
assert admin_block, "Backend srv_admin_local_80 not found"
assert "# IP Whitelist - Only allow specific IPs" in admin_block
assert "acl whitelisted_ip src" in admin_block
assert "http-request deny deny_status 403 if !whitelisted_ip" in admin_block
def test_website_normal_access(self, docker_compose_plugins_combined):
"""Test normal access to public website"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "website.local"}
)
assert response.status_code == 200
def test_website_blocked_paths(self, docker_compose_plugins_combined):
"""Test blocked paths on public website"""
blocked_paths = ["/admin", "/wp-admin", "/.env", "/config"]
for path in blocked_paths:
response = requests.get(
f"http://127.0.0.1{path}",
headers={"Host": "website.local"}
)
assert response.status_code == 404
def test_api_without_token(self, docker_compose_plugins_combined):
"""Test API without JWT token"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "api.local"}
)
assert response.status_code == 403
assert "Missing Authorization HTTP header" in response.text
def test_api_with_valid_token(self, docker_compose_plugins_combined, jwt_token):
"""Test API with valid JWT token"""
response = requests.get(
"http://127.0.0.1/",
headers={
"Host": "api.local",
"Authorization": f"Bearer {jwt_token}"
}
)
assert response.status_code == 200
def test_api_blocked_paths_with_token(self, docker_compose_plugins_combined, jwt_token):
"""Test blocked paths on API even with valid JWT"""
blocked_paths = ["/internal", "/debug", "/metrics"]
for path in blocked_paths:
response = requests.get(
f"http://127.0.0.1{path}",
headers={
"Host": "api.local",
"Authorization": f"Bearer {jwt_token}"
}
)
assert response.status_code == 403
def test_admin_panel_localhost(self, docker_compose_plugins_combined):
"""Test admin panel from localhost (should be allowed)"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "admin.local"}
)
assert response.status_code == 200
def test_haproxy_stats(self, docker_compose_plugins_combined):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-ip-whitelist.yml - IP Whitelist Plugin
# =============================================================================
def extract_backend_block(config: str, backend_name: str) -> str:
"""Extract a specific backend block from HAProxy configuration"""
lines = config.split('\n')
backend_lines = []
in_backend = False
for line in lines:
if line.startswith(f'backend {backend_name}'):
in_backend = True
backend_lines.append(line)
elif in_backend:
# Stop when we hit another backend, frontend, or global section
if line.startswith(('backend ', 'frontend ', 'global ', 'defaults ')):
break
backend_lines.append(line)
return '\n'.join(backend_lines)
@pytest.mark.security
class TestIPWhitelist:
"""Tests for IP whitelist plugin"""
def test_haproxy_config(self, docker_compose_ip_whitelist):
"""Test HAProxy configuration has IP whitelist rules in the correct backend"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Extract the specific backend block
backend_block = extract_backend_block(config, "srv_admin_local_80")
assert backend_block, "Backend srv_admin_local_80 not found"
# Verify IP whitelist plugin comment is in this backend
assert "# IP Whitelist - Only allow specific IPs" in backend_block
# Verify ACL for whitelisted IPs is in this backend
assert "acl whitelisted_ip src" in backend_block
# Extract the ACL line to verify IPs
acl_line = [line for line in backend_block.split('\n') if 'acl whitelisted_ip src' in line][0]
assert "127.0.0.1" in acl_line
assert "192.168.0.0/16" in acl_line
assert "10.0.0.0/8" in acl_line
assert "172.16.0.0/12" in acl_line
# Verify deny rule for non-whitelisted IPs is in this backend
assert "http-request deny deny_status 403 if !whitelisted_ip" in backend_block
def test_localhost_allowed(self, docker_compose_ip_whitelist):
"""Test access from localhost (should be allowed)"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "admin.local"}
)
assert response.status_code == 200
assert "Admin Panel" in response.text
def test_haproxy_stats(self, docker_compose_ip_whitelist):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-cloudflare.yml - Cloudflare IP Restoration Plugin
# =============================================================================
@pytest.mark.cloudflare
class TestCloudflare:
"""Tests for Cloudflare IP restoration plugin"""
def test_haproxy_config(self, docker_compose_cloudflare):
"""Test HAProxy configuration has Cloudflare plugin rules in the correct backend"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Extract the specific backend block
backend_block = extract_backend_block(config, "srv_myapp_local_80")
assert backend_block, "Backend srv_myapp_local_80 not found"
# Verify Cloudflare plugin comment
assert "# Cloudflare - Restore original visitor IP" in backend_block
# Verify ACL for Cloudflare IPs
assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in backend_block
# 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
# Verify X-Forwarded-For header restoration with transaction variable
assert "http-request set-header X-Forwarded-For %[var(txn.real_ip)] if from_cloudflare" in backend_block
def test_normal_request(self, docker_compose_cloudflare):
"""
Test normal request without CF-Connecting-IP header
When a request comes from a Cloudflare IP (Docker network is in cloudflare_ips.lst)
but has NO CF-Connecting-IP header, the X-Forwarded-For will be empty because
HAProxy tries to extract from a non-existent header. This is expected behavior.
"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "myapp.local"}
)
assert response.status_code == 200
data = response.json()
assert 'headers' in data
assert 'x_forwarded_for' in data
# Verify X-Forwarded-For is empty (not a translated IP)
# Request comes from "Cloudflare IP" (Docker network) but has no CF-Connecting-IP
x_forwarded_for = data['x_forwarded_for']
assert x_forwarded_for == '', \
f"Expected X-Forwarded-For to be empty (no CF-Connecting-IP provided), got '{x_forwarded_for}'"
# Verify client_ip is the HAProxy container IP (backend sees connection from HAProxy)
client_ip = data['client_ip']
assert client_ip.startswith('172.'), \
f"Expected client_ip to be HAProxy container IP (172.x.x.x), got '{client_ip}'"
def test_cloudflare_ip_translation(self, docker_compose_cloudflare):
"""
Test that Cloudflare plugin actually translates CF-Connecting-IP to X-Forwarded-For
This test verifies the Cloudflare plugin correctly:
1. Detects requests from Cloudflare IPs (127.0.0.1 is in cloudflare_ips.lst)
2. Extracts the CF-Connecting-IP header value
3. Sets X-Forwarded-For header to that value
4. Backend receives the correct translated IP
"""
test_ip = "203.0.113.50"
response = requests.get(
"http://127.0.0.1/",
headers={
"Host": "myapp.local",
"CF-Connecting-IP": test_ip
}
)
assert response.status_code == 200
# Parse JSON response from header-echo server
data = response.json()
# VERIFY: X-Forwarded-For was set to the CF-Connecting-IP value
assert data['x_forwarded_for'] == test_ip, \
f"Expected X-Forwarded-For to be '{test_ip}', got '{data['x_forwarded_for']}'. " \
f"Cloudflare IP translation is NOT working!"
# Verify client_ip is still the HAProxy container IP (connection doesn't change)
client_ip = data['client_ip']
assert client_ip.startswith('172.'), \
f"Expected client_ip to be HAProxy container IP (172.x.x.x), got '{client_ip}'"
def test_haproxy_stats(self, docker_compose_cloudflare):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Test: docker-compose-changed-label.yml - Custom Label Prefix
# =============================================================================
@pytest.fixture
def docker_compose_changed_label() -> Generator[None, None, None]:
"""Fixture for docker-compose-changed-label.yml"""
fixture = DockerComposeFixture("docker-compose-changed-label.yml")
fixture.up()
yield
fixture.down()
@pytest.mark.custom_label
class TestChangedLabel:
"""Tests for docker-compose-changed-label.yml - Custom label prefix"""
def test_haproxy_config(self, docker_compose_changed_label):
"""Test HAProxy configuration with custom label prefix"""
result = subprocess.run(
["docker", "exec", "docker-haproxy-1", "cat", "/etc/haproxy/haproxy.cfg"],
capture_output=True,
text=True,
check=True
)
config = result.stdout
# Verify HTTPS backend exists
assert "backend srv_host1_local_443" in config
# Verify SSL configuration (frontend with SSL)
assert "bind *:443" in config
assert "ssl crt" in config
# Verify HTTP backend exists
assert "backend srv_host1_local_80" in config
# Verify HTTP to HTTPS redirect is configured
assert "redirect prefix https://host1.local code 301" in config
def test_https_access(self, docker_compose_changed_label):
"""Test HTTPS access to host1.local"""
response = requests.get(
"https://127.0.0.1/",
headers={"Host": "host1.local"},
verify=False # Self-signed certificate
)
assert response.status_code == 200
# byjg/static-httpserver returns a "Coming Soon" page
assert "soon" in response.text.lower() or "coming" in response.text.lower()
def test_http_redirect(self, docker_compose_changed_label):
"""Test HTTP to HTTPS redirect"""
response = requests.get(
"http://127.0.0.1/",
headers={"Host": "host1.local"},
allow_redirects=False
)
# The redirect uses 301 (permanent) as configured in the labels
assert response.status_code == 301
assert response.headers["Location"] == "https://host1.local/"
def test_custom_label_prefix(self, docker_compose_changed_label):
"""Verify custom label prefix 'haproxy' is being used"""
# Get container ID for static-httpserver
result = subprocess.run(
["docker", "ps", "-q", "-f", "ancestor=byjg/static-httpserver"],
capture_output=True,
text=True,
check=True
)
container_id = result.stdout.strip()
assert container_id, "Container not found"
# Inspect container labels
result = subprocess.run(
["docker", "inspect", container_id],
capture_output=True,
text=True,
check=True
)
# Verify labels start with "haproxy." not "easyhaproxy."
assert '"haproxy.http.host":' in result.stdout or '"haproxy.http.host"' in result.stdout
assert '"haproxy.https.host":' in result.stdout or '"haproxy.https.host"' in result.stdout
def test_haproxy_stats(self, docker_compose_changed_label):
"""Test HAProxy stats interface"""
response = requests.get(
"http://localhost:1936",
auth=("admin", "password")
)
assert response.status_code == 200
assert "Statistics Report for HAProxy" in response.text
# =============================================================================
# Helper functions for manual testing
# =============================================================================
def run_manual_test(compose_file: str, test_function):
"""
Helper function to run a test manually without pytest
Example:
def my_test():
response = requests.get("http://localhost/")
assert response.status_code == 200
run_manual_test("docker-compose.yml", my_test)
"""
fixture = DockerComposeFixture(compose_file)
try:
fixture.up()
test_function()
print("✅ Test passed!")
except AssertionError as e:
print(f"❌ Test failed: {e}")
finally:
fixture.down()
if __name__ == "__main__":
print("This is a pytest test suite. Run with: pytest test_docker_compose.py -v")
print("\nAvailable test classes:")
print(" - TestBasicSSL: Basic SSL setup tests")
print(" - TestJWTValidator: JWT validator plugin tests")
print(" - TestMultiContainers: Load balancing tests")
print(" - TestPHPFPM: PHP-FPM FastCGI tests")
print(" - TestPluginsCombined: Combined plugins tests")
print(" - TestIPWhitelist: IP whitelist plugin tests")
print(" - TestCloudflare: Cloudflare IP restoration plugin tests")
print(" - TestChangedLabel: Custom label prefix tests")

2017
tests_e2e/test_kubernetes.py Normal file

File diff suppressed because it is too large Load diff