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:
parent
b34d7822e4
commit
d66c4f8595
51 changed files with 35 additions and 35 deletions
18
tests_e2e/docker/AGENTS.md
Normal file
18
tests_e2e/docker/AGENTS.md
Normal 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.
|
||||
|
||||
|
||||
52
tests_e2e/docker/README.md
Normal file
52
tests_e2e/docker/README.md
Normal 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/)
|
||||
93
tests_e2e/docker/docker-compose-acme.yml
Normal file
93
tests_e2e/docker/docker-compose-acme.yml
Normal 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
|
||||
|
||||
65
tests_e2e/docker/docker-compose-changed-label.yml
Normal file
65
tests_e2e/docker/docker-compose-changed-label.yml
Normal file
File diff suppressed because one or more lines are too long
80
tests_e2e/docker/docker-compose-cloudflare.yml
Normal file
80
tests_e2e/docker/docker-compose-cloudflare.yml
Normal 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
|
||||
79
tests_e2e/docker/docker-compose-ip-whitelist.yml
Normal file
79
tests_e2e/docker/docker-compose-ip-whitelist.yml
Normal 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
|
||||
88
tests_e2e/docker/docker-compose-jwt-validator.yml
Normal file
88
tests_e2e/docker/docker-compose-jwt-validator.yml
Normal 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
|
||||
76
tests_e2e/docker/docker-compose-multi-containers.yml
Normal file
76
tests_e2e/docker/docker-compose-multi-containers.yml
Normal 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
|
||||
|
||||
82
tests_e2e/docker/docker-compose-php-fpm.yml
Normal file
82
tests_e2e/docker/docker-compose-php-fpm.yml
Normal 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"
|
||||
138
tests_e2e/docker/docker-compose-plugins-combined.yml
Normal file
138
tests_e2e/docker/docker-compose-plugins-combined.yml
Normal 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
|
||||
58
tests_e2e/docker/docker-compose-portainer-app-example.yml
Normal file
58
tests_e2e/docker/docker-compose-portainer-app-example.yml
Normal 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
|
||||
110
tests_e2e/docker/docker-compose-portainer.yml
Normal file
110
tests_e2e/docker/docker-compose-portainer.yml
Normal 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
|
||||
|
||||
|
||||
94
tests_e2e/docker/docker-compose.yml
Normal file
94
tests_e2e/docker/docker-compose.yml
Normal file
File diff suppressed because one or more lines are too long
151
tests_e2e/docker/php-app/README.md
Normal file
151
tests_e2e/docker/php-app/README.md
Normal 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)
|
||||
152
tests_e2e/docker/php-app/index.php
Normal file
152
tests_e2e/docker/php-app/index.php
Normal 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>
|
||||
9
tests_e2e/docker/php-app/info.php
Normal file
9
tests_e2e/docker/php-app/info.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
/**
|
||||
* PHP Info Page
|
||||
*
|
||||
* This page displays comprehensive PHP configuration information
|
||||
* including FastCGI environment variables set by EasyHAProxy.
|
||||
*/
|
||||
|
||||
phpinfo();
|
||||
106
tests_e2e/docker/php-app/test-path-info.php
Normal file
106
tests_e2e/docker/php-app/test-path-info.php
Normal 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>
|
||||
11
tests_e2e/fixtures/header-echo/Dockerfile
Normal file
11
tests_e2e/fixtures/header-echo/Dockerfile
Normal 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"]
|
||||
66
tests_e2e/fixtures/header-echo/README.md
Normal file
66
tests_e2e/fixtures/header-echo/README.md
Normal 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`
|
||||
33
tests_e2e/fixtures/header-echo/server.py
Normal file
33
tests_e2e/fixtures/header-echo/server.py
Normal 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
100
tests_e2e/generate-keys.sh
Executable 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
9
tests_e2e/kubernetes/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# kind installation directory
|
||||
.kind/
|
||||
|
||||
# kubectl config
|
||||
kubeconfig
|
||||
|
||||
# Test artifacts
|
||||
*.log
|
||||
service_tls_generated.yml
|
||||
50
tests_e2e/kubernetes/README.md
Normal file
50
tests_e2e/kubernetes/README.md
Normal 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)
|
||||
140
tests_e2e/kubernetes/cloudflare.yml
Normal file
140
tests_e2e/kubernetes/cloudflare.yml
Normal 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
|
||||
124
tests_e2e/kubernetes/ip-whitelist.yml
Normal file
124
tests_e2e/kubernetes/ip-whitelist.yml
Normal 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
|
||||
135
tests_e2e/kubernetes/jwt-validator-secret-example.yml
Normal file
135
tests_e2e/kubernetes/jwt-validator-secret-example.yml
Normal 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
|
||||
160
tests_e2e/kubernetes/jwt-validator.yml
Normal file
160
tests_e2e/kubernetes/jwt-validator.yml
Normal 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
|
||||
278
tests_e2e/kubernetes/plugins-combined.yml
Normal file
278
tests_e2e/kubernetes/plugins-combined.yml
Normal 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
|
||||
137
tests_e2e/kubernetes/service.yml
Normal file
137
tests_e2e/kubernetes/service.yml
Normal 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"
|
||||
143
tests_e2e/kubernetes/service_tls.yml
Normal file
143
tests_e2e/kubernetes/service_tls.yml
Normal 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"
|
||||
169
tests_e2e/kubernetes/setup-cluster.sh
Executable file
169
tests_e2e/kubernetes/setup-cluster.sh
Executable 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 ""
|
||||
39
tests_e2e/kubernetes/teardown-cluster.sh
Executable file
39
tests_e2e/kubernetes/teardown-cluster.sh
Executable 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
|
||||
97
tests_e2e/static/README.md
Normal file
97
tests_e2e/static/README.md
Normal 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)
|
||||
31
tests_e2e/static/conf/config-basic.yml
Normal file
31
tests_e2e/static/conf/config-basic.yml
Normal 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
|
||||
88
tests_e2e/static/conf/config-certbot.yml
Normal file
88
tests_e2e/static/conf/config-certbot.yml
Normal 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
|
||||
78
tests_e2e/static/conf/config-deny-pages.yml
Normal file
78
tests_e2e/static/conf/config-deny-pages.yml
Normal 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
|
||||
86
tests_e2e/static/conf/config-jwt-validator.yml
Normal file
86
tests_e2e/static/conf/config-jwt-validator.yml
Normal 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
|
||||
76
tests_e2e/static/docker-compose.yml
Normal file
76
tests_e2e/static/docker-compose.yml
Normal 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
67
tests_e2e/swarm/README.md
Normal 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)
|
||||
122
tests_e2e/swarm/cloudflare.yml
Normal file
122
tests_e2e/swarm/cloudflare.yml
Normal 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
|
||||
87
tests_e2e/swarm/easyhaproxy.yml
Normal file
87
tests_e2e/swarm/easyhaproxy.yml
Normal 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
|
||||
113
tests_e2e/swarm/ip-whitelist.yml
Normal file
113
tests_e2e/swarm/ip-whitelist.yml
Normal 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
|
||||
132
tests_e2e/swarm/jwt-validator.yml
Normal file
132
tests_e2e/swarm/jwt-validator.yml
Normal 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
|
||||
200
tests_e2e/swarm/plugins-combined.yml
Normal file
200
tests_e2e/swarm/plugins-combined.yml
Normal 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
|
||||
67
tests_e2e/swarm/portainer.yml
Normal file
67
tests_e2e/swarm/portainer.yml
Normal 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
|
||||
|
||||
85
tests_e2e/swarm/services.yml
Normal file
85
tests_e2e/swarm/services.yml
Normal file
File diff suppressed because one or more lines are too long
933
tests_e2e/test_docker_compose.py
Normal file
933
tests_e2e/test_docker_compose.py
Normal 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
2017
tests_e2e/test_kubernetes.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue