Merge pull request #64 from byjg/plugins
Implement Plugin System with Built-in Plugins and Comprehensive Examples
This commit is contained in:
commit
0324267adf
99 changed files with 9885 additions and 462 deletions
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
|
|
@ -170,6 +170,7 @@ jobs:
|
||||||
sed -i "s#easy-haproxy:[a-zA-Z0-9\.-]*#easy-haproxy:$TAG#g" deploy/kubernetes/easyhaproxy-*.yml
|
sed -i "s#easy-haproxy:[a-zA-Z0-9\.-]*#easy-haproxy:$TAG#g" deploy/kubernetes/easyhaproxy-*.yml
|
||||||
|
|
||||||
sed -i "s#easy-haproxy/[a-zA-Z0-9\.-]*/#easy-haproxy/$TAG/#g" docs/kubernetes.md
|
sed -i "s#easy-haproxy/[a-zA-Z0-9\.-]*/#easy-haproxy/$TAG/#g" docs/kubernetes.md
|
||||||
|
sed -i "s#easy-haproxy:[a-zA-Z0-9\.-]*#easy-haproxy:$TAG#g" docs/swarm.md
|
||||||
sed -i "s#appVersion: \"[a-zA-Z0-9\.-]*\"#appVersion: \"$TAG\"#g" helm/easyhaproxy/Chart.yaml
|
sed -i "s#appVersion: \"[a-zA-Z0-9\.-]*\"#appVersion: \"$TAG\"#g" helm/easyhaproxy/Chart.yaml
|
||||||
|
|
||||||
find examples -type f -name '*.yml' -exec sed -i "s#\(byjg/easy-haproxy:\)[a-zA-Z0-9\.-]*#\1$TAG#g" {} \; -print
|
find examples -type f -name '*.yml' -exec sed -i "s#\(byjg/easy-haproxy:\)[a-zA-Z0-9\.-]*#\1$TAG#g" {} \; -print
|
||||||
|
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -6,3 +6,12 @@ __pycache__
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
|
/examples/static/conf/config.yml
|
||||||
|
/examples/docker/certs/haproxy/.place_holder_cert.pem
|
||||||
|
/examples/static/host1.local.pem
|
||||||
|
/examples/swarm/certs/host1.local.pem
|
||||||
|
/examples/docker/host2.local.pem
|
||||||
|
/examples/swarm/certs/host2.local.pem
|
||||||
|
/examples/docker/jwt_private.pem
|
||||||
|
/examples/docker/jwt_pubkey.pem
|
||||||
|
/examples/docker/cloudflare_ips.lst
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -6,4 +6,4 @@ build:
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
pytest tests/
|
cd src/ && pytest tests/ -vv
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,13 @@ Detailed configuration guides for advanced setups:
|
||||||
- [Container Labels](docs/container-labels.md) - Configure Docker/Swarm containers with labels
|
- [Container Labels](docs/container-labels.md) - Configure Docker/Swarm containers with labels
|
||||||
- [Environment Variables](docs/environment-variable.md) - Configure EasyHAProxy behavior
|
- [Environment Variables](docs/environment-variable.md) - Configure EasyHAProxy behavior
|
||||||
- [Volumes](docs/volumes.md) - Map volumes for certificates, config, and custom files
|
- [Volumes](docs/volumes.md) - Map volumes for certificates, config, and custom files
|
||||||
|
- [Plugins](docs/plugins.md) - Extend HAProxy with plugins ([Development Guide](docs/plugin-development.md))
|
||||||
|
- [JWT Validator](docs/Plugins/jwt-validator.md) - JWT authentication validation
|
||||||
|
- [FastCGI](docs/Plugins/fastcgi.md) - PHP-FPM and FastCGI application support
|
||||||
|
- [Cloudflare](docs/Plugins/cloudflare.md) - Restore visitor IP from Cloudflare CDN
|
||||||
|
- [IP Whitelist](docs/Plugins/ip-whitelist.md) - Restrict access to IPs/CIDR ranges
|
||||||
|
- [Deny Pages](docs/Plugins/deny-pages.md) - Block access to specific paths
|
||||||
|
- [Cleanup](docs/Plugins/cleanup.md) - Automatic cleanup of temporary files
|
||||||
- [Other Configurations](docs/other.md) - Additional configurations (ports, custom errors, etc.)
|
- [Other Configurations](docs/other.md) - Additional configurations (ports, custom errors, etc.)
|
||||||
- [Limitations](docs/limitations.md) - Important limitations and considerations
|
- [Limitations](docs/limitations.md) - Important limitations and considerations
|
||||||
|
|
||||||
|
|
|
||||||
350
RELEASE.md
Normal file
350
RELEASE.md
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
# EasyHAProxy Release Guide
|
||||||
|
|
||||||
|
This guide explains how to create a new release of EasyHAProxy, including Docker images, Helm charts, and documentation updates.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Release Process](#release-process)
|
||||||
|
- [Automated Release (Recommended)](#automated-release-recommended)
|
||||||
|
- [Manual Release](#manual-release)
|
||||||
|
- [Helm Chart Release](#helm-chart-release)
|
||||||
|
- [Post-Release Checklist](#post-release-checklist)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The EasyHAProxy release process uses GitHub Actions to automatically:
|
||||||
|
- Run tests
|
||||||
|
- Build multi-architecture Docker images (amd64, arm64)
|
||||||
|
- Publish Docker images to Docker Hub
|
||||||
|
- Update Helm chart versions
|
||||||
|
- Publish Helm charts
|
||||||
|
- Update documentation
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before creating a release, ensure you have:
|
||||||
|
|
||||||
|
1. **Permissions:**
|
||||||
|
- Write access to the GitHub repository
|
||||||
|
- Docker Hub credentials (for maintainers)
|
||||||
|
- Access to GitHub secrets (for CI/CD)
|
||||||
|
|
||||||
|
2. **Local Setup:**
|
||||||
|
- Git configured with your credentials
|
||||||
|
- Docker installed (for local testing)
|
||||||
|
- Python 3.x with pytest (for running tests)
|
||||||
|
|
||||||
|
3. **Repository Secrets (for maintainers):**
|
||||||
|
- `DOCKER_REGISTRY`: Docker Hub registry URL
|
||||||
|
- `DOCKER_REGISTRY_USER`: Docker Hub username
|
||||||
|
- `DOCKER_REGISTRY_TOKEN`: Docker Hub access token
|
||||||
|
- `DOC_TOKEN`: GitHub token for documentation updates
|
||||||
|
|
||||||
|
## Release Process
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
EasyHAProxy follows [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
|
- **MAJOR.MINOR.PATCH** (e.g., `4.6.0`)
|
||||||
|
- **MAJOR**: Breaking changes or major architectural updates
|
||||||
|
- **MINOR**: New features, plugin additions, backward-compatible changes
|
||||||
|
- **PATCH**: Bug fixes, documentation updates, minor improvements
|
||||||
|
|
||||||
|
**Current Version:** `4.6.0` (as of Chart.yaml)
|
||||||
|
|
||||||
|
## Automated Release (Recommended)
|
||||||
|
|
||||||
|
The automated release process is triggered by pushing a semantic version tag.
|
||||||
|
|
||||||
|
### Step 1: Prepare the Release
|
||||||
|
|
||||||
|
1. **Ensure all changes are committed and pushed:**
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
git add .
|
||||||
|
git commit -m "Prepare release X.Y.Z"
|
||||||
|
git push origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run tests locally:**
|
||||||
|
```bash
|
||||||
|
cd src/
|
||||||
|
pytest tests/ -vv
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build and test Docker image locally:**
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
# Or manually:
|
||||||
|
docker build -t byjg/easy-haproxy:local -f build/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create and Push a Release Tag
|
||||||
|
|
||||||
|
1. **Create a new semantic version tag:**
|
||||||
|
```bash
|
||||||
|
# For a new minor version (new features)
|
||||||
|
git tag 4.7.0
|
||||||
|
|
||||||
|
# For a patch version (bug fixes)
|
||||||
|
git tag 4.6.1
|
||||||
|
|
||||||
|
# For a major version (breaking changes)
|
||||||
|
git tag 5.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Push the tag to GitHub:**
|
||||||
|
```bash
|
||||||
|
git push origin 4.7.0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Monitor the GitHub Actions workflow:**
|
||||||
|
- Go to: https://github.com/byjg/docker-easy-haproxy/actions
|
||||||
|
- Watch the "Docker" workflow progress
|
||||||
|
- Verify all jobs complete successfully:
|
||||||
|
- ✅ Test
|
||||||
|
- ✅ Build (multi-arch)
|
||||||
|
- ✅ Helm
|
||||||
|
- ✅ HelmDeploy
|
||||||
|
- ✅ Documentation
|
||||||
|
|
||||||
|
### Step 3: What Happens Automatically
|
||||||
|
|
||||||
|
When you push a semantic version tag, GitHub Actions will:
|
||||||
|
|
||||||
|
1. **Run Tests** (`Test` job):
|
||||||
|
- Install Python dependencies
|
||||||
|
- Run pytest on all tests
|
||||||
|
|
||||||
|
2. **Build Multi-Arch Docker Images** (`Build` job):
|
||||||
|
- Build for `linux/amd64` and `linux/arm64`
|
||||||
|
- Tag image with version number (e.g., `byjg/easy-haproxy:4.7.0`)
|
||||||
|
- Push to Docker Hub
|
||||||
|
|
||||||
|
3. **Update Versions** (`Helm` job):
|
||||||
|
- Update `helm/easyhaproxy/Chart.yaml`:
|
||||||
|
- `appVersion`: Set to new version (e.g., `4.7.0`)
|
||||||
|
- `version`: Auto-increment patch version (e.g., `0.1.9` → `0.1.10`)
|
||||||
|
- Update all version references in:
|
||||||
|
- `deploy/docker/docker-compose.yml`
|
||||||
|
- `deploy/kubernetes/easyhaproxy-*.yml`
|
||||||
|
- `docs/kubernetes.md`
|
||||||
|
- `examples/*/*.yml`
|
||||||
|
- Commit and push changes with message: `[skip ci] Update from X.Y.Z to A.B.C`
|
||||||
|
|
||||||
|
4. **Publish Helm Chart** (`HelmDeploy` job):
|
||||||
|
- Package Helm chart
|
||||||
|
- Publish to Helm repository at https://opensource.byjg.com/helm/
|
||||||
|
|
||||||
|
5. **Update Documentation** (`Documentation` job):
|
||||||
|
- Publish documentation updates
|
||||||
|
|
||||||
|
### Step 4: Verify the Release
|
||||||
|
|
||||||
|
1. **Check Docker Hub:**
|
||||||
|
```bash
|
||||||
|
docker pull byjg/easy-haproxy:4.7.0
|
||||||
|
docker images | grep easy-haproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify Helm chart:**
|
||||||
|
```bash
|
||||||
|
helm repo add byjg https://opensource.byjg.com/helm
|
||||||
|
helm repo update
|
||||||
|
helm search repo easyhaproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create GitHub Release:**
|
||||||
|
- Go to: https://github.com/byjg/docker-easy-haproxy/releases/new
|
||||||
|
- Select the tag you created
|
||||||
|
- Generate release notes
|
||||||
|
- Add highlights of changes
|
||||||
|
- Publish release
|
||||||
|
|
||||||
|
## Manual Release
|
||||||
|
|
||||||
|
For emergency releases or when CI/CD is unavailable.
|
||||||
|
|
||||||
|
### Manual Docker Build (Multi-Arch)
|
||||||
|
|
||||||
|
1. **Set up environment:**
|
||||||
|
```bash
|
||||||
|
export DOCKER_USERNAME=your-username
|
||||||
|
export DOCKER_PASSWORD=your-token
|
||||||
|
export DOCKER_REGISTRY=docker.io
|
||||||
|
export VERSIONS="4.7.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run multi-arch build:**
|
||||||
|
```bash
|
||||||
|
./build-multiarch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script uses `buildah` and `podman` to create multi-architecture images.
|
||||||
|
|
||||||
|
### Manual Helm Chart Update
|
||||||
|
|
||||||
|
1. **Update Chart.yaml:**
|
||||||
|
```bash
|
||||||
|
cd helm/easyhaproxy/
|
||||||
|
|
||||||
|
# Update appVersion
|
||||||
|
sed -i 's/appVersion: ".*"/appVersion: "4.7.0"/' Chart.yaml
|
||||||
|
|
||||||
|
# Increment chart version
|
||||||
|
# From: version: 0.1.9
|
||||||
|
# To: version: 0.1.10
|
||||||
|
nano Chart.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Package and publish Helm chart:**
|
||||||
|
```bash
|
||||||
|
helm package helm/easyhaproxy/
|
||||||
|
# Follow your Helm repository's publishing process
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helm Chart Release
|
||||||
|
|
||||||
|
The Helm chart version is automatically managed by CI/CD, but you can manually control it:
|
||||||
|
|
||||||
|
### Helm Chart Version Strategy
|
||||||
|
|
||||||
|
- **Chart version** (`version` in Chart.yaml):
|
||||||
|
- Auto-incremented by CI/CD (patch version)
|
||||||
|
- Format: `0.1.X` where X increments with each Docker release
|
||||||
|
- Manual override: Edit Chart.yaml before tagging
|
||||||
|
|
||||||
|
- **App version** (`appVersion` in Chart.yaml):
|
||||||
|
- Set to Docker image version (e.g., `4.7.0`)
|
||||||
|
- Automatically updated by CI/CD
|
||||||
|
|
||||||
|
### Current Helm Chart
|
||||||
|
|
||||||
|
- **Chart Version:** `0.1.9`
|
||||||
|
- **App Version:** `4.6.0`
|
||||||
|
- **Repository:** https://opensource.byjg.com/helm/
|
||||||
|
|
||||||
|
## Post-Release Checklist
|
||||||
|
|
||||||
|
After a successful release:
|
||||||
|
|
||||||
|
- [ ] Verify Docker image on Docker Hub
|
||||||
|
- [ ] Test Docker image: `docker run byjg/easy-haproxy:X.Y.Z --version`
|
||||||
|
- [ ] Verify Helm chart availability
|
||||||
|
- [ ] Test Helm installation
|
||||||
|
- [ ] Create GitHub Release with changelog
|
||||||
|
- [ ] Update project README if needed
|
||||||
|
- [ ] Announce release (if major/minor)
|
||||||
|
- [ ] Update dependent projects (if applicable)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Fails
|
||||||
|
|
||||||
|
**Problem:** GitHub Actions build job fails
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Check test output in GitHub Actions logs
|
||||||
|
2. Run tests locally: `cd src/ && pytest tests/ -vv`
|
||||||
|
3. Fix failing tests and push changes
|
||||||
|
4. Delete and recreate tag:
|
||||||
|
```bash
|
||||||
|
git tag -d 4.7.0
|
||||||
|
git push origin :refs/tags/4.7.0
|
||||||
|
git tag 4.7.0
|
||||||
|
git push origin 4.7.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Push Fails
|
||||||
|
|
||||||
|
**Problem:** Cannot push to Docker Hub
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify Docker Hub credentials in GitHub secrets
|
||||||
|
2. Check Docker Hub token permissions
|
||||||
|
3. Ensure image name matches: `byjg/easy-haproxy`
|
||||||
|
|
||||||
|
### Helm Chart Not Published
|
||||||
|
|
||||||
|
**Problem:** Helm chart doesn't appear in repository
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Check `HelmDeploy` job logs in GitHub Actions
|
||||||
|
2. Verify `DOC_TOKEN` secret is valid
|
||||||
|
3. Wait a few minutes for chart to propagate
|
||||||
|
4. Clear Helm cache: `helm repo update`
|
||||||
|
|
||||||
|
### Version Not Updated
|
||||||
|
|
||||||
|
**Problem:** Version references not updated in docs/examples
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Check `Helm` job logs for sed command errors
|
||||||
|
2. Verify commit was pushed with `[skip ci]` message
|
||||||
|
3. Manually update version references if needed:
|
||||||
|
```bash
|
||||||
|
find examples -type f -name '*.yml' -exec sed -i "s/\(byjg\/easy-haproxy:\)[0-9\.]*/\1X.Y.Z/g" {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Arch Build Issues
|
||||||
|
|
||||||
|
**Problem:** ARM64 build fails
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify QEMU is set up in GitHub Actions
|
||||||
|
2. Check build logs for architecture-specific errors
|
||||||
|
3. Test locally with Docker Buildx:
|
||||||
|
```bash
|
||||||
|
docker buildx create --use
|
||||||
|
docker buildx build --platform linux/amd64,linux/arm64 -t test .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local build
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cd src/ && pytest tests/ -vv
|
||||||
|
|
||||||
|
# Create release tag
|
||||||
|
git tag 4.7.0 && git push origin 4.7.0
|
||||||
|
|
||||||
|
# Pull specific version
|
||||||
|
docker pull byjg/easy-haproxy:4.7.0
|
||||||
|
|
||||||
|
# Install Helm chart
|
||||||
|
helm install easyhaproxy byjg/easyhaproxy --version 0.1.10
|
||||||
|
|
||||||
|
# Check Helm chart info
|
||||||
|
helm show chart byjg/easyhaproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important URLs
|
||||||
|
|
||||||
|
- **GitHub Repository:** https://github.com/byjg/docker-easy-haproxy
|
||||||
|
- **Docker Hub:** https://hub.docker.com/r/byjg/easy-haproxy
|
||||||
|
- **Helm Repository:** https://opensource.byjg.com/helm/
|
||||||
|
- **Documentation:** https://opensource.byjg.com/devops/docker-easy-haproxy/
|
||||||
|
- **GitHub Actions:** https://github.com/byjg/docker-easy-haproxy/actions
|
||||||
|
|
||||||
|
### Version History
|
||||||
|
|
||||||
|
| Version | Release Date | Type | Highlights |
|
||||||
|
|---------|-------------|------|------------|
|
||||||
|
| 4.6.0 | 2024-11-27 | Minor | FastCGI plugin, JWT enhancements |
|
||||||
|
| 4.5.0 | 2024-XX-XX | Minor | Previous release |
|
||||||
|
| ... | ... | ... | ... |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need Help?**
|
||||||
|
- Open an issue: https://github.com/byjg/docker-easy-haproxy/issues
|
||||||
|
- Check documentation: https://opensource.byjg.com/devops/docker-easy-haproxy/
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
easyhaproxy:
|
easyhaproxy:
|
||||||
image: byjg/easy-haproxy:4.6.0
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
|
|
||||||
84
docs/Plugins/cleanup.md
Normal file
84
docs/Plugins/cleanup.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cleanup Plugin
|
||||||
|
|
||||||
|
**Type:** Global Plugin
|
||||||
|
**Runs:** Once per discovery cycle
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Cleanup plugin performs cleanup tasks during each discovery cycle, such as removing old temporary files.
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Prevents disk space issues by automatically cleaning up temporary files created by EasyHAProxy.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|----------------------|----------------------------------------------|---------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `max_idle_time` | Maximum age in seconds before deleting files | `300` |
|
||||||
|
| `cleanup_temp_files` | Enable temp file cleanup | `true` |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
plugins:
|
||||||
|
enabled: [cleanup]
|
||||||
|
config:
|
||||||
|
cleanup:
|
||||||
|
max_idle_time: 600
|
||||||
|
cleanup_temp_files: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure the Cleanup plugin globally:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|-------------------------------------------------|----------------------|----------|---------|----------------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGINS_ENABLED` | - | string | - | Enable cleanup plugin (value: `cleanup`) |
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLEANUP_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin |
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME` | `max_idle_time` | integer | `300` | Maximum age in seconds before deleting files |
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLEANUP_CLEANUP_TEMP_FILES` | `cleanup_temp_files` | boolean | `true` | Enable temp file cleanup |
|
||||||
|
|
||||||
|
**Note:** This is a global plugin - configuration applies to the entire system.
|
||||||
|
|
||||||
|
### Custom Idle Time (1 hour)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
plugins:
|
||||||
|
enabled: [cleanup]
|
||||||
|
config:
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 3600 # 1 hour
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The cleanup plugin:
|
||||||
|
- Runs once during each discovery cycle
|
||||||
|
- Scans temporary directories for old files
|
||||||
|
- Removes files older than `max_idle_time` seconds
|
||||||
|
- Helps maintain disk space efficiency
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- This is a **global plugin** - it runs once per discovery cycle, not per domain
|
||||||
|
- Does not generate HAProxy configuration
|
||||||
|
- Performs maintenance operations in the background
|
||||||
|
- Safe to enable in production environments
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Environment Variables Reference](../environment-variable.md)
|
||||||
|
- [Static Configuration Reference](../static.md)
|
||||||
129
docs/Plugins/cloudflare.md
Normal file
129
docs/Plugins/cloudflare.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 18
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cloudflare Plugin
|
||||||
|
|
||||||
|
**Type:** Domain Plugin
|
||||||
|
**Runs:** Once for each discovered domain/host
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Cloudflare plugin restores the original visitor IP address when requests come through Cloudflare's CDN. The plugin includes **built-in Cloudflare IP ranges** that are automatically written to the IP list file - no manual configuration required!
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Cloudflare replaces the visitor's IP with its own. This plugin restores the original IP from the `CF-Connecting-IP` header.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|-------------------|------------------------------------------|-----------------------------------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `use_builtin_ips` | Use built-in Cloudflare IP ranges | `true` |
|
||||||
|
| `ip_list_path` | Path to Cloudflare IP list | `/etc/haproxy/cloudflare_ips.lst` |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Basic - Uses Built-in IPs)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
myapp:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: example.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare
|
||||||
|
# Built-in Cloudflare IPs are automatically used - no additional configuration needed!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Custom IP List)
|
||||||
|
|
||||||
|
If you want to use your own IP list file instead of the built-in ranges:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.plugins: cloudflare
|
||||||
|
easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false
|
||||||
|
easyhaproxy.http.plugin.cloudflare.ip_list_path: /custom/path/cf_ips.lst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Annotations
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
easyhaproxy.plugins: "cloudflare"
|
||||||
|
easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: myapp
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
plugins:
|
||||||
|
config:
|
||||||
|
cloudflare:
|
||||||
|
enabled: true
|
||||||
|
use_builtin_ips: true # Uses built-in Cloudflare IPs (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure Cloudflare plugin defaults for all domains:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|-------------------------------------------------|-------------------|----------|-----------------------------------|---------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_USE_BUILTIN_IPS` | `use_builtin_ips` | boolean | `true` | Use built-in Cloudflare IP ranges |
|
||||||
|
| `EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH` | `ip_list_path` | string | `/etc/haproxy/cloudflare_ips.lst` | Path to Cloudflare IP list file |
|
||||||
|
|
||||||
|
**Note:** Environment variables set defaults for ALL domains. To enable/disable per-domain, use container labels or Kubernetes annotations.
|
||||||
|
|
||||||
|
## Generated HAProxy Configuration
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# Cloudflare - Restore original visitor IP
|
||||||
|
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
|
||||||
|
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Cloudflare IP Ranges
|
||||||
|
|
||||||
|
The plugin includes the current Cloudflare IP ranges (22 ranges total):
|
||||||
|
|
||||||
|
**IPv4 Ranges (15):**
|
||||||
|
- 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22
|
||||||
|
- 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20
|
||||||
|
- 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13
|
||||||
|
- 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22
|
||||||
|
|
||||||
|
**IPv6 Ranges (7):**
|
||||||
|
- 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32
|
||||||
|
- 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32
|
||||||
|
|
||||||
|
These ranges are automatically written to `/etc/haproxy/cloudflare_ips.lst` during each discovery cycle.
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- ✅ **No manual configuration required** - Built-in Cloudflare IPs are included!
|
||||||
|
- The plugin runs once per domain during the discovery cycle
|
||||||
|
- IP list file is automatically created and updated
|
||||||
|
- To update Cloudflare IPs in the future, simply update the plugin source code and rebuild
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Container Labels Reference](../container-labels.md)
|
||||||
129
docs/Plugins/deny-pages.md
Normal file
129
docs/Plugins/deny-pages.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deny Pages Plugin
|
||||||
|
|
||||||
|
**Type:** Domain Plugin
|
||||||
|
**Runs:** Once for each discovered domain/host
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Deny Pages plugin blocks access to specific paths for a domain, returning a configurable HTTP status code.
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Protect admin panels, internal APIs, or debugging endpoints from public access.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|---------------|----------------------------------------|------------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `paths` | Comma-separated list of paths to block | (required) |
|
||||||
|
| `status_code` | HTTP status code to return | `403` |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Basic)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: example.com
|
||||||
|
easyhaproxy.http.plugins: deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/private,/debug
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### WordPress Protection
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: wordpress.example.com
|
||||||
|
easyhaproxy.http.plugins: deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /wp-admin,/wp-login.php,/.env
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Annotations
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
easyhaproxy.plugins: "deny_pages"
|
||||||
|
easyhaproxy.plugin.deny_pages.paths: "/admin,/private"
|
||||||
|
easyhaproxy.plugin.deny_pages.status_code: "403"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: webapp
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
easymapping:
|
||||||
|
- host: example.com
|
||||||
|
port: 80
|
||||||
|
container: webapp:80
|
||||||
|
plugins:
|
||||||
|
- deny_pages
|
||||||
|
plugin_config:
|
||||||
|
deny_pages:
|
||||||
|
paths: /admin,/private,/debug
|
||||||
|
status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Plugins (with Cloudflare)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: secure-app.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/config
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure Deny Pages plugin defaults for all domains:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|---------------------------------------------|---------------|---------|---------|----------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGIN_DENY_PAGES_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
|
| `EASYHAPROXY_PLUGIN_DENY_PAGES_PATHS` | `paths` | string | - | Comma-separated list of paths to block |
|
||||||
|
| `EASYHAPROXY_PLUGIN_DENY_PAGES_STATUS_CODE` | `status_code` | integer | `403` | HTTP status code to return |
|
||||||
|
|
||||||
|
**Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations.
|
||||||
|
|
||||||
|
## Generated HAProxy Configuration
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# Deny Pages - Block specific paths
|
||||||
|
acl denied_path path_beg /admin /private /debug
|
||||||
|
http-request deny deny_status 404 if denied_path
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The plugin runs once per domain during the discovery cycle
|
||||||
|
- Path matching uses `path_beg` (prefix matching), so `/admin` blocks `/admin/*` too
|
||||||
|
- Consider using `404` instead of `403` to hide the existence of blocked paths
|
||||||
|
- Works well in combination with other security plugins
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Container Labels Reference](../container-labels.md)
|
||||||
177
docs/Plugins/fastcgi.md
Normal file
177
docs/Plugins/fastcgi.md
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 17
|
||||||
|
---
|
||||||
|
|
||||||
|
# FastCGI Plugin
|
||||||
|
|
||||||
|
**Type:** Domain Plugin
|
||||||
|
**Runs:** Once for each discovered domain/host
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The FastCGI plugin configures HAProxy to communicate with PHP-FPM and other FastCGI applications. It automatically generates the necessary HAProxy `fcgi-app` configuration that defines CGI parameters for proper PHP-FPM communication.
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Automatically generates HAProxy `fcgi-app` configuration that defines required CGI parameters for PHP-FPM communication without manual HAProxy configuration.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|-------------------|-----------------------------------------|------------------------------------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `document_root` | Document root path | `/var/www/html` |
|
||||||
|
| `script_filename` | Custom pattern for SCRIPT_FILENAME | `%[path]` (uses HAProxy's default) |
|
||||||
|
| `index_file` | Default index file | `index.php` |
|
||||||
|
| `path_info` | Enable PATH_INFO support | `true` |
|
||||||
|
| `custom_params` | Dictionary of custom FastCGI parameters | (optional) |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Docker/Docker Compose (TCP connection)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
php-fpm:
|
||||||
|
image: php:8.2-fpm
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: phpapp.local
|
||||||
|
easyhaproxy.http.port: 80
|
||||||
|
easyhaproxy.http.localport: 9000
|
||||||
|
easyhaproxy.http.proto: fcgi
|
||||||
|
easyhaproxy.http.plugins: fastcgi
|
||||||
|
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html
|
||||||
|
easyhaproxy.http.plugin.fastcgi.index_file: index.php
|
||||||
|
volumes:
|
||||||
|
- ./app:/var/www/html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Unix socket)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
php-fpm:
|
||||||
|
image: php:8.2-fpm
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: phpapp.local
|
||||||
|
easyhaproxy.http.socket: /run/php/php-fpm.sock
|
||||||
|
easyhaproxy.http.proto: fcgi
|
||||||
|
easyhaproxy.http.plugins: fastcgi
|
||||||
|
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html
|
||||||
|
easyhaproxy.http.plugin.fastcgi.index_file: index.php
|
||||||
|
volumes:
|
||||||
|
- ./app:/var/www/html
|
||||||
|
- /run/php:/run/php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Document Root and Index File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.plugins: fastcgi
|
||||||
|
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/myapp/public
|
||||||
|
easyhaproxy.http.plugin.fastcgi.index_file: app.php
|
||||||
|
easyhaproxy.http.plugin.fastcgi.path_info: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Annotations
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
easyhaproxy.plugins: "fastcgi"
|
||||||
|
easyhaproxy.plugin.fastcgi.document_root: "/var/www/html"
|
||||||
|
easyhaproxy.plugin.fastcgi.index_file: "index.php"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: phpapp.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: php-fpm
|
||||||
|
port:
|
||||||
|
number: 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
easymapping:
|
||||||
|
- host: phpapp.local
|
||||||
|
port: 80
|
||||||
|
container: php-fpm:9000
|
||||||
|
proto: fcgi
|
||||||
|
plugins:
|
||||||
|
- fastcgi
|
||||||
|
plugin_config:
|
||||||
|
fastcgi:
|
||||||
|
document_root: /var/www/html
|
||||||
|
index_file: index.php
|
||||||
|
path_info: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure FastCGI plugin defaults for all domains:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|----------------------------------------------|-------------------|----------|-----------------|---------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGIN_FASTCGI_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
|
| `EASYHAPROXY_PLUGIN_FASTCGI_DOCUMENT_ROOT` | `document_root` | string | `/var/www/html` | Document root path |
|
||||||
|
| `EASYHAPROXY_PLUGIN_FASTCGI_SCRIPT_FILENAME` | `script_filename` | string | `%[path]` | Custom pattern for SCRIPT_FILENAME |
|
||||||
|
| `EASYHAPROXY_PLUGIN_FASTCGI_INDEX_FILE` | `index_file` | string | `index.php` | Default index file |
|
||||||
|
| `EASYHAPROXY_PLUGIN_FASTCGI_PATH_INFO` | `path_info` | boolean | `true` | Enable PATH_INFO support |
|
||||||
|
|
||||||
|
**Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations. Custom params (`custom_params`) cannot be configured via environment variables - use YAML or labels instead.
|
||||||
|
|
||||||
|
## Generated HAProxy Configuration
|
||||||
|
|
||||||
|
The plugin generates a top-level `fcgi-app` section and a `use-fcgi-app` directive in the backend:
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# Top-level fcgi-app definition (added after defaults, before frontends/backends)
|
||||||
|
fcgi-app fcgi_phpapp_local
|
||||||
|
docroot /var/www/html
|
||||||
|
index index.php
|
||||||
|
path-info ^(/.+\.php)(/.*)?$
|
||||||
|
|
||||||
|
# Backend configuration (added to the backend section)
|
||||||
|
backend srv_phpapp_local_80
|
||||||
|
use-fcgi-app fcgi_phpapp_local
|
||||||
|
# TCP connection:
|
||||||
|
server srv-0 172.19.0.3:9000 proto fcgi
|
||||||
|
# OR Unix socket:
|
||||||
|
# server srv-0 /run/php/php-fpm.sock proto fcgi
|
||||||
|
```
|
||||||
|
|
||||||
|
## CGI Parameters
|
||||||
|
|
||||||
|
**Note:** HAProxy automatically sets standard CGI parameters based on the `fcgi-app` configuration when communicating with PHP-FPM via the FastCGI protocol.
|
||||||
|
|
||||||
|
The plugin configures:
|
||||||
|
- ✅ **SCRIPT_FILENAME** - Path to PHP script
|
||||||
|
- ✅ **DOCUMENT_ROOT** - Document root directory
|
||||||
|
- ✅ **SCRIPT_NAME** - Script name from URL
|
||||||
|
- ✅ **REQUEST_URI** - Full request URI with query string
|
||||||
|
- ✅ **QUERY_STRING** - URL query parameters
|
||||||
|
- ✅ **REQUEST_METHOD** - HTTP method (GET, POST, etc.)
|
||||||
|
- ✅ **CONTENT_TYPE & CONTENT_LENGTH** - Request body info
|
||||||
|
- ✅ **SERVER_NAME & SERVER_PORT** - Server details
|
||||||
|
- ✅ **HTTPS** - SSL/TLS status
|
||||||
|
- ✅ **PATH_INFO** - Path information (optional)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Required:** Use this plugin together with `proto: fcgi` parameter for complete PHP-FPM support
|
||||||
|
- The plugin runs once per domain during the discovery cycle
|
||||||
|
- HAProxy handles the actual FastCGI protocol communication and CGI parameter transmission
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Container Labels Reference](../container-labels.md)
|
||||||
126
docs/Plugins/ip-whitelist.md
Normal file
126
docs/Plugins/ip-whitelist.md
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 19
|
||||||
|
---
|
||||||
|
|
||||||
|
# IP Whitelist Plugin
|
||||||
|
|
||||||
|
**Type:** Domain Plugin
|
||||||
|
**Runs:** Once for each discovered domain/host
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The IP Whitelist plugin restricts access to a domain to only specific IP addresses or CIDR ranges.
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Restrict access to internal tools, admin panels, or staging environments to only trusted IP addresses.
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|---------------|--------------------------------------------------|------------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `allowed_ips` | Comma-separated list of IPs/CIDR ranges to allow | (required) |
|
||||||
|
| `status_code` | HTTP status code to return for blocked IPs | `403` |
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Basic)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
admin:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: admin.example.com
|
||||||
|
easyhaproxy.http.plugins: ip_whitelist
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 192.168.1.0/24,10.0.0.5
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Office Network Access
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: admin.example.com
|
||||||
|
easyhaproxy.http.plugins: ip_whitelist
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 203.0.113.0/24,198.51.100.42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Annotations
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
easyhaproxy.plugins: "ip_whitelist"
|
||||||
|
easyhaproxy.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.5"
|
||||||
|
easyhaproxy.plugin.ip_whitelist.status_code: "403"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: admin.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: admin-panel
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
easymapping:
|
||||||
|
- host: admin.example.com
|
||||||
|
port: 443
|
||||||
|
container: admin-panel:443
|
||||||
|
plugins:
|
||||||
|
- ip_whitelist
|
||||||
|
plugin_config:
|
||||||
|
ip_whitelist:
|
||||||
|
allowed_ips: 192.168.1.0/24,10.0.0.5
|
||||||
|
status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure IP Whitelist plugin defaults for all domains:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|-----------------------------------------------|---------------|----------|---------|--------------------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGIN_IP_WHITELIST_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
|
| `EASYHAPROXY_PLUGIN_IP_WHITELIST_ALLOWED_IPS` | `allowed_ips` | string | - | Comma-separated list of IPs/CIDR ranges to allow |
|
||||||
|
| `EASYHAPROXY_PLUGIN_IP_WHITELIST_STATUS_CODE` | `status_code` | integer | `403` | HTTP status code to return for blocked IPs |
|
||||||
|
|
||||||
|
**Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations.
|
||||||
|
|
||||||
|
## Generated HAProxy Configuration
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src 192.168.1.0/24 10.0.0.5
|
||||||
|
http-request deny deny_status 403 if !whitelisted_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
## IP Address Formats
|
||||||
|
|
||||||
|
The plugin supports:
|
||||||
|
- **Single IPs:** `10.0.0.5`, `203.0.113.42`
|
||||||
|
- **CIDR ranges:** `192.168.1.0/24`, `10.0.0.0/8`
|
||||||
|
- **Multiple entries:** Comma-separated list of IPs and/or CIDR ranges
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Warning:** This blocks ALL IPs except those in the whitelist. Make sure to include your own IP!
|
||||||
|
- The plugin runs once per domain during the discovery cycle
|
||||||
|
- Test thoroughly before deploying to production
|
||||||
|
- Consider using VPN CIDR ranges for remote access
|
||||||
|
- Works well with staging and admin environments
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Container Labels Reference](../container-labels.md)
|
||||||
309
docs/Plugins/jwt-validator.md
Normal file
309
docs/Plugins/jwt-validator.md
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 16
|
||||||
|
---
|
||||||
|
|
||||||
|
# JWT Validator Plugin
|
||||||
|
|
||||||
|
**Type:** Domain Plugin
|
||||||
|
**Runs:** Once for each discovered domain/host
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The JWT Validator plugin validates JWT (JSON Web Token) authentication tokens using HAProxy's built-in JWT functionality.
|
||||||
|
|
||||||
|
## Why Use It
|
||||||
|
|
||||||
|
Protect APIs and services with JWT authentication without needing application-level code.
|
||||||
|
|
||||||
|
## Generating JWT Keys
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|-------------------|----------------------------------------------------------------------------------------------|-------------|
|
||||||
|
| `enabled` | Enable/disable plugin | `true` |
|
||||||
|
| `algorithm` | JWT signing algorithm | `RS256` |
|
||||||
|
| `issuer` | Expected JWT issuer (optional, set to `none`/`null` to skip validation) | (optional) |
|
||||||
|
| `audience` | Expected JWT audience (optional, set to `none`/`null` to skip validation) | (optional) |
|
||||||
|
| `pubkey_path` | Path to public key file (required if `pubkey` not provided) | (required) |
|
||||||
|
| `pubkey` | Public key content as base64-encoded string (required if `pubkey_path` not provided) | (optional) |
|
||||||
|
| `paths` | List of paths that require JWT validation (optional) | (all paths) |
|
||||||
|
| `only_paths` | If `true`, only specified paths are accessible; if `false`, only specified paths require JWT | `false` |
|
||||||
|
| `allow_anonymous` | If `true`, allows requests without Authorization header (validates JWT if present) | `false` |
|
||||||
|
|
||||||
|
## Path Validation Logic
|
||||||
|
|
||||||
|
- **No paths configured:** ALL requests to the domain require JWT validation (default behavior)
|
||||||
|
- **Paths configured + `only_paths=false`:** Only specified paths require JWT validation, other paths pass through without validation
|
||||||
|
- **Paths configured + `only_paths=true`:** Only specified paths are accessible (with JWT validation), all other paths are denied
|
||||||
|
|
||||||
|
## Anonymous Access Logic
|
||||||
|
|
||||||
|
- **`allow_anonymous=false` (default):** Requests without `Authorization` header are denied with "Missing Authorization HTTP header"
|
||||||
|
- **`allow_anonymous=true`:** Requests without `Authorization` header are allowed to pass through, but JWTs are validated if the header is present
|
||||||
|
|
||||||
|
**Use Cases for `allow_anonymous=true`:**
|
||||||
|
- Optional authentication (show different content for authenticated vs anonymous users)
|
||||||
|
- Mixed public/private content where some users have enhanced access with JWT
|
||||||
|
- Gradual JWT authentication rollout
|
||||||
|
- Public APIs that provide additional features to authenticated users
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Docker/Docker Compose (Protect All Paths)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: api.example.com
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
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
|
||||||
|
volumes:
|
||||||
|
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect Specific Paths Only
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.only_paths: false
|
||||||
|
# /api/health, /api/docs, etc. remain publicly accessible
|
||||||
|
```
|
||||||
|
|
||||||
|
### Only Allow Specific Paths
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.paths: /api/public,/api/v1
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.only_paths: true
|
||||||
|
# All paths except /api/public and /api/v1 are denied
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skip Issuer/Audience Validation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.issuer: none
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.audience: none
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Allow Anonymous Access (Optional JWT)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: api.example.com
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true
|
||||||
|
volumes:
|
||||||
|
- ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
# Requests without Authorization header are allowed
|
||||||
|
# Requests with Authorization header are validated
|
||||||
|
# Invalid JWTs are rejected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Annotations
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
easyhaproxy.plugins: "jwt_validator"
|
||||||
|
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"
|
||||||
|
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
|
||||||
|
easyhaproxy.plugin.jwt_validator.only_paths: "false"
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: api.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: api-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static YAML Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
easymapping:
|
||||||
|
- host: api.example.com
|
||||||
|
port: 443
|
||||||
|
container: api-service: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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Configure JWT Validator plugin defaults for all domains:
|
||||||
|
|
||||||
|
| Environment Variable | Config Key | Type | Default | Description |
|
||||||
|
|----------------------------------------------------|-------------------|---------|---------|---------------------------------------------|
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ALGORITHM` | `algorithm` | string | `RS256` | JWT signing algorithm |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ISSUER` | `issuer` | string | - | Expected JWT issuer (optional) |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_AUDIENCE` | `audience` | string | - | Expected JWT audience (optional) |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PUBKEY_PATH` | `pubkey_path` | string | - | Path to public key file |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PUBKEY` | `pubkey` | string | - | Public key as base64-encoded string |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_PATHS` | `paths` | string | - | Comma-separated paths requiring JWT |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ONLY_PATHS` | `only_paths` | boolean | `false` | If true, only specified paths accessible |
|
||||||
|
| `EASYHAPROXY_PLUGIN_JWT_VALIDATOR_ALLOW_ANONYMOUS` | `allow_anonymous` | boolean | `false` | Allow requests without Authorization header |
|
||||||
|
|
||||||
|
**Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations.
|
||||||
|
|
||||||
|
## Generated HAProxy Configuration
|
||||||
|
|
||||||
|
### All Paths Protected
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# JWT Validator - Validate JWT tokens
|
||||||
|
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
|
||||||
|
|
||||||
|
# Extract JWT header and payload
|
||||||
|
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
|
||||||
|
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
|
||||||
|
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
|
||||||
|
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
|
||||||
|
|
||||||
|
# Validate JWT
|
||||||
|
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||||
|
|
||||||
|
# Validate expiration
|
||||||
|
http-request set-var(txn.now) date()
|
||||||
|
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Paths Only (only_paths=false)
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# JWT Validator - Validate JWT tokens
|
||||||
|
|
||||||
|
# Define paths that require JWT validation
|
||||||
|
acl jwt_protected_path path_beg /api/admin
|
||||||
|
acl jwt_protected_path path_beg /api/sensitive
|
||||||
|
|
||||||
|
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found } if jwt_protected_path
|
||||||
|
|
||||||
|
# Extract JWT header and payload
|
||||||
|
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if jwt_protected_path
|
||||||
|
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if jwt_protected_path
|
||||||
|
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if jwt_protected_path
|
||||||
|
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if jwt_protected_path
|
||||||
|
|
||||||
|
# Validate JWT (only on protected paths)
|
||||||
|
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if jwt_protected_path
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if jwt_protected_path
|
||||||
|
|
||||||
|
# Validate expiration
|
||||||
|
http-request set-var(txn.now) date() if jwt_protected_path
|
||||||
|
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if jwt_protected_path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Paths Only (only_paths=true)
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# JWT Validator - Validate JWT tokens
|
||||||
|
|
||||||
|
# Define paths that require JWT validation
|
||||||
|
acl jwt_protected_path path_beg /api/public
|
||||||
|
acl jwt_protected_path path_beg /api/v1
|
||||||
|
|
||||||
|
# Deny access to paths not in the protected list
|
||||||
|
http-request deny content-type 'text/html' string 'Access denied' unless jwt_protected_path
|
||||||
|
|
||||||
|
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
|
||||||
|
|
||||||
|
# Extract JWT header and payload
|
||||||
|
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
|
||||||
|
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
|
||||||
|
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
|
||||||
|
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
|
||||||
|
|
||||||
|
# Validate JWT (all requests at this point are on allowed paths)
|
||||||
|
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||||
|
|
||||||
|
# Validate expiration
|
||||||
|
http-request set-var(txn.now) date()
|
||||||
|
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Allow Anonymous Access (allow_anonymous=true)
|
||||||
|
|
||||||
|
```haproxy
|
||||||
|
# JWT Validator - Validate JWT tokens
|
||||||
|
|
||||||
|
# Allow anonymous access - validate JWT only if Authorization header is present
|
||||||
|
|
||||||
|
# Extract JWT header and payload
|
||||||
|
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if { req.hdr(authorization) -m found }
|
||||||
|
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if { req.hdr(authorization) -m found }
|
||||||
|
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if { req.hdr(authorization) -m found }
|
||||||
|
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if { req.hdr(authorization) -m found }
|
||||||
|
|
||||||
|
# Validate JWT (only if Authorization header is present)
|
||||||
|
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if { req.hdr(authorization) -m found }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ } if { req.hdr(authorization) -m found }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com } if { req.hdr(authorization) -m found }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if { req.hdr(authorization) -m found }
|
||||||
|
|
||||||
|
# Validate expiration (only if Authorization header is present)
|
||||||
|
http-request set-var(txn.now) date() if { req.hdr(authorization) -m found }
|
||||||
|
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if { req.hdr(authorization) -m found }
|
||||||
|
```
|
||||||
|
|
||||||
|
## What It Validates
|
||||||
|
|
||||||
|
- ✅ Authorization header presence
|
||||||
|
- ✅ JWT signing algorithm (RS256, RS512, etc.)
|
||||||
|
- ✅ JWT issuer (if configured)
|
||||||
|
- ✅ JWT audience (if configured)
|
||||||
|
- ✅ JWT signature using public key
|
||||||
|
- ✅ JWT expiration time
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Required:** HAProxy 2.5+ with JWT support
|
||||||
|
- Mount public key file as read-only volume
|
||||||
|
- The plugin runs once per domain during the discovery cycle
|
||||||
|
- Test thoroughly with your JWT provider before deploying to production
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Plugin System Overview](../plugins.md)
|
||||||
|
- [Container Labels Reference](../container-labels.md)
|
||||||
|
|
@ -7,7 +7,7 @@ sidebar_position: 11
|
||||||
## Container (Docker or Swarm) labels
|
## Container (Docker or Swarm) labels
|
||||||
|
|
||||||
| Label | Description | Default | Example |
|
| Label | Description | Default | Example |
|
||||||
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------|
|
|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------|
|
||||||
| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com |
|
| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com |
|
||||||
| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp |
|
| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp |
|
||||||
| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 |
|
| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 |
|
||||||
|
|
@ -20,6 +20,8 @@ sidebar_position: 11
|
||||||
| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false |
|
| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false |
|
||||||
| easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use** this with `ssl` or `certbot` parameters | false | true OR false |
|
| easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use** this with `ssl` or `certbot` parameters | false | true OR false |
|
||||||
| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based |
|
| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based |
|
||||||
|
| easyhaproxy.[definition].proto | (Optional) Backend server protocol (e.g., fcgi for PHP-FPM, h2 for HTTP/2) | *empty* | fcgi, h2 |
|
||||||
|
| easyhaproxy.[definition].socket | (Optional) Unix socket path for backend connection (alternative to host:port) | *empty* | /run/php/php-fpm.sock |
|
||||||
|
|
||||||
:::info Understanding Definitions
|
:::info Understanding Definitions
|
||||||
The `[definition]` is a string identifier that groups related configuration labels together. Different definitions create separate HAProxy configurations.
|
The `[definition]` is a string identifier that groups related configuration labels together. Different definitions create separate HAProxy configurations.
|
||||||
|
|
@ -67,8 +69,6 @@ docker run \
|
||||||
If you are using docker-compose you can use this way:
|
If you are using docker-compose you can use this way:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mycontainer:
|
mycontainer:
|
||||||
image: some/myimage
|
image: some/myimage
|
||||||
|
|
@ -93,6 +93,50 @@ docker run \
|
||||||
some/tcp-service
|
some/tcp-service
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### FastCGI (PHP-FPM) Support
|
||||||
|
|
||||||
|
EasyHAProxy supports FastCGI protocol for PHP-FPM and other FastCGI applications.
|
||||||
|
|
||||||
|
#### Using Unix Socket
|
||||||
|
|
||||||
|
```yaml title="PHP-FPM with Unix socket"
|
||||||
|
services:
|
||||||
|
php-fpm:
|
||||||
|
image: php:8.2-fpm
|
||||||
|
labels:
|
||||||
|
easyhaproxy.fcgi.host: phpapp.local
|
||||||
|
easyhaproxy.fcgi.port: 80
|
||||||
|
easyhaproxy.fcgi.socket: /run/php/php-fpm.sock
|
||||||
|
easyhaproxy.fcgi.proto: fcgi
|
||||||
|
volumes:
|
||||||
|
- /run/php:/run/php
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using TCP Connection
|
||||||
|
|
||||||
|
```yaml title="PHP-FPM with TCP connection"
|
||||||
|
services:
|
||||||
|
php-fpm:
|
||||||
|
image: php:8.2-fpm
|
||||||
|
labels:
|
||||||
|
easyhaproxy.fcgi.host: phpapp.local
|
||||||
|
easyhaproxy.fcgi.port: 80
|
||||||
|
easyhaproxy.fcgi.localport: 9000
|
||||||
|
easyhaproxy.fcgi.proto: fcgi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated HAProxy Configuration:**
|
||||||
|
|
||||||
|
```
|
||||||
|
backend srv_phpapp_local_80
|
||||||
|
balance roundrobin
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
http-request set-header X-Forwarded-Port %[dst_port]
|
||||||
|
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi
|
||||||
|
```
|
||||||
|
|
||||||
### Redirect Domains
|
### Redirect Domains
|
||||||
|
|
||||||
```bash title="Domain redirect configuration"
|
```bash title="Domain redirect configuration"
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,18 @@ sidebar_position: 12
|
||||||
| EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *empty* |
|
| EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *empty* |
|
||||||
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` |
|
| EASYHAPROXY_SSL_MODE | (Optional) `strict` supports only the most recent TLS version; `default` good SSL integration with recent browsers; `loose` supports all old SSL protocols for old browsers (not recommended). | `default` |
|
||||||
| EASYHAPROXY_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 |
|
| EASYHAPROXY_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 |
|
||||||
| EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | INFO |
|
| EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG |
|
||||||
| CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG |
|
| CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG |
|
||||||
| HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG |
|
| HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | INFO |
|
||||||
| HAPROXY_USERNAME | (Optional) The HAProxy username to the statistics. | `admin` |
|
| HAPROXY_USERNAME | (Optional) The HAProxy username for the statistics endpoint (used only when `HAPROXY_PASSWORD` is set). | `admin` |
|
||||||
| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics. If not set, statistics will be available with no password | *empty* |
|
| HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics endpoint. Stats are **disabled** unless this is defined. | *empty* |
|
||||||
| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics | `1936` |
|
| HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics. Only applies when `HAPROXY_PASSWORD` is defined. | `1936` |
|
||||||
| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` |
|
| HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` |
|
||||||
|
|
||||||
|
:::tip HAProxy Stats
|
||||||
|
Statistics are only configured when `HAPROXY_PASSWORD` is set. Without a password, the stats section is not generated.
|
||||||
|
:::
|
||||||
|
|
||||||
:::note ACME/Certbot Environment Variables
|
:::note ACME/Certbot Environment Variables
|
||||||
For ACME/Certbot configuration (Let's Encrypt, ZeroSSL, etc.), see the [ACME documentation](acme.md#environment-variables) for the complete list of `EASYHAPROXY_CERTBOT_*` variables.
|
For ACME/Certbot configuration (Let's Encrypt, ZeroSSL, etc.), see the [ACME documentation](acme.md#environment-variables) for the complete list of `EASYHAPROXY_CERTBOT_*` variables.
|
||||||
:::
|
:::
|
||||||
|
|
|
||||||
|
|
@ -91,16 +91,151 @@ You don't need to expose any port in your container.
|
||||||
## Kubernetes annotations
|
## Kubernetes annotations
|
||||||
|
|
||||||
| annotation | Description | Default | Example |
|
| annotation | Description | Default | Example |
|
||||||
|----------------------------------|-------------------------------------------------------------------------------------|--------------|----------------------------|
|
|-------------------------------------|-------------------------------------------------------------------------------------|--------------|----------------------------|
|
||||||
| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress |
|
| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress |
|
||||||
| easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to HTTPS. | false | true or false |
|
| easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to HTTPS. | false | true or false |
|
||||||
| easyhaproxy.certbot | (optional) Boolean. It will request certbot certificates for the ingresses domains. | false | true or false |
|
| easyhaproxy.certbot | (optional) Boolean. It will request certbot certificates for the ingresses domains. | false | true or false |
|
||||||
| easyhaproxy.redirect | (optional) JSON. Key pair with a domain and its destination. | *empty* | \{"domain":"redirect_url"} |
|
| easyhaproxy.redirect | (optional) JSON. Key pair with a domain and its destination. | *empty* | \{"domain":"redirect_url"} |
|
||||||
| easyhaproxy.mode | (optional) Set the HTTP mode for that connection. | http | http or tcp |
|
| easyhaproxy.mode | (optional) Set the HTTP mode for that connection. | http | http or tcp |
|
||||||
| easyhaproxy.listen_port | (optional) Override the HTTP listen port created for that ingress | 80 | 8081 |
|
| easyhaproxy.listen_port | (optional) Override the HTTP listen port created for that ingress | 80 | 8081 |
|
||||||
|
| easyhaproxy.plugins | (optional) Comma-separated list of plugins to enable for this ingress | *empty* | cloudflare,deny_pages |
|
||||||
|
| easyhaproxy.plugin.`{name}`.`{key}` | (optional) Plugin-specific configuration (see [Using Plugins](plugins.md)) | *varies* | See examples below |
|
||||||
|
|
||||||
**Important**: The annotations are per ingress and applied to all hosts in that ingress configuration.
|
**Important**: The annotations are per ingress and applied to all hosts in that ingress configuration.
|
||||||
|
|
||||||
|
## Using Plugins with Kubernetes
|
||||||
|
|
||||||
|
Plugins extend HAProxy configuration with additional functionality like JWT validation, IP whitelisting, or Cloudflare IP restoration. For a complete list of available plugins, see the [Using Plugins](plugins.md) guide.
|
||||||
|
|
||||||
|
### Enabling Plugins for an Ingress
|
||||||
|
|
||||||
|
Add the `easyhaproxy.plugins` annotation with a comma-separated list of plugin names:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "cloudflare,deny_pages"
|
||||||
|
name: example-ingress
|
||||||
|
namespace: example
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: example.org
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: example-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuring Plugin Options
|
||||||
|
|
||||||
|
Use `easyhaproxy.plugin.{plugin_name}.{option}` annotations to configure individual plugins:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "deny_pages"
|
||||||
|
easyhaproxy.plugin.deny_pages.paths: "/admin,/private,/config"
|
||||||
|
easyhaproxy.plugin.deny_pages.status_code: "403"
|
||||||
|
name: secure-app-ingress
|
||||||
|
namespace: production
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: myapp.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: myapp-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Plugin Examples
|
||||||
|
|
||||||
|
**Protect API with JWT validation:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "jwt_validator"
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** For JWT validation, you'll need to mount the public key file into the EasyHAProxy pod. See [Using Plugins](plugins.md#protect-api-with-jwt-authentication) for details.
|
||||||
|
|
||||||
|
**Restrict access to specific IPs:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "ip_whitelist"
|
||||||
|
easyhaproxy.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.5"
|
||||||
|
easyhaproxy.plugin.ip_whitelist.status_code: "403"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restore Cloudflare visitor IPs:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "cloudflare"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiple plugins together:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
easyhaproxy.plugins: "cloudflare,deny_pages"
|
||||||
|
easyhaproxy.plugin.deny_pages.paths: "/wp-admin,/wp-login.php"
|
||||||
|
easyhaproxy.plugin.deny_pages.status_code: "404"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Plugin Configuration
|
||||||
|
|
||||||
|
Some plugins (like `cleanup`) are global and execute once per discovery cycle. Configure these via environment variables or YAML configuration:
|
||||||
|
|
||||||
|
**Using Helm values.yaml:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
easyhaproxy:
|
||||||
|
plugins:
|
||||||
|
enabled: cleanup
|
||||||
|
config:
|
||||||
|
cleanup:
|
||||||
|
max_idle_time: 600
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using environment variables:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: EASYHAPROXY_PLUGINS_ENABLED
|
||||||
|
value: "cleanup"
|
||||||
|
- name: EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME
|
||||||
|
value: "600"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on plugin types and available plugins, see the [Using Plugins](plugins.md) guide.
|
||||||
|
|
||||||
## Certbot / ACME / Letsencrypt
|
## Certbot / ACME / Letsencrypt
|
||||||
|
|
||||||
It is necessary add the annotation `easyhaproxy.certbot` to the ingress configuration:
|
It is necessary add the annotation `easyhaproxy.certbot` to the ingress configuration:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 15
|
sidebar_position: 23
|
||||||
---
|
---
|
||||||
|
|
||||||
# Limitations and Considerations
|
# Limitations and Considerations
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 14
|
sidebar_position: 22
|
||||||
---
|
---
|
||||||
|
|
||||||
# Other configurations
|
# Other configurations
|
||||||
|
|
@ -9,7 +9,7 @@ sidebar_position: 14
|
||||||
Some ports on the EasyHAProxy container and in the firewall are required to be open. However, you don't need to expose the other container ports because EasyHAProxy will handle that.
|
Some ports on the EasyHAProxy container and in the firewall are required to be open. However, you don't need to expose the other container ports because EasyHAProxy will handle that.
|
||||||
|
|
||||||
- The ports `80` and `443`.
|
- The ports `80` and `443`.
|
||||||
- If you enable the HAProxy statistics, you must also expose the port defined in `HAPROXY_STATS_PORT` environment variable (default 1936). Be aware that statistics are enabled by default with no password.
|
- If you enable the HAProxy statistics, you must also expose the port defined in `HAPROXY_STATS_PORT` environment variable (default 1936). Statistics are only generated when you set `HAPROXY_PASSWORD`.
|
||||||
- Every port defined in `easyhaproxy.[definitions].port` also should be exposed.
|
- Every port defined in `easyhaproxy.[definitions].port` also should be exposed.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
|
||||||
1855
docs/plugin-development.md
Normal file
1855
docs/plugin-development.md
Normal file
File diff suppressed because it is too large
Load diff
422
docs/plugins.md
Normal file
422
docs/plugins.md
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
---
|
||||||
|
sidebar_position: 14
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using Plugins
|
||||||
|
|
||||||
|
EasyHAProxy supports a plugin system that extends HAProxy configuration with custom functionality. This guide explains how to use and configure plugins.
|
||||||
|
|
||||||
|
## What are Plugins?
|
||||||
|
|
||||||
|
Plugins automatically run during the discovery cycle and can:
|
||||||
|
- Add HAProxy configuration directives
|
||||||
|
- Perform maintenance tasks
|
||||||
|
- Modify discovery data
|
||||||
|
- Integrate with external services
|
||||||
|
|
||||||
|
## Plugin Types
|
||||||
|
|
||||||
|
### Global Plugins
|
||||||
|
|
||||||
|
Execute **once per discovery cycle** regardless of how many domains are discovered.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Cleanup tasks
|
||||||
|
- Global monitoring
|
||||||
|
- DNS updates
|
||||||
|
- Log management
|
||||||
|
|
||||||
|
**Example:** `cleanup` plugin
|
||||||
|
|
||||||
|
### Domain Plugins
|
||||||
|
|
||||||
|
Execute **once for each discovered domain/host**.
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Domain-specific configuration
|
||||||
|
- IP restoration (Cloudflare)
|
||||||
|
- Path blocking
|
||||||
|
- Custom headers per domain
|
||||||
|
|
||||||
|
**Examples:** `cloudflare`, `deny_pages`
|
||||||
|
|
||||||
|
## Built-in Plugins
|
||||||
|
|
||||||
|
EasyHAProxy includes several built-in plugins ready to use:
|
||||||
|
|
||||||
|
- [Cloudflare](Plugins/cloudflare.md) - Restore visitor IP from Cloudflare CDN
|
||||||
|
- [Cleanup](Plugins/cleanup.md) - Cleanup temporary files
|
||||||
|
- [Deny Pages](Plugins/deny-pages.md) - Block specific paths
|
||||||
|
- [IP Whitelist](Plugins/ip-whitelist.md) - Restrict access to IPs/CIDR ranges
|
||||||
|
- [JWT Validator](Plugins/jwt-validator.md) - Validate JWT tokens
|
||||||
|
- [FastCGI](Plugins/fastcgi.md) - Configure PHP-FPM and FastCGI applications
|
||||||
|
|
||||||
|
## Configuration Methods
|
||||||
|
|
||||||
|
Plugins can be configured using different methods depending on your deployment environment:
|
||||||
|
|
||||||
|
### 1. Kubernetes Annotations (Ingress Resources)
|
||||||
|
|
||||||
|
Enable and configure domain plugins for specific Kubernetes ingresses:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
# Enable plugins
|
||||||
|
easyhaproxy.plugins: "jwt_validator,deny_pages"
|
||||||
|
# Configure jwt_validator plugin (protect specific paths only)
|
||||||
|
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"
|
||||||
|
easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
|
||||||
|
easyhaproxy.plugin.jwt_validator.only_paths: "false"
|
||||||
|
# Configure deny_pages plugin
|
||||||
|
easyhaproxy.plugin.deny_pages.paths: "/admin,/private"
|
||||||
|
easyhaproxy.plugin.deny_pages.status_code: "403"
|
||||||
|
name: api-ingress
|
||||||
|
namespace: production
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: api.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: api-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
```
|
||||||
|
|
||||||
|
**Annotation format:**
|
||||||
|
- Enable plugins: `easyhaproxy.plugins: plugin1,plugin2`
|
||||||
|
- Configure plugin: `easyhaproxy.plugin.<plugin_name>.<config_key>: value`
|
||||||
|
|
||||||
|
See the [Kubernetes guide](kubernetes.md#using-plugins-with-kubernetes) for more examples.
|
||||||
|
|
||||||
|
### 2. Container Labels (Docker/Docker Compose)
|
||||||
|
|
||||||
|
Enable and configure domain plugins for specific Docker containers:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
image: myapp:latest
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: example.com
|
||||||
|
easyhaproxy.http.port: 80
|
||||||
|
# Enable multiple plugins
|
||||||
|
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||||
|
# Configure deny_pages plugin
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/api/internal
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
**Label format:**
|
||||||
|
- Enable plugins: `easyhaproxy.<definition>.plugins: plugin1,plugin2`
|
||||||
|
- Configure plugin: `easyhaproxy.<definition>.plugin.<plugin_name>.<config_key>: value`
|
||||||
|
|
||||||
|
**Where `<definition>` is:** `http`, `https`, `tcp`, etc.
|
||||||
|
|
||||||
|
### 3. Static YAML Configuration
|
||||||
|
|
||||||
|
Configure plugins in `/etc/haproxy/static/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
# Global settings
|
||||||
|
abort_on_error: false # Log and continue on errors (recommended)
|
||||||
|
|
||||||
|
# Enable GLOBAL plugins (run once per discovery cycle)
|
||||||
|
enabled: [cleanup]
|
||||||
|
|
||||||
|
# Configure plugins (both global and domain plugins)
|
||||||
|
config:
|
||||||
|
# Global plugin configuration (cleanup runs once per cycle)
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 600
|
||||||
|
|
||||||
|
# Domain plugin configuration (applies to ALL domains by default)
|
||||||
|
cloudflare:
|
||||||
|
enabled: true # Apply to all domains
|
||||||
|
use_builtin_ips: true # Use built-in Cloudflare IPs
|
||||||
|
|
||||||
|
# Domain plugin disabled by default (enable per-domain via labels/annotations)
|
||||||
|
deny_pages:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important distinctions:**
|
||||||
|
|
||||||
|
- **Global plugins** (like `cleanup`): Run once per discovery cycle, configured here only
|
||||||
|
- **Domain plugins** (like `cloudflare`, `deny_pages`, `jwt_validator`):
|
||||||
|
- Configuration here sets **defaults for ALL domains**
|
||||||
|
- Can be enabled/disabled per-domain via container labels or Kubernetes annotations
|
||||||
|
- Per-domain configuration overrides these defaults
|
||||||
|
|
||||||
|
### 4. Environment Variables
|
||||||
|
|
||||||
|
Configure plugins via environment variables. **Note:** Environment variables set system-wide defaults and cannot configure plugins per-domain.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable GLOBAL plugins (run once per discovery cycle)
|
||||||
|
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
||||||
|
EASYHAPROXY_PLUGINS_ABORT_ON_ERROR=false
|
||||||
|
|
||||||
|
# Configure GLOBAL plugins
|
||||||
|
EASYHAPROXY_PLUGIN_CLEANUP_ENABLED=true
|
||||||
|
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||||
|
|
||||||
|
# Configure DOMAIN plugins (sets defaults for ALL domains)
|
||||||
|
EASYHAPROXY_PLUGIN_CLOUDFLARE_ENABLED=true
|
||||||
|
EASYHAPROXY_PLUGIN_CLOUDFLARE_USE_BUILTIN_IPS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variable format:**
|
||||||
|
- Enable global plugins: `EASYHAPROXY_PLUGINS_ENABLED=plugin1,plugin2`
|
||||||
|
- Configure plugin: `EASYHAPROXY_PLUGIN_<PLUGIN_NAME>_<CONFIG_KEY>=value`
|
||||||
|
|
||||||
|
**Scope limitations:**
|
||||||
|
- **Global plugins**: Environment variables configure the single instance
|
||||||
|
- **Domain plugins**: Environment variables set defaults for ALL domains
|
||||||
|
- **Per-domain configuration**: Use container labels (Docker) or annotations (Kubernetes) instead
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Protect API with JWT Authentication
|
||||||
|
|
||||||
|
**Secure entire API domain:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: api.example.com
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.issuer: https://auth0.myapp.com/
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
volumes:
|
||||||
|
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protect only admin/sensitive endpoints:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: api.example.com
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/users,/api/billing
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.only_paths: false
|
||||||
|
volumes:
|
||||||
|
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
# /api/health, /api/docs, etc. remain publicly accessible
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restrict API to only allow specific endpoints:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: api.example.com
|
||||||
|
easyhaproxy.http.plugins: jwt_validator
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.paths: /api/v1,/api/v2
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.only_paths: true
|
||||||
|
volumes:
|
||||||
|
- ./auth_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
|
||||||
|
# All paths except /api/v1 and /api/v2 are denied
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restrict Admin Panel to Office IPs
|
||||||
|
|
||||||
|
Protect admin panel by only allowing access from office network:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: admin.example.com
|
||||||
|
easyhaproxy.http.plugins: ip_whitelist
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 203.0.113.0/24,198.51.100.42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protect Admin Paths
|
||||||
|
|
||||||
|
Block access to WordPress admin and other sensitive paths:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: wordpress.example.com
|
||||||
|
easyhaproxy.http.plugins: deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /wp-admin,/wp-login.php,/.env
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare IP Restoration
|
||||||
|
|
||||||
|
Restore original visitor IPs for applications behind Cloudflare:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: myapp.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Plugins Together
|
||||||
|
|
||||||
|
Combine multiple plugins for one domain:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
easyhaproxy.http.host: secure-app.com
|
||||||
|
easyhaproxy.http.plugins: cloudflare,deny_pages
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: /admin,/config
|
||||||
|
easyhaproxy.http.plugin.deny_pages.status_code: 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Cleanup
|
||||||
|
|
||||||
|
Keep your system clean with automatic temp file removal:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# /etc/haproxy/static/config.yaml
|
||||||
|
plugins:
|
||||||
|
enabled: [cleanup]
|
||||||
|
config:
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 3600 # 1 hour
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Log and Continue (Recommended)
|
||||||
|
|
||||||
|
By default, plugin errors are logged as warnings and discovery continues:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
abort_on_error: false # Default
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** Most situations. Ensures a failing plugin doesn't prevent HAProxy updates.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Plugin errors logged as warnings
|
||||||
|
- Discovery cycle continues
|
||||||
|
- Other plugins still execute
|
||||||
|
- HAProxy config is generated without the failed plugin
|
||||||
|
|
||||||
|
### Abort on Error
|
||||||
|
|
||||||
|
Stop discovery cycle if any plugin fails:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
abort_on_error: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use:** Critical plugins where failure should halt deployment.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Plugin error stops discovery
|
||||||
|
- Previous HAProxy config remains active
|
||||||
|
- No configuration changes until issue is resolved
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Enable Debug Logging
|
||||||
|
|
||||||
|
See detailed plugin execution information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EASYHAPROXY_LOG_LEVEL=DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
**Look for:**
|
||||||
|
```
|
||||||
|
INFO: Loaded builtin plugin: cloudflare (domain)
|
||||||
|
INFO: Loaded builtin plugin: cleanup (global)
|
||||||
|
DEBUG: Executing domain plugin: cloudflare for domain: example.com
|
||||||
|
DEBUG: Plugin cloudflare metadata: {'domain': 'example.com', 'ip_list_path': '/etc/haproxy/cloudflare_ips.lst'}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Not Loading
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. Plugin file exists in `/etc/haproxy/plugins/` or builtin directory
|
||||||
|
2. Python syntax is valid
|
||||||
|
3. Plugin class inherits from `PluginInterface`
|
||||||
|
4. Check logs for load errors
|
||||||
|
|
||||||
|
### Plugin Not Executing
|
||||||
|
|
||||||
|
**For domain plugins:**
|
||||||
|
1. Check container has label: `easyhaproxy.http.plugins: plugin_name`
|
||||||
|
2. Verify plugin name is correct (case-sensitive)
|
||||||
|
3. Enable debug logging
|
||||||
|
|
||||||
|
**For global plugins:**
|
||||||
|
1. Check YAML config: `plugins.enabled: [plugin_name]`
|
||||||
|
2. Or env var: `EASYHAPROXY_PLUGINS_ENABLED=plugin_name`
|
||||||
|
3. Enable debug logging
|
||||||
|
|
||||||
|
### Configuration Not Applied
|
||||||
|
|
||||||
|
**Check precedence order:**
|
||||||
|
|
||||||
|
For Kubernetes deployments:
|
||||||
|
1. Ingress annotations (highest)
|
||||||
|
2. YAML configuration
|
||||||
|
3. Environment variables (lowest)
|
||||||
|
|
||||||
|
For Docker deployments:
|
||||||
|
1. Container labels (highest)
|
||||||
|
2. YAML configuration
|
||||||
|
3. Environment variables (lowest)
|
||||||
|
|
||||||
|
Per-ingress/per-container settings override global configuration.
|
||||||
|
|
||||||
|
### Plugin Output Missing
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
1. Plugin is enabled (`enabled: true`)
|
||||||
|
2. Plugin configuration is correct
|
||||||
|
3. Plugin's `process()` method returns valid `PluginResult`
|
||||||
|
4. Check debug logs for plugin execution
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Start with log-and-continue mode** - Use `abort_on_error: false` until you're confident plugins are stable
|
||||||
|
2. **Use container labels for domain-specific config** - Easier to manage per-service
|
||||||
|
3. **Use YAML/env for global config** - Better for global plugins and defaults
|
||||||
|
4. **Enable debug logging during testing** - Helps identify configuration issues
|
||||||
|
5. **Test plugin changes in staging first** - Avoid production surprises
|
||||||
|
6. **Keep plugin configurations simple** - Use defaults when possible
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Plugins must be written in Python
|
||||||
|
- Domain plugins execute for each domain, so keep them lightweight
|
||||||
|
- Plugins cannot modify the Jinja2 template structure directly
|
||||||
|
- Plugin errors in abort mode prevent all configuration updates
|
||||||
|
|
||||||
|
## Creating Custom Plugins
|
||||||
|
|
||||||
|
Want to create your own plugins? See the [Plugin Developer Guide](plugin-development.md) for detailed instructions on building custom plugins that extend EasyHAProxy functionality.
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Plugin Developer Guide](plugin-development.md) - Create custom plugins
|
||||||
|
- [Container Labels](container-labels.md) - Label configuration reference
|
||||||
|
- [Environment Variables](environment-variable.md) - Environment variable reference
|
||||||
|
- [Static Configuration](static.md) - YAML configuration reference
|
||||||
|
- [Kubernetes Guide](kubernetes.md) - Using plugins with Kubernetes
|
||||||
|
|
@ -31,8 +31,6 @@ docker network create -d overlay --attachable easyhaproxy
|
||||||
And then deploy the EasyHAProxy stack:
|
And then deploy the EasyHAProxy stack:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
image: byjg/easy-haproxy:4.6.0
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
|
@ -76,8 +74,6 @@ Mapping to `/var/run/docker.sock` is necessary to discover the docker containers
|
||||||
To make your containers "discoverable" by EasyHAProxy, that is the minimum configuration you need:
|
To make your containers "discoverable" by EasyHAProxy, that is the minimum configuration you need:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
container:
|
container:
|
||||||
image: my/image:tag
|
image: my/image:tag
|
||||||
|
|
|
||||||
52
examples/docker/README.md
Normal file
52
examples/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/)
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDyTCCArGgAwIBAgIUVpIokbXupa29drpQBmRlKTViM+UwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwdDELMAkGA1UEBhMCQVUxFTATBgNVBAgMDFBsYWNlIEhvbGRlcjEVMBMGA1UE
|
|
||||||
BwwMUGxhY2UgSG9sZGVyMSEwHwYDVQQKDBhQbGFjZSBIb2xkZXIgQ2VydGlmaWNh
|
|
||||||
dGUxFDASBgNVBAMMC2V4YW1wbGUub3JnMB4XDTIyMDgxNTA0NTEwMFoXDTMyMDgx
|
|
||||||
MjA0NTEwMFowdDELMAkGA1UEBhMCQVUxFTATBgNVBAgMDFBsYWNlIEhvbGRlcjEV
|
|
||||||
MBMGA1UEBwwMUGxhY2UgSG9sZGVyMSEwHwYDVQQKDBhQbGFjZSBIb2xkZXIgQ2Vy
|
|
||||||
dGlmaWNhdGUxFDASBgNVBAMMC2V4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEAqufw4FdqYcJep7mHHcYGUN79GNBLpvAIdg+1NbKx+cB/
|
|
||||||
PtaDuozqVkkT8CmM0Mruay4vCbYkMytCeKgHj2+hLMy7oUQvx2pK/V0i0foPAC0m
|
|
||||||
gAvgmaWZbQTENHX4A0Rwvim0yixgeBVhz4hTMOIunilSXbKRkFBUidCnYQe1Nzy1
|
|
||||||
dbH/fh8++fzLCglDE2kydrE3Zq/54G2xFOxPt1DZRnQ3RBYaMIR/uPPjpVxRWl+p
|
|
||||||
w4ucklAIZcu2htlOpGl7/3baMtnhpTo9LrkWzSNS7CJQvj6BvDULbcN3+hNPOoVE
|
|
||||||
MM9MCaM9V+FS25kf+DfyaVUVDVIv0thpwa+f3tCXSwIDAQABo1MwUTAdBgNVHQ4E
|
|
||||||
FgQUV831A1qfpV+Sy/J+lN6LKLgopsQwHwYDVR0jBBgwFoAUV831A1qfpV+Sy/J+
|
|
||||||
lN6LKLgopsQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhnf7
|
|
||||||
aW/jpr8JWC0l2lo0z1HpOaSNehhcfNyM6Y43lR3Lr1avibbIXkAOzCuSWanFNnex
|
|
||||||
dOq8PbG9bmO1ncM6qzXRBzk4pLVJmzzDZs/fPVghSZumY2bzzJFtdCQ5FiLaaE/c
|
|
||||||
cKtBPvoUjfvrBU9OwFSb9UaoQdxtateb/Kk6JfzWi6YZqxSXFNa0ZTWJaoRFQVj5
|
|
||||||
bZZe+wgpGRz46p+2YMwsNolXxa+7yY9x6kOMqZP6++5LZGXm5iWxjWbN4WtqmNnN
|
|
||||||
cK35fAdLlg4d3wn5tVuTkpKH0FcaRlgpBSjVKejgnFTCxKcOwOCqOfu+nuSSWpa7
|
|
||||||
aRqdAbMTeVFQhyLXhQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCq5/DgV2phwl6n
|
|
||||||
uYcdxgZQ3v0Y0Eum8Ah2D7U1srH5wH8+1oO6jOpWSRPwKYzQyu5rLi8JtiQzK0J4
|
|
||||||
qAePb6EszLuhRC/Hakr9XSLR+g8ALSaAC+CZpZltBMQ0dfgDRHC+KbTKLGB4FWHP
|
|
||||||
iFMw4i6eKVJdspGQUFSJ0KdhB7U3PLV1sf9+Hz75/MsKCUMTaTJ2sTdmr/ngbbEU
|
|
||||||
7E+3UNlGdDdEFhowhH+48+OlXFFaX6nDi5ySUAhly7aG2U6kaXv/dtoy2eGlOj0u
|
|
||||||
uRbNI1LsIlC+PoG8NQttw3f6E086hUQwz0wJoz1X4VLbmR/4N/JpVRUNUi/S2GnB
|
|
||||||
r5/e0JdLAgMBAAECggEAI0nY9rmWAbF8ke1A9OjajQA+Ck2YEVQmqxn7NKc9EHCq
|
|
||||||
1XK9qFtIV6CnOUObC9GbAQ58L+kn+FjKVNd9GCTYhsOPSnEl3GsaKM5+ThTv2/12
|
|
||||||
oaHSMmd7EoOVb6+cEjCjhuBdsBERqjngBFYFt2Y8cfPeSfKBE+dCTWKD7QkGZe0Q
|
|
||||||
tEII2NeE1AoQwG34TANxyn+HbZG809i+k21Pm0tPNyAFwhPvotpjSftMNgco8jcx
|
|
||||||
39DCt7rG1etpc4VhIUUl8GqsNAHYg8/SfscE18PYeX7RJpKGS128E1mKCqWqn+Ie
|
|
||||||
VCiMmWoQ4ZBlUFp3tS4+FWcQtDr8Z82QTZQn3ODwIQKBgQDV1NR+SxjBBnzpHDZC
|
|
||||||
cTxCK0PU37VmPjyr6BAXlftCPLGLgKEVIDRuna6EoiZ0/K0RMDZRoaJgDQPoAXDV
|
|
||||||
UwJOWYtlJBzUxwQbBJc3S7C3kEC+qeRT9lRwIrn7P6gT1DMcSRsV4ULwGRWAMa2Q
|
|
||||||
6apbK/K+56Ds7LiStu3OcJSxDwKBgQDMnAvLm0uMo9ZceKG515ruqzQj2YPz2+Zc
|
|
||||||
f38pD1hESDRj1bzgT09FAsejPnlN7KPp8TFgRUB9Rqb6DZCkx49zdtcDkuZIKfQH
|
|
||||||
Ga7ITBXnTwe8M+nwq2Q2LJYPdB/p8mBqh4ujA2XIS7ZHCKqOFGsseP4H1uxZeORI
|
|
||||||
pIPQp4C+BQKBgQDKHw9tAZc4feV8g4pWa6rF8ReBFKTnLFU1OXpckQybo7s/Xirl
|
|
||||||
STfGh437GTq4wk7lPGlb6CkQGb1jhFkfjANWBBZbWDNYfXZIA6LcRdOY7+YDU5vc
|
|
||||||
Ma/G/0xFTfqWI7LcPc44dGFNiqhkMJEbtYOuAnDGOzRGP8yIAhnvVUN3yQKBgQC1
|
|
||||||
eaYglYGBoQMMi1Xt7iQVkbWyIkedr6l22wJe2aRRE7Wb4sQeM1m8fMWyrUOL8NpF
|
|
||||||
MU6481NKibKp0AQ9kl5Sa9Iy8kTbNpKhBY93SbyXpwnWTDku4+UDA7KozDdOGVKY
|
|
||||||
ydX45JeO+lAWWsJjOAsCq+Gr9F020jmvkHL1SsuuPQKBgQDHHCSOlUycLnzxEAks
|
|
||||||
uiu5MseFzkmdWN1ShrjPkcJ4HLmSD5zRgCySpBB92WPU+yBANlY7WN6fJHVJk/d7
|
|
||||||
sNAg78GuAxcWrNAQwu6DRjhnb8zTVICJX8HmDLmsoOHwLsShdckWMks5XEk3NzoV
|
|
||||||
4ym6aG0tDX+rkBkP/VIjSXA+Cg==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
|
|
@ -1,10 +1,62 @@
|
||||||
# This example shows how to setup HTTP-01 ACME CA Challenge
|
# ==============================================================================
|
||||||
|
# EXAMPLE: Let's Encrypt SSL with ACME/Certbot
|
||||||
|
# ==============================================================================
|
||||||
#
|
#
|
||||||
# You need
|
# WHAT THIS DEMONSTRATES:
|
||||||
# - public IP pointing your machine
|
# - Automatic SSL certificate generation using Let's Encrypt
|
||||||
# - open ports 80 and 443 in your firewall
|
# - HTTP-01 ACME challenge protocol
|
||||||
|
# - Certificate persistence across container restarts
|
||||||
version: "3"
|
# - 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:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,40 @@
|
||||||
# To test:
|
# ==============================================================================
|
||||||
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
# EXAMPLE: Custom Label Prefix
|
||||||
|
# ==============================================================================
|
||||||
#
|
#
|
||||||
# or add to /etc/hosts
|
# WHAT THIS DEMONSTRATES:
|
||||||
# 127.0.0.1 host1.local
|
# - Using a custom label prefix instead of default "easyhaproxy"
|
||||||
|
# - Useful for running multiple EasyHAProxy instances
|
||||||
version: "3"
|
# - Custom label configuration (haproxy.* instead of easyhaproxy.*)
|
||||||
|
#
|
||||||
|
# REQUIREMENTS (run these first):
|
||||||
|
# ```bash
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "host1.local" /etc/hosts || echo "127.0.0.1 host1.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# HOW TO START:
|
||||||
|
# ```bash
|
||||||
|
# docker compose -f docker-compose-changed-label.yml 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 hostname in response
|
||||||
|
#
|
||||||
|
# # Verify custom label prefix is working
|
||||||
|
# docker inspect $(docker ps -q -f "ancestor=byjg/static-httpserver") | grep "haproxy.http"
|
||||||
|
# # Expected: Labels starting with "haproxy." instead of "easyhaproxy."
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# CLEAN UP:
|
||||||
|
# ```bash
|
||||||
|
# docker compose -f docker-compose-changed-label.yml down
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
|
|
@ -27,7 +57,7 @@ services:
|
||||||
container:
|
container:
|
||||||
image: byjg/static-httpserver
|
image: byjg/static-httpserver
|
||||||
labels:
|
labels:
|
||||||
haproxy.http.redirect: host1.local--https://host1.local
|
haproxy.http.redirect: '{"host1.local": "https://host1.local"}'
|
||||||
haproxy.http.host: host1.local
|
haproxy.http.host: host1.local
|
||||||
haproxy.http.port: 80
|
haproxy.http.port: 80
|
||||||
|
|
||||||
|
|
|
||||||
80
examples/docker/docker-compose-cloudflare.yml
Normal file
80
examples/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
|
||||||
|
#
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "myapp.local" /etc/hosts || echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
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
|
||||||
|
webapp:
|
||||||
|
image: byjg/static-httpserver
|
||||||
|
environment:
|
||||||
|
TITLE: "App Behind Cloudflare"
|
||||||
|
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
|
||||||
81
examples/docker/docker-compose-ip-whitelist.yml
Normal file
81
examples/docker/docker-compose-ip-whitelist.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# REQUIREMENTS (run these first):
|
||||||
|
# ```bash
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "admin.local" /etc/hosts || echo "127.0.0.1 admin.local" | sudo tee -a /etc/hosts
|
||||||
|
#
|
||||||
|
# # IMPORTANT: Update the allowed_ips in this file (line 52) 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 http://admin.local/
|
||||||
|
# # 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:
|
||||||
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
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
examples/docker/docker-compose-jwt-validator.yml
Normal file
88
examples/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 ../.. && ./examples/generate-keys.sh && cd examples/docker
|
||||||
|
#
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "api.local" /etc/hosts || echo "127.0.0.1 api.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# 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 http://api.local/
|
||||||
|
# # 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" 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:
|
||||||
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
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
|
||||||
|
|
@ -1,13 +1,48 @@
|
||||||
# curl -H Host:www.helloworld.com localhost:19901
|
# ==============================================================================
|
||||||
# f6d8d45b7411
|
# EXAMPLE: Load Balancing with Multiple Container Replicas
|
||||||
# 59b213cb8592
|
# ==============================================================================
|
||||||
|
#
|
||||||
# curl -I -H Host:google.helloworld.com localhost:19901
|
# WHAT THIS DEMONSTRATES:
|
||||||
# HTTP/1.1 301 Moved Permanently
|
# - Multiple container replicas behind a single domain
|
||||||
# content-length: 0
|
# - Round-robin load balancing across replicas
|
||||||
# location: www.google.com/
|
# - Domain redirect functionality
|
||||||
|
# - Custom port configuration
|
||||||
version: "3"
|
#
|
||||||
|
# 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:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
|
|
|
||||||
84
examples/docker/docker-compose-php-fpm.yml
Normal file
84
examples/docker/docker-compose-php-fpm.yml
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# REQUIREMENTS (run these first):
|
||||||
|
# ```bash
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "phpapp.local" /etc/hosts || echo "127.0.0.1 phpapp.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# 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 http://phpapp.local/
|
||||||
|
# # Expected: 200 OK with PHP environment info
|
||||||
|
#
|
||||||
|
# # Test PHP info page
|
||||||
|
# curl http://phpapp.local/info.php
|
||||||
|
# # Expected: phpinfo() output
|
||||||
|
#
|
||||||
|
# # Test PATH_INFO routing
|
||||||
|
# curl http://phpapp.local/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:
|
||||||
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
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
examples/docker/docker-compose-plugins-combined.yml
Normal file
138
examples/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 ../.. && ./examples/generate-keys.sh && cd examples/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
|
||||||
|
#
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "website.local" /etc/hosts || echo "127.0.0.1 website.local api.local admin.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# 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 http://website.local/
|
||||||
|
# # Expected: 200 OK
|
||||||
|
# curl http://website.local/admin
|
||||||
|
# # Expected: HTTP 404 - Path blocked
|
||||||
|
#
|
||||||
|
# # Test protected API (JWT required)
|
||||||
|
# curl http://api.local/
|
||||||
|
# # 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 "Authorization: Bearer $TOKEN" http://api.local/
|
||||||
|
# # Expected: 200 OK
|
||||||
|
#
|
||||||
|
# # Test admin panel (IP whitelist)
|
||||||
|
# curl http://admin.local/
|
||||||
|
# # 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:
|
||||||
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
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
|
||||||
|
|
@ -1,4 +1,45 @@
|
||||||
version: "3"
|
# ==============================================================================
|
||||||
|
# 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:
|
services:
|
||||||
container:
|
container:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,62 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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_certbot
|
||||||
# docker volume create certs_haproxy
|
# docker volume create certs_haproxy
|
||||||
# docker volume create portainer_data
|
# docker volume create portainer_data
|
||||||
|
#
|
||||||
|
# # Create shared network (idempotent)
|
||||||
# docker network create easyhaproxy
|
# 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
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
easyhaproxy:
|
easyhaproxy:
|
||||||
image: byjg/easy-haproxy:4.6.0
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,53 @@
|
||||||
# To test:
|
# ==============================================================================
|
||||||
|
# EXAMPLE: Basic SSL Setup with Two Virtual Hosts
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# WHAT THIS DEMONSTRATES:
|
||||||
|
# - Basic SSL setup with custom certificates (embedded base64 and file-based)
|
||||||
|
# - Automatic HTTP to HTTPS redirect
|
||||||
|
# - Two virtual hosts (host1.local and host2.local)
|
||||||
|
# - HAProxy stats interface
|
||||||
|
#
|
||||||
|
# REQUIREMENTS (run these first):
|
||||||
|
# ```bash
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "host1.local" /etc/hosts || echo "127.0.0.1 host1.local host2.local" | sudo tee -a /etc/hosts
|
||||||
|
#
|
||||||
|
# # Generate SSL certificates
|
||||||
|
# cd ../.. && ./examples/generate-keys.sh && cd examples/docker
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# HOW TO START:
|
||||||
|
# ```bash
|
||||||
|
# docker compose up -d
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# HOW TO VERIFY IT'S WORKING:
|
||||||
|
# ```bash
|
||||||
|
# # Test HTTPS
|
||||||
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
||||||
# curl -k -H "Host: host2.local" https://127.0.0.1/
|
# curl -k -H "Host: host2.local" https://127.0.0.1/
|
||||||
|
# # Expected: 200 OK with hostname in response
|
||||||
#
|
#
|
||||||
# curl -I -H Host:host1.local http://127.0.0.1
|
# # Test HTTP redirect
|
||||||
# HTTP/1.1 301 Moved Permanently
|
# curl -I -H "Host: host1.local" http://127.0.0.1
|
||||||
# content-length: 0
|
# # Expected: HTTP/1.1 301 Moved Permanently, Location: https://host1.local/
|
||||||
# location: https://host1.local/
|
|
||||||
#
|
#
|
||||||
# curl -I -H Host:host2.local http://127.0.0.1
|
# # View SSL certificate
|
||||||
# HTTP/1.1 301 Moved Permanently
|
# openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local < /dev/null
|
||||||
# content-length: 0
|
|
||||||
# location: https://host1.local/
|
|
||||||
#
|
#
|
||||||
# Test SSL:
|
# # Access stats interface
|
||||||
# openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local
|
# # URL: http://localhost:1936
|
||||||
|
# # Username: admin
|
||||||
version: "3"
|
# # Password: password
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# CLEAN UP:
|
||||||
|
# ```bash
|
||||||
|
# docker compose down
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDqTCCApGgAwIBAgIUId5b9t9uqH78g02EzbWF6FKVw3gwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwZDELMAkGA1UEBhMCQlIxFzAVBgNVBAgMDlJpbyBkZSBKYW5laXJvMRcwFQYD
|
|
||||||
VQQHDA5SaW8gZGUgSmFuZWlybzENMAsGA1UECgwEQUNNRTEUMBIGA1UEAwwLaG9z
|
|
||||||
dDIubG9jYWwwHhcNMjIwODE1MDQyNzA1WhcNMjMwODE1MDQyNzA1WjBkMQswCQYD
|
|
||||||
VQQGEwJCUjEXMBUGA1UECAwOUmlvIGRlIEphbmVpcm8xFzAVBgNVBAcMDlJpbyBk
|
|
||||||
ZSBKYW5laXJvMQ0wCwYDVQQKDARBQ01FMRQwEgYDVQQDDAtob3N0Mi5sb2NhbDCC
|
|
||||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMSKvrOahavCXnvSF5131hpo
|
|
||||||
6k65C57jgRQ84FaDj5MbJOVlYQVFtMG0XOk7a+hh5v1fe4wH0R7I6FDo0V9sS+ss
|
|
||||||
ko5bsElc1xYlg5HbuKq89vRSKg6EDlztx3BKbi912Pmt5vFGNJ16zcw77DUrQIXo
|
|
||||||
4I/b4a3pmBiWj43NoTIrmSWHtsGwwOj3iDvSweqdYXJIr3hpHH5u6pohjDoQvqDz
|
|
||||||
K6Mu8p6mhCUKNs7KFJnNInNG25oQT6O0n4OGtmgRjLWopdEnOhMkKsfIoI1XtlXB
|
|
||||||
LBDv7huICk3t5ywtfCQyO09kX7lFIgd5rn7+MjwH5WNeqbQJxuaqjoXQnNZUgUsC
|
|
||||||
AwEAAaNTMFEwHQYDVR0OBBYEFNhMBG8q6a+iK2nECwVTn6B9EXZOMB8GA1UdIwQY
|
|
||||||
MBaAFNhMBG8q6a+iK2nECwVTn6B9EXZOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
|
|
||||||
hvcNAQELBQADggEBAJmudvx8+p5iIUsT8fm/fbVM0DA6qWALDYUJnTn3j6Lq4vpf
|
|
||||||
PFC+q1LmuWfBQMyqKrHrP3e493EctXoiSKZO6iN5dVJIur02OjGuiAEcsYuY1nLn
|
|
||||||
s9piiI+UEwxH6ux1NaHUnzsWauoBvRhzjXvO6SAVSZJYa9dY5mizXklDyDNuG5U0
|
|
||||||
lXv9egMGBsy0dG6eFXkU5CPdxWU540yI2sCtSAj7z+WRUD5k7gJ7tVoY3//jHQZG
|
|
||||||
5STTmm5t9kpIZTWkptyJos9oZJFYMIXqW2Fc6tyLZpRp31R78tDs6ETIkToDc0RR
|
|
||||||
jz66th6HI+ZlgIBQhw09+hYAhBDe9+Dmd/SzQZc=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEir6zmoWrwl57
|
|
||||||
0hedd9YaaOpOuQue44EUPOBWg4+TGyTlZWEFRbTBtFzpO2voYeb9X3uMB9EeyOhQ
|
|
||||||
6NFfbEvrLJKOW7BJXNcWJYOR27iqvPb0UioOhA5c7cdwSm4vddj5rebxRjSdes3M
|
|
||||||
O+w1K0CF6OCP2+Gt6ZgYlo+NzaEyK5klh7bBsMDo94g70sHqnWFySK94aRx+buqa
|
|
||||||
IYw6EL6g8yujLvKepoQlCjbOyhSZzSJzRtuaEE+jtJ+DhrZoEYy1qKXRJzoTJCrH
|
|
||||||
yKCNV7ZVwSwQ7+4biApN7ecsLXwkMjtPZF+5RSIHea5+/jI8B+VjXqm0Ccbmqo6F
|
|
||||||
0JzWVIFLAgMBAAECggEBAKceitVROQQ5e/mxRR9CfK1sNH/H3Ne3/1PkB6XIrFab
|
|
||||||
qB3evEatZOuon7A6NKEeTjl37Se+pdSVZOUXcqC/BzbraZre3+EhrkpIj72ApV+Y
|
|
||||||
2iwZiWVaaJQgI4uZ3mNAw8RaWJsj5S1a9I8LDOiQ5IZ45CmvABDPJeMScvJSvRRY
|
|
||||||
e5N0L6stqS7Z+IoyGVKUfp1iNO0YyywOUiSkIRXgscuRXZGYpiGPomsJ+Js1ejzW
|
|
||||||
jyStlZJEr4L1285rGPrmHqjTwFd+hG80Wc4179xL+WRE6HBEUZSiy95fe6kcPHXX
|
|
||||||
BgiVYtcFKmiBi2dTbxl4e94ut239i0HtlJ1ZJtLh+BECgYEA8a298K2zXkHosxhN
|
|
||||||
tRrH7XfMPTkHDDd3rxM21LT+fIXqinGUp9LYaDcbjuTPs8e33uKMd7R6Q40x89yW
|
|
||||||
IXNka/VL0PXUeV67aCVLLqgXDLGudluJinH0XvmI0CmBecFSMqIFmQlgqERoGGs3
|
|
||||||
UMac0p876T4XkGQQJdf62bFpE4UCgYEA0DBH0PDlOpwXccDgXrMfayr8HAhI+G5R
|
|
||||||
yWQ//9iirtU83chwIWwkh53eLLMzLgdqJnPiWyaUW5BqzmYuD23nhxcQ7PNdIqOO
|
|
||||||
H1sE6zqLNshv46t5QKlh1Q4qjd7UqtgrSrY63RXJCMWTwnNMeDLtj8gaKbjkrG3R
|
|
||||||
BM2ildt6Uo8CgYBX7NDUli1SloH1XlsvD047S8FHaM7yl994F3J0UmDfpszcj1P4
|
|
||||||
9pF64Mmq4/3Yt0li0mMuTb/Jgb3xrYgFJXkcecKahEVH3ropup+umsLAAIirUMQq
|
|
||||||
VSkFwJ0Qtnj/deDUwPNuaOX8cd65O5CFV6zIR9xBEDD8fBsP2ZLOzmefDQKBgQDF
|
|
||||||
m24vVthd/1cJdCgD+0VxNYXDHeIVXLFo1S0iLYCNLn3tjZlRQBKUXzZJe3ay0/rf
|
|
||||||
sNND7aSYHMYkTzydDJbc1PoNzxmyDUiTXpOWqyUExM/fbB1VUPE5h47AxqdZ2oGN
|
|
||||||
EtdgjpMZLmCIC2SkGsL+3NJok8UKHdpuErmmQIMk5QKBgQCgEWcYtLXC3YDYMFdI
|
|
||||||
UgcTebFqSs3mLYgub1xekW3IXR2yom4V5fQTLiF7Yfn2dpDW4IcMU0UJFYVsUlhK
|
|
||||||
aGtet4Vm5Nn8+Mghot5yAjqO9yAUaub7wgifKIe99tQKd8uZyCvJ0hhvmDDSfx4m
|
|
||||||
B/TEiFAO99yF49iSxEVSAS6pqQ==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
151
examples/docker/php-app/README.md
Normal file
151
examples/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
examples/docker/php-app/index.php
Normal file
152
examples/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
examples/docker/php-app/info.php
Normal file
9
examples/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
examples/docker/php-app/test-path-info.php
Normal file
106
examples/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>
|
||||||
97
examples/generate-keys.sh
Executable file
97
examples/generate-keys.sh
Executable file
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/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 ""
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
mkdir -p examples/static
|
||||||
|
mkdir -p examples/docker
|
||||||
|
mkdir -p examples/docker/certs/haproxy
|
||||||
|
mkdir -p examples/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 examples/static/host1.local.pem \
|
||||||
|
-out examples/static/host1.local.pem \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Organization/CN=host1.local"
|
||||||
|
|
||||||
|
# Copy to swarm directory
|
||||||
|
cp examples/static/host1.local.pem examples/swarm/certs/host1.local.pem
|
||||||
|
echo " Created host1.local.pem (4096-bit, 10 years)"
|
||||||
|
echo " - examples/static/host1.local.pem"
|
||||||
|
echo " - examples/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 examples/docker/host2.local.pem \
|
||||||
|
-out examples/docker/host2.local.pem \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Organization/CN=host2.local"
|
||||||
|
|
||||||
|
# Copy to swarm directory
|
||||||
|
cp examples/docker/host2.local.pem examples/swarm/certs/host2.local.pem
|
||||||
|
echo " Created host2.local.pem (2048-bit, 1 year)"
|
||||||
|
echo " - examples/docker/host2.local.pem"
|
||||||
|
echo " - examples/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 examples/docker/jwt_private.pem 2048
|
||||||
|
|
||||||
|
# Extract public key
|
||||||
|
openssl rsa -in examples/docker/jwt_private.pem -pubout -out examples/docker/jwt_pubkey.pem
|
||||||
|
|
||||||
|
echo " Created JWT key pair (2048-bit)"
|
||||||
|
echo " - examples/docker/jwt_private.pem (private key)"
|
||||||
|
echo " - examples/docker/jwt_pubkey.pem (public key)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Generate Placeholder Certificate
|
||||||
|
# ============================================================================
|
||||||
|
echo "Generating placeholder certificate..."
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
|
-keyout examples/docker/certs/haproxy/.place_holder_cert.pem \
|
||||||
|
-out examples/docker/certs/haproxy/.place_holder_cert.pem \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Organization/CN=placeholder"
|
||||||
|
|
||||||
|
echo " Created placeholder certificate"
|
||||||
|
echo " - examples/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 ""
|
||||||
50
examples/kubernetes/README.md
Normal file
50
examples/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)
|
||||||
134
examples/kubernetes/cloudflare.yml
Normal file
134
examples/kubernetes/cloudflare.yml
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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/4.6.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 "App Behind Cloudflare"
|
||||||
|
#
|
||||||
|
# # In production behind Cloudflare, the plugin will restore real client IPs
|
||||||
|
# # from the CF-Connecting-IP header
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# 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: byjg/static-httpserver
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: TITLE
|
||||||
|
value: "App Behind Cloudflare"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '0.1'
|
||||||
|
memory: '64Mi'
|
||||||
|
requests:
|
||||||
|
cpu: '0.05'
|
||||||
|
memory: '32Mi'
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
|
||||||
|
# Enable Cloudflare plugin
|
||||||
|
easyhaproxy.plugins: "cloudflare"
|
||||||
|
|
||||||
|
# Optional: Specify custom IP list path
|
||||||
|
# easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst"
|
||||||
|
name: webapp-ingress-cloudflare
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: myapp.example.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: webapp-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
121
examples/kubernetes/ip-whitelist.yml
Normal file
121
examples/kubernetes/ip-whitelist.yml
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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/4.6.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:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
|
||||||
|
# Enable IP whitelist plugin
|
||||||
|
easyhaproxy.plugins: "ip_whitelist"
|
||||||
|
|
||||||
|
# Allow specific IPs and networks
|
||||||
|
# UPDATE THIS with your actual office/VPN IPs!
|
||||||
|
easyhaproxy.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,198.51.100.42,10.0.0.0/8"
|
||||||
|
|
||||||
|
# Status code to return for blocked IPs
|
||||||
|
easyhaproxy.plugin.ip_whitelist.status_code: "403"
|
||||||
|
name: admin-ingress-whitelist
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: admin.example.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: admin-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
141
examples/kubernetes/jwt-validator.yml
Normal file
141
examples/kubernetes/jwt-validator.yml
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# EXAMPLE: JWT Validator Plugin for Kubernetes
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# 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/4.6.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:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
rules:
|
||||||
|
- host: api.example.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: api-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
272
examples/kubernetes/plugins-combined.yml
Normal file
272
examples/kubernetes/plugins-combined.yml
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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/4.6.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:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
kubernetes.io/ingress.class: easyhaproxy-ingress
|
||||||
|
# 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:
|
||||||
|
rules:
|
||||||
|
- host: admin.example.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: admin-service
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
|
@ -1,3 +1,57 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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/4.6.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
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,58 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# 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/4.6.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
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
|
|
|
||||||
97
examples/static/README.md
Normal file
97
examples/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 (`./examples/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 ../.. && ./examples/generate-keys.sh && cd examples/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
examples/static/conf/config-basic.yml
Normal file
31
examples/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
examples/static/conf/config-certbot.yml
Normal file
88
examples/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
examples/static/conf/config-deny-pages.yml
Normal file
78
examples/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
examples/static/conf/config-jwt-validator.yml
Normal file
86
examples/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
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
stats:
|
|
||||||
username: admin
|
|
||||||
password: password
|
|
||||||
port: 1936 # Optional (default 1936)
|
|
||||||
|
|
||||||
customerrors: true # Optional (default false)
|
|
||||||
|
|
||||||
easymapping:
|
|
||||||
- port: 80
|
|
||||||
redirect:
|
|
||||||
host1.local: https://host1.local
|
|
||||||
www.host1.local: https://host1.local
|
|
||||||
|
|
||||||
- port: 443
|
|
||||||
ssl: true
|
|
||||||
hosts:
|
|
||||||
host1.local:
|
|
||||||
containers:
|
|
||||||
- container:8080
|
|
||||||
|
|
@ -1,7 +1,62 @@
|
||||||
# To test:
|
# ==============================================================================
|
||||||
|
# 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 ../.. && ./examples/generate-keys.sh && cd examples/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/
|
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
||||||
|
# # Expected: 200 OK with "Hello from Static HTTP Server!"
|
||||||
version: "3"
|
#
|
||||||
|
# # 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:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFDTCCAvWgAwIBAgIURi+w1ZVgeedTlNIAwqQBMJv6dXswDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFjEUMBIGA1UEAwwLaG9zdDEubG9jYWwwHhcNMjEwODEwMTg0OTA2WhcNMzEw
|
|
||||||
ODA4MTg0OTA2WjAWMRQwEgYDVQQDDAtob3N0MS5sb2NhbDCCAiIwDQYJKoZIhvcN
|
|
||||||
AQEBBQADggIPADCCAgoCggIBAMBDAhLAygJuaW6w6ffigzTAAGXpmEz0tIxn1k4Z
|
|
||||||
x5wN5rpv/qu0QMYz+Av2u1eOKEKZeaFRVpT0r93dX7IvbEZHt25GPiBvlLGqhjKR
|
|
||||||
PnSk/7U8XmsnttUAV7rVEK1UrdFw8/IwriQC+dhr0mnYfSDMkvBoMFpdhVNTrbAZ
|
|
||||||
1TB6rQjE7Ar0Mt8my96XJmwrcjK2Tj+E2rgPIUz1e5cekFYIDSBatmw+3+vr+T5x
|
|
||||||
FNFkJ2o30W5o8ZflCJJzrVaihqQics6ZKDgpf7iqXMFiwWIlhdQpGvx5Gf/KFTK9
|
|
||||||
UaOnRZz/X+2CebAFaTHR3k/PYppWTgBBBuRvlpCw+wdnkmteC0SQRF91QWVr7ejo
|
|
||||||
7KaOlGI5VtvMUsWvTeAZmpaymIaATETuOJaY0JU11OmLeD9DOj5E2SQ7qIX/pFcp
|
|
||||||
xpzG5j4c+MlgvxP2VAkNTeAXCaYiPBQH5ZZg0HE2WnB1KhLRFlHd4iHQD2GJ5yN/
|
|
||||||
6fCFBfZfKSeK8JauwxgWkra53OcDq/mKd+DA/dK+/ruG7tqwVgIa04HOplzM7LYR
|
|
||||||
GB0Irs9+lr5/PJbQZmU073Mdn6cXAg3p+6wvwFlDkS5v13gBDYNHtF62bc551edF
|
|
||||||
Z6kGzJ7wmGRo84aBP7MuRZeReLOrSS67a1wLdzZsMnP1TJ7x9Lfr9MKl2uDnQdnY
|
|
||||||
ex8DAgMBAAGjUzBRMB0GA1UdDgQWBBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAfBgNV
|
|
||||||
HSMEGDAWgBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAPBgNVHRMBAf8EBTADAQH/MA0G
|
|
||||||
CSqGSIb3DQEBCwUAA4ICAQChQYNuah3+mTpIBDYxGrjTJNuOTIMaWzMyi1tkf+L0
|
|
||||||
sEGwpbmAO2mWWQYF7WVLsi98PULh3adjt2jiud9VlaaC6gnwn5Zo1+Pilo9sNLLW
|
|
||||||
6ij0+rN4kwIm/pNqi+jDuu2cvAuHIwZWeh8bEe/5UCxo4ihmWFQN8eJ6TUKCphRC
|
|
||||||
6Eor/SSZZBQHgPl0BchzHOkwu7R3LCndRqxjhAoVb9yQOV+ZsmTeJXulwNzJ1uLt
|
|
||||||
T8OIgIiDpmBo7HSN2H0k3chx00AsjUyJ9mmAWPejFe/KXLRPcVZR17jhzgfIBEzs
|
|
||||||
M5WtWFm1aHDjVv6M6iteVm61E9T+k/M11ru1e2YwsxTDvb6x04mcrNu9soqddBbr
|
|
||||||
VfpluuoQ/hEAbXtFNPoTySpz0cwOwcHCowVOLmdKgvImszZiMyHHG8VGGmPh88n7
|
|
||||||
wVxb0gV0P4RMrcMLdeTdn55YQr1CqBr34eB6ol6AsbTm3VzBHRVmFNksl1o5JB5t
|
|
||||||
tXLgF/G8/rzJ/4m1PaVuxrB7DxUmIk8EPbSIVkvZvd7LBzKwQ6IfVaucewHfEajQ
|
|
||||||
VIiexSMiFc7lw3KnxjOHZjf6FM9VYg3No++GdC99s7LkIuJwAMLNqTQ7Hvhn7YvP
|
|
||||||
4FlSIgc6xj0YkGZEQlb5o/5nauEqQU0ABgw6jtI4NxrNLT6cp7CO4M0xIDEg/3YD
|
|
||||||
aA==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDAQwISwMoCbmlu
|
|
||||||
sOn34oM0wABl6ZhM9LSMZ9ZOGcecDea6b/6rtEDGM/gL9rtXjihCmXmhUVaU9K/d
|
|
||||||
3V+yL2xGR7duRj4gb5SxqoYykT50pP+1PF5rJ7bVAFe61RCtVK3RcPPyMK4kAvnY
|
|
||||||
a9Jp2H0gzJLwaDBaXYVTU62wGdUweq0IxOwK9DLfJsvelyZsK3Iytk4/hNq4DyFM
|
|
||||||
9XuXHpBWCA0gWrZsPt/r6/k+cRTRZCdqN9FuaPGX5QiSc61WooakInLOmSg4KX+4
|
|
||||||
qlzBYsFiJYXUKRr8eRn/yhUyvVGjp0Wc/1/tgnmwBWkx0d5Pz2KaVk4AQQbkb5aQ
|
|
||||||
sPsHZ5JrXgtEkERfdUFla+3o6OymjpRiOVbbzFLFr03gGZqWspiGgExE7jiWmNCV
|
|
||||||
NdTpi3g/Qzo+RNkkO6iF/6RXKcacxuY+HPjJYL8T9lQJDU3gFwmmIjwUB+WWYNBx
|
|
||||||
NlpwdSoS0RZR3eIh0A9hiecjf+nwhQX2XyknivCWrsMYFpK2udznA6v5infgwP3S
|
|
||||||
vv67hu7asFYCGtOBzqZczOy2ERgdCK7Pfpa+fzyW0GZlNO9zHZ+nFwIN6fusL8BZ
|
|
||||||
Q5Eub9d4AQ2DR7Retm3OedXnRWepBsye8JhkaPOGgT+zLkWXkXizq0kuu2tcC3c2
|
|
||||||
bDJz9Uye8fS36/TCpdrg50HZ2HsfAwIDAQABAoICAQC/xZbZ0cctqagsqvaVNTEe
|
|
||||||
eq1q+hfaGvPEYQaYHIrIE+2i5XcnGcLKcKfodxDjAn8R/zgdOp6cMX0CVn/PohHk
|
|
||||||
AEDtE8+AVwwAM1FsOwgLHVGaGz8qrxBlYdQgHcpmueIu2PXbC8eHUBiaUOIuhaw5
|
|
||||||
/RRMDAC/Ai2ssfi7gOjvVE4oQxQW0QG1KGOOAUJn/uYHw2RFY2Uu1pimxO2kDO53
|
|
||||||
gcxmC1WOnyCHmHaiW/Uh7z6JamfSM4dXtTJZslyh37dhHKNbg9VkP7CQKA4hLzop
|
|
||||||
hbf5qY6rargONiny1HgMPxrmwKuUouJyOtN0yBtxjDCUNaXUBwiy7sNGS+H4vsyB
|
|
||||||
5P9HhIHStu+FZt3HG7EIqCndiaSKDS4jWaVQAbbo4nZ2Zs2BD+xDePRCRUqX7rM4
|
|
||||||
4XzPIRWWXmmWf/7Ig29Hbrp4a9LcOmQ2leCJtbaTFSN96OLUJ5E+hQ0ulCZgBVmQ
|
|
||||||
RCUYkJP4lOzbaKdzjxgHMrHzm45eUFf8LirOxi2uyxXHQmDNu4b3X18kt3PgUmUm
|
|
||||||
3dXpl3fqSyJa7SCV8ZNBrsrDq1E+thYtu91QbVSGxHd9HrNVe3XdLbOCdU9CuC69
|
|
||||||
Nglznaa7sZLqmyKejTfGsY7xrWdNcMPl4p4fcID/O4EpASZforpTeKNT0ZIfZZew
|
|
||||||
b0mAQeYZqQM8i/qMYN/uAQKCAQEA5qg1sRNMc6VdM/tRglasGYoxjgRC2OqADZgs
|
|
||||||
mAXMUJ3kErpyxt+eCimy8ibuYpzRTIQ8fBTWRkCtRZXJ7+KcLVtk9QZIoLbhyNwd
|
|
||||||
4IxEQZFuUljDbvSjTLSycsHvo65ibWIfTL7bgWlLGgGq/UOzfGsgH6S9wLp5G30G
|
|
||||||
8ELyjI5eTIYICrfTmVL+c45MRpEMKo+cvz8PysiaOFTn3cyswPVdYaeEEqMQjU8w
|
|
||||||
IGNsGZLytY7BABBcY0ldrtba/O+Fv/+RH7uUtzP7xpCIwFCx80ZzN+WRy9NvI63U
|
|
||||||
zq3yIBoW9GyApD2+PLaPNxf7QLTUChY1Zz/dYRltKOxv2Aa5gQKCAQEA1WLWNqp0
|
|
||||||
fhB/ZtfSEShxFMM89cjN6Aaz1WKL7uTBou9oSJnxjkhkaV76acnT/iqXtxMNgHi1
|
|
||||||
fImDpU3PvM0Y4Ud2T47oHc6P1BrZPN/GmXy/s6BAEdPwLe7J+4nTISHAdGmrh+a/
|
|
||||||
5pktu32g9lWqftxecFIVSLPWkxT0XKiMxp1ffkL+OavpMgMFZK41iKs3dNShKPog
|
|
||||||
L8GSPcP9x/yn78P2eK3N+PGjlA6pPzrANyWU7N0/bmHcB9TKP+udYWcjVhru7MYN
|
|
||||||
wNrE4kKdC8v8i7x7tDbvb79T+Fo6PIh53p0OsnZzA8UR0QNR+vDQufQuyaj8REC+
|
|
||||||
ZG8YyCKsvk8ygwKCAQA/fsSxB0f/eeErYx6wC536teEoYCHqxrsTgvWbr9TryFs1
|
|
||||||
kJ/yATLnR01cfb0X5mVzc9+WpMHLuxg31KEvaSlnDwa+sMkjfNSwz29mFhbgGeHN
|
|
||||||
x2OdUrj1b7TEBIEshN/RjrZhERUqDcs/0H+6kn2BXZgNPfOCb5LRL1zOnQ9aBAMP
|
|
||||||
e8IQ+UPFrGQheWWj81/vA3O57ekyAID7ytu9Yg+YWrMnI88mtj7jN45fDB+A9sPb
|
|
||||||
mP2mP9q+9j5U2A6WnHUsQnU30BKDUEsaAUWz80LZXmZvV8IH4x9wKfUwJBBIKAZz
|
|
||||||
qL7M97Y7zmGkX/Spfl30nOJ8lschaLd1EYlEZa2BAoIBAQCye25T4TV5MJFv0zuZ
|
|
||||||
MGuNg1Sc/O4Fkn2fEUOceWjhwUBH4cPjT/f1DwWDsNaJ9NRbxCr5931OArPDc5c8
|
|
||||||
A404+Y4jM5RBQkKZli94tHAod+jc9UBB6TUvJll59SlMwC9679wS21ZOKnfPKGCX
|
|
||||||
SsZGQEsZxf6ZhhsHgXJ3gl/lzUJPmPeOA5YVR+Od9/09KIFFTojSfoynhVCuKx49
|
|
||||||
xb4uVYn2HOJ4xJ0fPTghdCHMvrmXeeQRjvb88eaNmqVUEHHFFtgb4fklA5fE7RTx
|
|
||||||
BhliRDBwZ7bUkINK6yVk9n6BTns5mMvRLmgdnJpYvE7KC02LTbZb3I+j8C0ZUa+N
|
|
||||||
qy7DAoIBAAieribS7WUcl2aBlkm5+W7qNm/INm5zvnoSPo6V3wa5hs6f9+C/kbdF
|
|
||||||
87jQPA/YFe3uR2sAJ7slX5euZK8WmfpFmgzlu0sEz81MLQ/WypZtZytyVtWzB2Pu
|
|
||||||
XCW1tdSH9eI2BmhXgokHNTM48Nk/xOENrP/seXrIx5LK0hnDHZotu/z6+YSkB9hF
|
|
||||||
cm2fZygD1dMLX6liRimxyFY+dICJNB95JifTLWYnWeGddkwPtXUeGXE1olzvNkLD
|
|
||||||
zMzE09uhkx/lRJnteOBEZaf80OB/09Oi9b9/rxY59dwsH6GaxLoTfEKuPnvBVMNR
|
|
||||||
YkU14WzQKleFkiBJI9lVvnfgGnOlgg0=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
67
examples/swarm/README.md
Normal file
67
examples/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)
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFDTCCAvWgAwIBAgIURi+w1ZVgeedTlNIAwqQBMJv6dXswDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFjEUMBIGA1UEAwwLaG9zdDEubG9jYWwwHhcNMjEwODEwMTg0OTA2WhcNMzEw
|
|
||||||
ODA4MTg0OTA2WjAWMRQwEgYDVQQDDAtob3N0MS5sb2NhbDCCAiIwDQYJKoZIhvcN
|
|
||||||
AQEBBQADggIPADCCAgoCggIBAMBDAhLAygJuaW6w6ffigzTAAGXpmEz0tIxn1k4Z
|
|
||||||
x5wN5rpv/qu0QMYz+Av2u1eOKEKZeaFRVpT0r93dX7IvbEZHt25GPiBvlLGqhjKR
|
|
||||||
PnSk/7U8XmsnttUAV7rVEK1UrdFw8/IwriQC+dhr0mnYfSDMkvBoMFpdhVNTrbAZ
|
|
||||||
1TB6rQjE7Ar0Mt8my96XJmwrcjK2Tj+E2rgPIUz1e5cekFYIDSBatmw+3+vr+T5x
|
|
||||||
FNFkJ2o30W5o8ZflCJJzrVaihqQics6ZKDgpf7iqXMFiwWIlhdQpGvx5Gf/KFTK9
|
|
||||||
UaOnRZz/X+2CebAFaTHR3k/PYppWTgBBBuRvlpCw+wdnkmteC0SQRF91QWVr7ejo
|
|
||||||
7KaOlGI5VtvMUsWvTeAZmpaymIaATETuOJaY0JU11OmLeD9DOj5E2SQ7qIX/pFcp
|
|
||||||
xpzG5j4c+MlgvxP2VAkNTeAXCaYiPBQH5ZZg0HE2WnB1KhLRFlHd4iHQD2GJ5yN/
|
|
||||||
6fCFBfZfKSeK8JauwxgWkra53OcDq/mKd+DA/dK+/ruG7tqwVgIa04HOplzM7LYR
|
|
||||||
GB0Irs9+lr5/PJbQZmU073Mdn6cXAg3p+6wvwFlDkS5v13gBDYNHtF62bc551edF
|
|
||||||
Z6kGzJ7wmGRo84aBP7MuRZeReLOrSS67a1wLdzZsMnP1TJ7x9Lfr9MKl2uDnQdnY
|
|
||||||
ex8DAgMBAAGjUzBRMB0GA1UdDgQWBBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAfBgNV
|
|
||||||
HSMEGDAWgBSQ/mtZd6h8en9YQVH6HO1PlWWiqzAPBgNVHRMBAf8EBTADAQH/MA0G
|
|
||||||
CSqGSIb3DQEBCwUAA4ICAQChQYNuah3+mTpIBDYxGrjTJNuOTIMaWzMyi1tkf+L0
|
|
||||||
sEGwpbmAO2mWWQYF7WVLsi98PULh3adjt2jiud9VlaaC6gnwn5Zo1+Pilo9sNLLW
|
|
||||||
6ij0+rN4kwIm/pNqi+jDuu2cvAuHIwZWeh8bEe/5UCxo4ihmWFQN8eJ6TUKCphRC
|
|
||||||
6Eor/SSZZBQHgPl0BchzHOkwu7R3LCndRqxjhAoVb9yQOV+ZsmTeJXulwNzJ1uLt
|
|
||||||
T8OIgIiDpmBo7HSN2H0k3chx00AsjUyJ9mmAWPejFe/KXLRPcVZR17jhzgfIBEzs
|
|
||||||
M5WtWFm1aHDjVv6M6iteVm61E9T+k/M11ru1e2YwsxTDvb6x04mcrNu9soqddBbr
|
|
||||||
VfpluuoQ/hEAbXtFNPoTySpz0cwOwcHCowVOLmdKgvImszZiMyHHG8VGGmPh88n7
|
|
||||||
wVxb0gV0P4RMrcMLdeTdn55YQr1CqBr34eB6ol6AsbTm3VzBHRVmFNksl1o5JB5t
|
|
||||||
tXLgF/G8/rzJ/4m1PaVuxrB7DxUmIk8EPbSIVkvZvd7LBzKwQ6IfVaucewHfEajQ
|
|
||||||
VIiexSMiFc7lw3KnxjOHZjf6FM9VYg3No++GdC99s7LkIuJwAMLNqTQ7Hvhn7YvP
|
|
||||||
4FlSIgc6xj0YkGZEQlb5o/5nauEqQU0ABgw6jtI4NxrNLT6cp7CO4M0xIDEg/3YD
|
|
||||||
aA==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDAQwISwMoCbmlu
|
|
||||||
sOn34oM0wABl6ZhM9LSMZ9ZOGcecDea6b/6rtEDGM/gL9rtXjihCmXmhUVaU9K/d
|
|
||||||
3V+yL2xGR7duRj4gb5SxqoYykT50pP+1PF5rJ7bVAFe61RCtVK3RcPPyMK4kAvnY
|
|
||||||
a9Jp2H0gzJLwaDBaXYVTU62wGdUweq0IxOwK9DLfJsvelyZsK3Iytk4/hNq4DyFM
|
|
||||||
9XuXHpBWCA0gWrZsPt/r6/k+cRTRZCdqN9FuaPGX5QiSc61WooakInLOmSg4KX+4
|
|
||||||
qlzBYsFiJYXUKRr8eRn/yhUyvVGjp0Wc/1/tgnmwBWkx0d5Pz2KaVk4AQQbkb5aQ
|
|
||||||
sPsHZ5JrXgtEkERfdUFla+3o6OymjpRiOVbbzFLFr03gGZqWspiGgExE7jiWmNCV
|
|
||||||
NdTpi3g/Qzo+RNkkO6iF/6RXKcacxuY+HPjJYL8T9lQJDU3gFwmmIjwUB+WWYNBx
|
|
||||||
NlpwdSoS0RZR3eIh0A9hiecjf+nwhQX2XyknivCWrsMYFpK2udznA6v5infgwP3S
|
|
||||||
vv67hu7asFYCGtOBzqZczOy2ERgdCK7Pfpa+fzyW0GZlNO9zHZ+nFwIN6fusL8BZ
|
|
||||||
Q5Eub9d4AQ2DR7Retm3OedXnRWepBsye8JhkaPOGgT+zLkWXkXizq0kuu2tcC3c2
|
|
||||||
bDJz9Uye8fS36/TCpdrg50HZ2HsfAwIDAQABAoICAQC/xZbZ0cctqagsqvaVNTEe
|
|
||||||
eq1q+hfaGvPEYQaYHIrIE+2i5XcnGcLKcKfodxDjAn8R/zgdOp6cMX0CVn/PohHk
|
|
||||||
AEDtE8+AVwwAM1FsOwgLHVGaGz8qrxBlYdQgHcpmueIu2PXbC8eHUBiaUOIuhaw5
|
|
||||||
/RRMDAC/Ai2ssfi7gOjvVE4oQxQW0QG1KGOOAUJn/uYHw2RFY2Uu1pimxO2kDO53
|
|
||||||
gcxmC1WOnyCHmHaiW/Uh7z6JamfSM4dXtTJZslyh37dhHKNbg9VkP7CQKA4hLzop
|
|
||||||
hbf5qY6rargONiny1HgMPxrmwKuUouJyOtN0yBtxjDCUNaXUBwiy7sNGS+H4vsyB
|
|
||||||
5P9HhIHStu+FZt3HG7EIqCndiaSKDS4jWaVQAbbo4nZ2Zs2BD+xDePRCRUqX7rM4
|
|
||||||
4XzPIRWWXmmWf/7Ig29Hbrp4a9LcOmQ2leCJtbaTFSN96OLUJ5E+hQ0ulCZgBVmQ
|
|
||||||
RCUYkJP4lOzbaKdzjxgHMrHzm45eUFf8LirOxi2uyxXHQmDNu4b3X18kt3PgUmUm
|
|
||||||
3dXpl3fqSyJa7SCV8ZNBrsrDq1E+thYtu91QbVSGxHd9HrNVe3XdLbOCdU9CuC69
|
|
||||||
Nglznaa7sZLqmyKejTfGsY7xrWdNcMPl4p4fcID/O4EpASZforpTeKNT0ZIfZZew
|
|
||||||
b0mAQeYZqQM8i/qMYN/uAQKCAQEA5qg1sRNMc6VdM/tRglasGYoxjgRC2OqADZgs
|
|
||||||
mAXMUJ3kErpyxt+eCimy8ibuYpzRTIQ8fBTWRkCtRZXJ7+KcLVtk9QZIoLbhyNwd
|
|
||||||
4IxEQZFuUljDbvSjTLSycsHvo65ibWIfTL7bgWlLGgGq/UOzfGsgH6S9wLp5G30G
|
|
||||||
8ELyjI5eTIYICrfTmVL+c45MRpEMKo+cvz8PysiaOFTn3cyswPVdYaeEEqMQjU8w
|
|
||||||
IGNsGZLytY7BABBcY0ldrtba/O+Fv/+RH7uUtzP7xpCIwFCx80ZzN+WRy9NvI63U
|
|
||||||
zq3yIBoW9GyApD2+PLaPNxf7QLTUChY1Zz/dYRltKOxv2Aa5gQKCAQEA1WLWNqp0
|
|
||||||
fhB/ZtfSEShxFMM89cjN6Aaz1WKL7uTBou9oSJnxjkhkaV76acnT/iqXtxMNgHi1
|
|
||||||
fImDpU3PvM0Y4Ud2T47oHc6P1BrZPN/GmXy/s6BAEdPwLe7J+4nTISHAdGmrh+a/
|
|
||||||
5pktu32g9lWqftxecFIVSLPWkxT0XKiMxp1ffkL+OavpMgMFZK41iKs3dNShKPog
|
|
||||||
L8GSPcP9x/yn78P2eK3N+PGjlA6pPzrANyWU7N0/bmHcB9TKP+udYWcjVhru7MYN
|
|
||||||
wNrE4kKdC8v8i7x7tDbvb79T+Fo6PIh53p0OsnZzA8UR0QNR+vDQufQuyaj8REC+
|
|
||||||
ZG8YyCKsvk8ygwKCAQA/fsSxB0f/eeErYx6wC536teEoYCHqxrsTgvWbr9TryFs1
|
|
||||||
kJ/yATLnR01cfb0X5mVzc9+WpMHLuxg31KEvaSlnDwa+sMkjfNSwz29mFhbgGeHN
|
|
||||||
x2OdUrj1b7TEBIEshN/RjrZhERUqDcs/0H+6kn2BXZgNPfOCb5LRL1zOnQ9aBAMP
|
|
||||||
e8IQ+UPFrGQheWWj81/vA3O57ekyAID7ytu9Yg+YWrMnI88mtj7jN45fDB+A9sPb
|
|
||||||
mP2mP9q+9j5U2A6WnHUsQnU30BKDUEsaAUWz80LZXmZvV8IH4x9wKfUwJBBIKAZz
|
|
||||||
qL7M97Y7zmGkX/Spfl30nOJ8lschaLd1EYlEZa2BAoIBAQCye25T4TV5MJFv0zuZ
|
|
||||||
MGuNg1Sc/O4Fkn2fEUOceWjhwUBH4cPjT/f1DwWDsNaJ9NRbxCr5931OArPDc5c8
|
|
||||||
A404+Y4jM5RBQkKZli94tHAod+jc9UBB6TUvJll59SlMwC9679wS21ZOKnfPKGCX
|
|
||||||
SsZGQEsZxf6ZhhsHgXJ3gl/lzUJPmPeOA5YVR+Od9/09KIFFTojSfoynhVCuKx49
|
|
||||||
xb4uVYn2HOJ4xJ0fPTghdCHMvrmXeeQRjvb88eaNmqVUEHHFFtgb4fklA5fE7RTx
|
|
||||||
BhliRDBwZ7bUkINK6yVk9n6BTns5mMvRLmgdnJpYvE7KC02LTbZb3I+j8C0ZUa+N
|
|
||||||
qy7DAoIBAAieribS7WUcl2aBlkm5+W7qNm/INm5zvnoSPo6V3wa5hs6f9+C/kbdF
|
|
||||||
87jQPA/YFe3uR2sAJ7slX5euZK8WmfpFmgzlu0sEz81MLQ/WypZtZytyVtWzB2Pu
|
|
||||||
XCW1tdSH9eI2BmhXgokHNTM48Nk/xOENrP/seXrIx5LK0hnDHZotu/z6+YSkB9hF
|
|
||||||
cm2fZygD1dMLX6liRimxyFY+dICJNB95JifTLWYnWeGddkwPtXUeGXE1olzvNkLD
|
|
||||||
zMzE09uhkx/lRJnteOBEZaf80OB/09Oi9b9/rxY59dwsH6GaxLoTfEKuPnvBVMNR
|
|
||||||
YkU14WzQKleFkiBJI9lVvnfgGnOlgg0=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDqTCCApGgAwIBAgIUId5b9t9uqH78g02EzbWF6FKVw3gwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwZDELMAkGA1UEBhMCQlIxFzAVBgNVBAgMDlJpbyBkZSBKYW5laXJvMRcwFQYD
|
|
||||||
VQQHDA5SaW8gZGUgSmFuZWlybzENMAsGA1UECgwEQUNNRTEUMBIGA1UEAwwLaG9z
|
|
||||||
dDIubG9jYWwwHhcNMjIwODE1MDQyNzA1WhcNMjMwODE1MDQyNzA1WjBkMQswCQYD
|
|
||||||
VQQGEwJCUjEXMBUGA1UECAwOUmlvIGRlIEphbmVpcm8xFzAVBgNVBAcMDlJpbyBk
|
|
||||||
ZSBKYW5laXJvMQ0wCwYDVQQKDARBQ01FMRQwEgYDVQQDDAtob3N0Mi5sb2NhbDCC
|
|
||||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMSKvrOahavCXnvSF5131hpo
|
|
||||||
6k65C57jgRQ84FaDj5MbJOVlYQVFtMG0XOk7a+hh5v1fe4wH0R7I6FDo0V9sS+ss
|
|
||||||
ko5bsElc1xYlg5HbuKq89vRSKg6EDlztx3BKbi912Pmt5vFGNJ16zcw77DUrQIXo
|
|
||||||
4I/b4a3pmBiWj43NoTIrmSWHtsGwwOj3iDvSweqdYXJIr3hpHH5u6pohjDoQvqDz
|
|
||||||
K6Mu8p6mhCUKNs7KFJnNInNG25oQT6O0n4OGtmgRjLWopdEnOhMkKsfIoI1XtlXB
|
|
||||||
LBDv7huICk3t5ywtfCQyO09kX7lFIgd5rn7+MjwH5WNeqbQJxuaqjoXQnNZUgUsC
|
|
||||||
AwEAAaNTMFEwHQYDVR0OBBYEFNhMBG8q6a+iK2nECwVTn6B9EXZOMB8GA1UdIwQY
|
|
||||||
MBaAFNhMBG8q6a+iK2nECwVTn6B9EXZOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
|
|
||||||
hvcNAQELBQADggEBAJmudvx8+p5iIUsT8fm/fbVM0DA6qWALDYUJnTn3j6Lq4vpf
|
|
||||||
PFC+q1LmuWfBQMyqKrHrP3e493EctXoiSKZO6iN5dVJIur02OjGuiAEcsYuY1nLn
|
|
||||||
s9piiI+UEwxH6ux1NaHUnzsWauoBvRhzjXvO6SAVSZJYa9dY5mizXklDyDNuG5U0
|
|
||||||
lXv9egMGBsy0dG6eFXkU5CPdxWU540yI2sCtSAj7z+WRUD5k7gJ7tVoY3//jHQZG
|
|
||||||
5STTmm5t9kpIZTWkptyJos9oZJFYMIXqW2Fc6tyLZpRp31R78tDs6ETIkToDc0RR
|
|
||||||
jz66th6HI+ZlgIBQhw09+hYAhBDe9+Dmd/SzQZc=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEir6zmoWrwl57
|
|
||||||
0hedd9YaaOpOuQue44EUPOBWg4+TGyTlZWEFRbTBtFzpO2voYeb9X3uMB9EeyOhQ
|
|
||||||
6NFfbEvrLJKOW7BJXNcWJYOR27iqvPb0UioOhA5c7cdwSm4vddj5rebxRjSdes3M
|
|
||||||
O+w1K0CF6OCP2+Gt6ZgYlo+NzaEyK5klh7bBsMDo94g70sHqnWFySK94aRx+buqa
|
|
||||||
IYw6EL6g8yujLvKepoQlCjbOyhSZzSJzRtuaEE+jtJ+DhrZoEYy1qKXRJzoTJCrH
|
|
||||||
yKCNV7ZVwSwQ7+4biApN7ecsLXwkMjtPZF+5RSIHea5+/jI8B+VjXqm0Ccbmqo6F
|
|
||||||
0JzWVIFLAgMBAAECggEBAKceitVROQQ5e/mxRR9CfK1sNH/H3Ne3/1PkB6XIrFab
|
|
||||||
qB3evEatZOuon7A6NKEeTjl37Se+pdSVZOUXcqC/BzbraZre3+EhrkpIj72ApV+Y
|
|
||||||
2iwZiWVaaJQgI4uZ3mNAw8RaWJsj5S1a9I8LDOiQ5IZ45CmvABDPJeMScvJSvRRY
|
|
||||||
e5N0L6stqS7Z+IoyGVKUfp1iNO0YyywOUiSkIRXgscuRXZGYpiGPomsJ+Js1ejzW
|
|
||||||
jyStlZJEr4L1285rGPrmHqjTwFd+hG80Wc4179xL+WRE6HBEUZSiy95fe6kcPHXX
|
|
||||||
BgiVYtcFKmiBi2dTbxl4e94ut239i0HtlJ1ZJtLh+BECgYEA8a298K2zXkHosxhN
|
|
||||||
tRrH7XfMPTkHDDd3rxM21LT+fIXqinGUp9LYaDcbjuTPs8e33uKMd7R6Q40x89yW
|
|
||||||
IXNka/VL0PXUeV67aCVLLqgXDLGudluJinH0XvmI0CmBecFSMqIFmQlgqERoGGs3
|
|
||||||
UMac0p876T4XkGQQJdf62bFpE4UCgYEA0DBH0PDlOpwXccDgXrMfayr8HAhI+G5R
|
|
||||||
yWQ//9iirtU83chwIWwkh53eLLMzLgdqJnPiWyaUW5BqzmYuD23nhxcQ7PNdIqOO
|
|
||||||
H1sE6zqLNshv46t5QKlh1Q4qjd7UqtgrSrY63RXJCMWTwnNMeDLtj8gaKbjkrG3R
|
|
||||||
BM2ildt6Uo8CgYBX7NDUli1SloH1XlsvD047S8FHaM7yl994F3J0UmDfpszcj1P4
|
|
||||||
9pF64Mmq4/3Yt0li0mMuTb/Jgb3xrYgFJXkcecKahEVH3ropup+umsLAAIirUMQq
|
|
||||||
VSkFwJ0Qtnj/deDUwPNuaOX8cd65O5CFV6zIR9xBEDD8fBsP2ZLOzmefDQKBgQDF
|
|
||||||
m24vVthd/1cJdCgD+0VxNYXDHeIVXLFo1S0iLYCNLn3tjZlRQBKUXzZJe3ay0/rf
|
|
||||||
sNND7aSYHMYkTzydDJbc1PoNzxmyDUiTXpOWqyUExM/fbB1VUPE5h47AxqdZ2oGN
|
|
||||||
EtdgjpMZLmCIC2SkGsL+3NJok8UKHdpuErmmQIMk5QKBgQCgEWcYtLXC3YDYMFdI
|
|
||||||
UgcTebFqSs3mLYgub1xekW3IXR2yom4V5fQTLiF7Yfn2dpDW4IcMU0UJFYVsUlhK
|
|
||||||
aGtet4Vm5Nn8+Mghot5yAjqO9yAUaub7wgifKIe99tQKd8uZyCvJ0hhvmDDSfx4m
|
|
||||||
B/TEiFAO99yF49iSxEVSAS6pqQ==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
122
examples/swarm/cloudflare.yml
Normal file
122
examples/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:4.6.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
|
||||||
|
|
@ -1,10 +1,57 @@
|
||||||
# To Install
|
# ==============================================================================
|
||||||
# docker network create --driver overlay --attachable easyhaproxy
|
# 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
|
# 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
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
haproxy:
|
haproxy:
|
||||||
image: byjg/easy-haproxy:4.6.0
|
image: byjg/easy-haproxy:4.6.0
|
||||||
|
|
|
||||||
113
examples/swarm/ip-whitelist.yml
Normal file
113
examples/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:4.6.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
examples/swarm/jwt-validator.yml
Normal file
132
examples/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:4.6.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
examples/swarm/plugins-combined.yml
Normal file
200
examples/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:4.6.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
|
||||||
|
|
@ -1,7 +1,46 @@
|
||||||
# To install:
|
# ==============================================================================
|
||||||
|
# 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
|
# docker stack deploy -c portainer.yml portainer
|
||||||
|
# ```
|
||||||
version: "3"
|
#
|
||||||
|
# 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:
|
services:
|
||||||
portainer:
|
portainer:
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,58 @@
|
||||||
# To install:
|
# ==============================================================================
|
||||||
|
# EXAMPLE: Basic Swarm Services with SSL
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# WHAT THIS DEMONSTRATES:
|
||||||
|
# - Basic Swarm services with SSL configuration
|
||||||
|
# - HTTP to HTTPS redirect
|
||||||
|
# - Two services with different SSL setups (embedded cert vs SSL file)
|
||||||
|
# - Using deploy.labels for service discovery in Swarm
|
||||||
|
#
|
||||||
|
# REQUIREMENTS (run these first):
|
||||||
|
# ```bash
|
||||||
|
# # Ensure EasyHAProxy is deployed
|
||||||
|
# docker stack deploy -c easyhaproxy.yml easyhaproxy
|
||||||
|
#
|
||||||
|
# # Generate SSL certificates
|
||||||
|
# cd ../.. && ./examples/generate-keys.sh && cd examples/swarm
|
||||||
|
#
|
||||||
|
# # Add to /etc/hosts (idempotent)
|
||||||
|
# grep -q "host1.local" /etc/hosts || echo "127.0.0.1 host1.local host2.local" | sudo tee -a /etc/hosts
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# HOW TO START:
|
||||||
|
# ```bash
|
||||||
# docker stack deploy -c services.yml services
|
# docker stack deploy -c services.yml services
|
||||||
|
# ```
|
||||||
#
|
#
|
||||||
# To test:
|
# HOW TO VERIFY IT'S WORKING:
|
||||||
|
# ```bash
|
||||||
|
# # Check services are running
|
||||||
|
# docker service ls | grep services
|
||||||
|
# # Expected: services_container and services_container2 with 1/1 replicas
|
||||||
|
#
|
||||||
|
# # Test HTTPS for host1.local
|
||||||
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
# curl -k -H "Host: host1.local" https://127.0.0.1/
|
||||||
|
# # Expected: 200 OK with hostname
|
||||||
|
#
|
||||||
|
# # Test HTTPS for host2.local
|
||||||
# curl -k -H "Host: host2.local" https://127.0.0.1/
|
# curl -k -H "Host: host2.local" https://127.0.0.1/
|
||||||
|
# # Expected: 200 OK with hostname
|
||||||
#
|
#
|
||||||
# curl -I -H Host:host1.local http://127.0.0.1
|
# # Test HTTP redirect
|
||||||
# HTTP/1.1 301 Moved Permanently
|
# curl -I -H "Host: host1.local" http://127.0.0.1
|
||||||
# content-length: 0
|
# # Expected: HTTP/1.1 301 Moved Permanently, Location: https://host1.local/
|
||||||
# location: https://host1.local/
|
|
||||||
#
|
#
|
||||||
# curl -I -H Host:host2.local http://127.0.0.1
|
# # Verify SSL certificate
|
||||||
# HTTP/1.1 301 Moved Permanently
|
# openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local < /dev/null
|
||||||
# content-length: 0
|
# ```
|
||||||
# location: https://host1.local/
|
|
||||||
#
|
#
|
||||||
# Test SSL:
|
# CLEAN UP:
|
||||||
# openssl s_client -showcerts -connect 127.0.0.1:443 -servername host1.local
|
# ```bash
|
||||||
|
# docker stack rm services
|
||||||
version: "3"
|
# ```
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
container:
|
container:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ type: application
|
||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.1.9
|
version: 1.0.0
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
class DockerLabelHandler:
|
class DockerLabelHandler:
|
||||||
|
|
@ -31,7 +33,16 @@ class DockerLabelHandler:
|
||||||
|
|
||||||
def get_json(self, label, default_value={}):
|
def get_json(self, label, default_value={}):
|
||||||
if self.has_label(label):
|
if self.has_label(label):
|
||||||
return json.loads(self.__data[label])
|
value = self.__data[label]
|
||||||
|
if not value: # Handle empty strings
|
||||||
|
return default_value
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
loggerEasyHaproxy.error(
|
||||||
|
f"Invalid JSON in label '{label}': {value}. Error: {e}. Using default value."
|
||||||
|
)
|
||||||
|
return default_value
|
||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
def set_data(self, data):
|
def set_data(self, data):
|
||||||
|
|
@ -54,19 +65,62 @@ class HaproxyConfigGenerator:
|
||||||
self.serving_hosts = []
|
self.serving_hosts = []
|
||||||
self.certs = {}
|
self.certs = {}
|
||||||
|
|
||||||
|
# Initialize plugin system
|
||||||
|
try:
|
||||||
|
from plugins import PluginManager
|
||||||
|
self.plugin_manager = PluginManager(
|
||||||
|
plugins_dir="/etc/haproxy/plugins",
|
||||||
|
abort_on_error=self.mapping.get("plugins", {}).get("abort_on_error", False)
|
||||||
|
)
|
||||||
|
self.plugin_manager.load_plugins()
|
||||||
|
self.plugin_manager.configure_plugins(self.mapping.get("plugins", {}))
|
||||||
|
self.global_plugin_configs = []
|
||||||
|
except Exception as e:
|
||||||
|
# If plugin system fails to initialize, log but continue
|
||||||
|
loggerEasyHaproxy.warning(f"Failed to initialize plugin system: {e}")
|
||||||
|
self.plugin_manager = None
|
||||||
|
self.global_plugin_configs = []
|
||||||
|
|
||||||
def generate(self, container_metadata={}):
|
def generate(self, container_metadata={}):
|
||||||
self.mapping.setdefault("easymapping", [])
|
self.mapping.setdefault("easymapping", [])
|
||||||
|
|
||||||
if container_metadata != {}:
|
if container_metadata != {}:
|
||||||
self.mapping["easymapping"] = self.parse(container_metadata)
|
self.mapping["easymapping"] = self.parse(container_metadata)
|
||||||
|
|
||||||
file_loader = FileSystemLoader('templates')
|
# Execute global plugins
|
||||||
|
if self.plugin_manager:
|
||||||
|
try:
|
||||||
|
from plugins import PluginContext
|
||||||
|
global_context = PluginContext(
|
||||||
|
parsed_object=container_metadata,
|
||||||
|
easymapping=self.mapping.get("easymapping", []),
|
||||||
|
container_env=self.mapping,
|
||||||
|
domain=None,
|
||||||
|
port=None,
|
||||||
|
host_config=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get enabled plugins from config
|
||||||
|
enabled_list = self.mapping.get("plugins", {}).get("enabled", [])
|
||||||
|
# If enabled list contains only empty string, treat as no plugins enabled
|
||||||
|
if enabled_list and len(enabled_list) > 0 and enabled_list[0] == "":
|
||||||
|
enabled_list = []
|
||||||
|
|
||||||
|
global_results = self.plugin_manager.execute_global_plugins(global_context, enabled_list)
|
||||||
|
# Extend instead of replace to preserve fcgi-app definitions from domain plugins
|
||||||
|
global_configs = [r.haproxy_config for r in global_results if r.haproxy_config]
|
||||||
|
self.global_plugin_configs.extend(global_configs)
|
||||||
|
except Exception as e:
|
||||||
|
loggerEasyHaproxy.warning(f"Failed to execute global plugins: {e}")
|
||||||
|
|
||||||
|
templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'templates')
|
||||||
|
file_loader = FileSystemLoader(templates_dir)
|
||||||
env = Environment(loader=file_loader)
|
env = Environment(loader=file_loader)
|
||||||
env.trim_blocks = True
|
env.trim_blocks = True
|
||||||
env.lstrip_blocks = True
|
env.lstrip_blocks = True
|
||||||
env.rstrip_blocks = True
|
env.rstrip_blocks = True
|
||||||
template = env.get_template('haproxy.cfg.j2')
|
template = env.get_template('haproxy.cfg.j2')
|
||||||
return template.render(data=self.mapping)
|
return template.render(data=self.mapping, global_plugin_configs=self.global_plugin_configs)
|
||||||
|
|
||||||
def parse(self, container_metadata):
|
def parse(self, container_metadata):
|
||||||
easymapping = dict()
|
easymapping = dict()
|
||||||
|
|
@ -131,13 +185,33 @@ class HaproxyConfigGenerator:
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Protocol for backend server communication (e.g., fcgi, h2)
|
||||||
|
proto = self.label.get(
|
||||||
|
self.label.create([definition, "proto"]),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unix socket path (alternative to host:port)
|
||||||
|
socket_path = self.label.get(
|
||||||
|
self.label.create([definition, "socket"]),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
for hostname in sorted(d[host_label].split(",")):
|
for hostname in sorted(d[host_label].split(",")):
|
||||||
hostname = hostname.strip()
|
hostname = hostname.strip()
|
||||||
self.serving_hosts.append("%s:%s" % (hostname, port))
|
self.serving_hosts.append("%s:%s" % (hostname, port))
|
||||||
easymapping[port]["hosts"].setdefault(hostname, {})
|
easymapping[port]["hosts"].setdefault(hostname, {})
|
||||||
easymapping[port]["hosts"][hostname].setdefault("containers", [])
|
easymapping[port]["hosts"][hostname].setdefault("containers", [])
|
||||||
easymapping[port]["hosts"][hostname].setdefault("certbot", False)
|
easymapping[port]["hosts"][hostname].setdefault("certbot", False)
|
||||||
easymapping[port]["hosts"][hostname]["containers"] += ["{}:{}".format(container, ct_port)]
|
easymapping[port]["hosts"][hostname].setdefault("proto", proto)
|
||||||
|
|
||||||
|
# Determine server address: Unix socket or TCP host:port
|
||||||
|
if socket_path:
|
||||||
|
server_address = socket_path
|
||||||
|
else:
|
||||||
|
server_address = "{}:{}".format(container, ct_port)
|
||||||
|
|
||||||
|
easymapping[port]["hosts"][hostname]["containers"] += [server_address]
|
||||||
easymapping[port]["hosts"][hostname]["certbot"] = certbot
|
easymapping[port]["hosts"][hostname]["certbot"] = certbot
|
||||||
easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool(
|
easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool(
|
||||||
self.label.create([definition, "redirect_ssl"])
|
self.label.create([definition, "redirect_ssl"])
|
||||||
|
|
@ -151,6 +225,68 @@ class HaproxyConfigGenerator:
|
||||||
self.label.create([definition, "redirect"])
|
self.label.create([definition, "redirect"])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Execute domain plugins for this host
|
||||||
|
if self.plugin_manager:
|
||||||
|
try:
|
||||||
|
from plugins import PluginContext
|
||||||
|
|
||||||
|
domain_context = PluginContext(
|
||||||
|
parsed_object=container_metadata,
|
||||||
|
easymapping=easymapping,
|
||||||
|
container_env=self.mapping,
|
||||||
|
domain=hostname,
|
||||||
|
port=port,
|
||||||
|
host_config=easymapping[port]["hosts"][hostname]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if plugins are enabled for this domain (from labels)
|
||||||
|
enabled_plugins = []
|
||||||
|
if self.label.has_label(self.label.create([definition, "plugins"])):
|
||||||
|
enabled_plugins = self.label.get(
|
||||||
|
self.label.create([definition, "plugins"]),
|
||||||
|
""
|
||||||
|
).split(",")
|
||||||
|
enabled_plugins = [p.strip() for p in enabled_plugins if p.strip()]
|
||||||
|
|
||||||
|
# Extract plugin configurations from labels
|
||||||
|
# Format: easyhaproxy.http.plugin.PLUGIN_NAME.CONFIG_KEY
|
||||||
|
plugin_configs = {}
|
||||||
|
for plugin_name in enabled_plugins:
|
||||||
|
plugin_configs[plugin_name] = {}
|
||||||
|
# Look for all labels matching easyhaproxy.{definition}.plugin.{plugin_name}.*
|
||||||
|
plugin_label_prefix = self.label.create([definition, "plugin", plugin_name])
|
||||||
|
for label_key in d.keys():
|
||||||
|
if label_key.startswith(plugin_label_prefix + "."):
|
||||||
|
# Extract config key (everything after plugin_label_prefix + ".")
|
||||||
|
config_key = label_key[len(plugin_label_prefix) + 1:]
|
||||||
|
plugin_configs[plugin_name][config_key] = d[label_key]
|
||||||
|
|
||||||
|
# Configure plugins with label-specific configs before execution
|
||||||
|
for plugin_name, config in plugin_configs.items():
|
||||||
|
if plugin_name in self.plugin_manager.plugins:
|
||||||
|
self.plugin_manager.plugins[plugin_name].configure(config)
|
||||||
|
|
||||||
|
domain_results = self.plugin_manager.execute_domain_plugins(
|
||||||
|
domain_context,
|
||||||
|
enabled_list=enabled_plugins
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store domain plugin configs for this host
|
||||||
|
easymapping[port]["hosts"][hostname]["plugin_configs"] = [
|
||||||
|
r.haproxy_config for r in domain_results if r.haproxy_config
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extract fcgi-app definitions from metadata and add to global configs
|
||||||
|
for result in domain_results:
|
||||||
|
if result.metadata and "fcgi_app_definition" in result.metadata:
|
||||||
|
if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs:
|
||||||
|
self.global_plugin_configs.append(result.metadata["fcgi_app_definition"])
|
||||||
|
except Exception as e:
|
||||||
|
loggerEasyHaproxy.warning(f"Failed to execute domain plugins for {hostname}: {e}")
|
||||||
|
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
||||||
|
else:
|
||||||
|
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
||||||
|
|
||||||
if certbot or clone_to_ssl:
|
if certbot or clone_to_ssl:
|
||||||
if "443" not in easymapping:
|
if "443" not in easymapping:
|
||||||
easymapping["443"] = {
|
easymapping["443"] = {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,23 @@ class ContainerEnv:
|
||||||
|
|
||||||
os.environ['EASYHAPROXY_CERTBOT_SERVER'] = env_vars["certbot"]["server"]
|
os.environ['EASYHAPROXY_CERTBOT_SERVER'] = env_vars["certbot"]["server"]
|
||||||
|
|
||||||
|
# Plugin configuration
|
||||||
|
env_vars["plugins"] = {
|
||||||
|
"abort_on_error": os.getenv("EASYHAPROXY_PLUGINS_ABORT_ON_ERROR", "false").lower() == "true",
|
||||||
|
"enabled": os.getenv("EASYHAPROXY_PLUGINS_ENABLED", "").split(",") if os.getenv("EASYHAPROXY_PLUGINS_ENABLED") else [],
|
||||||
|
"config": {} # Individual plugin configs from env vars
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse individual plugin configs (e.g., EASYHAPROXY_PLUGIN_CLOUDFLARE_*)
|
||||||
|
for key, value in os.environ.items():
|
||||||
|
if key.startswith("EASYHAPROXY_PLUGIN_"):
|
||||||
|
parts = key.split("_", 3) # ['EASYHAPROXY', 'PLUGIN', 'NAME', 'KEY']
|
||||||
|
if len(parts) >= 4:
|
||||||
|
plugin_name = parts[2].lower()
|
||||||
|
config_key = "_".join(parts[3:]).lower()
|
||||||
|
env_vars["plugins"]["config"].setdefault(plugin_name, {})
|
||||||
|
env_vars["plugins"]["config"][plugin_name][config_key] = value
|
||||||
|
|
||||||
return env_vars
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
|
|
||||||
from deepdiff import DeepDiff
|
from deepdiff import DeepDiff
|
||||||
|
|
||||||
|
|
|
||||||
250
src/plugins/__init__.py
Normal file
250
src/plugins/__init__.py
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
import os
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
|
class PluginType(Enum):
|
||||||
|
"""Plugin execution types"""
|
||||||
|
GLOBAL = "global" # Execute once per discovery cycle
|
||||||
|
DOMAIN = "domain" # Execute per domain/host
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PluginContext:
|
||||||
|
"""Container for all plugin execution data"""
|
||||||
|
parsed_object: dict # {IP: labels} from discovery
|
||||||
|
easymapping: list # Current HAProxy mapping structure
|
||||||
|
container_env: dict # Environment configuration
|
||||||
|
domain: Optional[str] = None # Domain name (for DOMAIN plugins)
|
||||||
|
port: Optional[str] = None # Port (for DOMAIN plugins)
|
||||||
|
host_config: Optional[dict] = None # Domain-specific config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PluginResult:
|
||||||
|
"""Plugin execution result"""
|
||||||
|
haproxy_config: str = "" # HAProxy config snippet to inject
|
||||||
|
modified_easymapping: Optional[list] = None # Modified easymapping structure
|
||||||
|
metadata: Dict[str, Any] = field(default_factory=dict) # Plugin metadata for logging
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInterface(ABC):
|
||||||
|
"""Base class all plugins must inherit"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the unique plugin name"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
"""Return the plugin type (GLOBAL or DOMAIN)"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin with settings from YAML/env/labels
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with plugin-specific configuration
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Process the plugin logic and return result
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: PluginContext with all necessary data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy config snippets and/or modified data
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManager:
|
||||||
|
"""Manages plugin loading, configuration, and execution"""
|
||||||
|
|
||||||
|
def __init__(self, plugins_dir: str = "/etc/haproxy/plugins", abort_on_error: bool = False):
|
||||||
|
"""
|
||||||
|
Initialize the plugin manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugins_dir: Directory containing plugin files
|
||||||
|
abort_on_error: If True, abort on plugin errors; if False, log and continue
|
||||||
|
"""
|
||||||
|
self.plugins_dir = plugins_dir
|
||||||
|
self.abort_on_error = abort_on_error
|
||||||
|
self.plugins: Dict[str, PluginInterface] = {}
|
||||||
|
self.global_plugins: List[PluginInterface] = []
|
||||||
|
self.domain_plugins: List[PluginInterface] = []
|
||||||
|
self.logger = loggerEasyHaproxy
|
||||||
|
|
||||||
|
def load_plugins(self) -> None:
|
||||||
|
"""
|
||||||
|
Discover and load plugins from the plugins directory
|
||||||
|
Loads both builtin plugins and external plugins
|
||||||
|
"""
|
||||||
|
# Load builtin plugins first
|
||||||
|
builtin_dir = os.path.join(os.path.dirname(__file__), "builtin")
|
||||||
|
self._load_plugins_from_directory(builtin_dir, "builtin")
|
||||||
|
|
||||||
|
# Load external plugins from /etc/haproxy/plugins
|
||||||
|
if os.path.exists(self.plugins_dir):
|
||||||
|
self._load_plugins_from_directory(self.plugins_dir, "external")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"Plugin directory {self.plugins_dir} does not exist, skipping external plugins")
|
||||||
|
|
||||||
|
def _load_plugins_from_directory(self, directory: str, source: str) -> None:
|
||||||
|
"""
|
||||||
|
Load plugins from a specific directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: Path to directory containing plugins
|
||||||
|
source: Source identifier ("builtin" or "external")
|
||||||
|
"""
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
return
|
||||||
|
|
||||||
|
for filename in os.listdir(directory):
|
||||||
|
if filename.endswith(".py") and not filename.startswith("__"):
|
||||||
|
filepath = os.path.join(directory, filename)
|
||||||
|
module_name = f"plugins.{source}.{filename[:-3]}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load module from file
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, filepath)
|
||||||
|
if spec and spec.loader:
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[module_name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
# Find plugin classes in module
|
||||||
|
for item_name in dir(module):
|
||||||
|
item = getattr(module, item_name)
|
||||||
|
if (isinstance(item, type) and
|
||||||
|
issubclass(item, PluginInterface) and
|
||||||
|
item is not PluginInterface):
|
||||||
|
# Instantiate plugin
|
||||||
|
plugin = item()
|
||||||
|
self.plugins[plugin.name] = plugin
|
||||||
|
|
||||||
|
# Categorize by type
|
||||||
|
if plugin.plugin_type == PluginType.GLOBAL:
|
||||||
|
self.global_plugins.append(plugin)
|
||||||
|
elif plugin.plugin_type == PluginType.DOMAIN:
|
||||||
|
self.domain_plugins.append(plugin)
|
||||||
|
|
||||||
|
self.logger.debug(f"Loaded {source} plugin: {plugin.name} ({plugin.plugin_type.value})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_error(f"Failed to load plugin from {filepath}: {str(e)}")
|
||||||
|
|
||||||
|
def configure_plugins(self, plugins_config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure all loaded plugins with their settings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugins_config: Plugin configuration from YAML/env
|
||||||
|
Format: {"plugin_name": {"key": "value"}, ...}
|
||||||
|
"""
|
||||||
|
for plugin_name, plugin in self.plugins.items():
|
||||||
|
try:
|
||||||
|
# Get plugin-specific config
|
||||||
|
plugin_cfg = plugins_config.get(plugin_name, {})
|
||||||
|
|
||||||
|
# Also check "config" sub-key for env var configs
|
||||||
|
if "config" in plugins_config and plugin_name in plugins_config["config"]:
|
||||||
|
plugin_cfg.update(plugins_config["config"][plugin_name])
|
||||||
|
|
||||||
|
# Configure plugin
|
||||||
|
plugin.configure(plugin_cfg)
|
||||||
|
self.logger.debug(f"Configured plugin: {plugin_name} with config: {plugin_cfg}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_error(f"Failed to configure plugin '{plugin_name}': {str(e)}")
|
||||||
|
|
||||||
|
def execute_global_plugins(self, context: PluginContext, enabled_list: Optional[List[str]] = None) -> List[PluginResult]:
|
||||||
|
"""
|
||||||
|
Execute all global plugins
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: PluginContext with execution data
|
||||||
|
enabled_list: Optional list of plugin names to execute. If None, execute all.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PluginResult from each plugin
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for plugin in self.global_plugins:
|
||||||
|
# Check if plugin is in enabled list (if provided)
|
||||||
|
if enabled_list is not None and plugin.name not in enabled_list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.debug(f"Executing global plugin: {plugin.name}")
|
||||||
|
result = plugin.process(context)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if result.metadata:
|
||||||
|
self.logger.debug(f"Plugin {plugin.name} metadata: {result.metadata}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_error(f"Global plugin '{plugin.name}' failed: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def execute_domain_plugins(self, context: PluginContext, enabled_list: Optional[List[str]] = None) -> List[PluginResult]:
|
||||||
|
"""
|
||||||
|
Execute all domain plugins for a specific domain
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: PluginContext with domain-specific data
|
||||||
|
enabled_list: Optional list of plugin names to execute. If None, execute all.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PluginResult from each plugin
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for plugin in self.domain_plugins:
|
||||||
|
# Check if plugin is in enabled list (if provided)
|
||||||
|
if enabled_list is not None and plugin.name not in enabled_list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.debug(f"Executing domain plugin: {plugin.name} for domain: {context.domain}")
|
||||||
|
result = plugin.process(context)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if result.metadata:
|
||||||
|
self.logger.debug(f"Plugin {plugin.name} metadata: {result.metadata}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_error(f"Domain plugin '{plugin.name}' failed for domain '{context.domain}': {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _handle_error(self, message: str) -> None:
|
||||||
|
"""
|
||||||
|
Handle plugin errors according to abort_on_error setting
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Error message to log
|
||||||
|
"""
|
||||||
|
if self.abort_on_error:
|
||||||
|
self.logger.error(message)
|
||||||
|
raise RuntimeError(message)
|
||||||
|
else:
|
||||||
|
self.logger.warning(message)
|
||||||
1
src/plugins/builtin/__init__.py
Normal file
1
src/plugins/builtin/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Built-in plugins for EasyHAProxy
|
||||||
124
src/plugins/builtin/cleanup.py
Normal file
124
src/plugins/builtin/cleanup.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""
|
||||||
|
Cleanup Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin performs cleanup tasks during each discovery cycle.
|
||||||
|
It runs as a GLOBAL plugin (once per cycle).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- max_idle_time: Maximum idle time before cleanup in seconds (default: 300)
|
||||||
|
- cleanup_temp_files: Clean up temporary files (default: true)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
cleanup:
|
||||||
|
enabled: true
|
||||||
|
max_idle_time: 300
|
||||||
|
cleanup_temp_files: true
|
||||||
|
|
||||||
|
Example Environment Variable:
|
||||||
|
EASYHAPROXY_PLUGINS_ENABLED=cleanup
|
||||||
|
EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME=600
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
|
class CleanupPlugin(PluginInterface):
|
||||||
|
"""Plugin to perform cleanup tasks during discovery cycle"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.max_idle_time = 300 # 5 minutes
|
||||||
|
self.cleanup_temp_files = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "cleanup"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.GLOBAL
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- max_idle_time: Maximum idle time in seconds
|
||||||
|
- cleanup_temp_files: Whether to clean up temp files
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "max_idle_time" in config:
|
||||||
|
try:
|
||||||
|
self.max_idle_time = int(config["max_idle_time"])
|
||||||
|
except ValueError:
|
||||||
|
loggerEasyHaproxy.warning(f"Invalid max_idle_time value: {config['max_idle_time']}, using default")
|
||||||
|
|
||||||
|
if "cleanup_temp_files" in config:
|
||||||
|
self.cleanup_temp_files = str(config["cleanup_temp_files"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Perform cleanup tasks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with metadata about cleanup actions
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
cleanup_actions = []
|
||||||
|
|
||||||
|
# Cleanup temporary files
|
||||||
|
if self.cleanup_temp_files:
|
||||||
|
temp_dirs = ["/tmp", "/var/tmp"]
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
for temp_dir in temp_dirs:
|
||||||
|
if not os.path.exists(temp_dir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find old EasyHAProxy temp files
|
||||||
|
pattern = os.path.join(temp_dir, "easyhaproxy_*")
|
||||||
|
for filepath in glob.glob(pattern):
|
||||||
|
try:
|
||||||
|
file_age = current_time - os.path.getmtime(filepath)
|
||||||
|
if file_age > self.max_idle_time:
|
||||||
|
os.remove(filepath)
|
||||||
|
cleanup_actions.append(f"Removed old temp file: {filepath}")
|
||||||
|
loggerEasyHaproxy.debug(f"Cleanup plugin: Removed {filepath}")
|
||||||
|
except Exception as e:
|
||||||
|
loggerEasyHaproxy.warning(f"Failed to remove temp file {filepath}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
loggerEasyHaproxy.warning(f"Failed to cleanup {temp_dir}: {e}")
|
||||||
|
|
||||||
|
# Log cleanup summary
|
||||||
|
if cleanup_actions:
|
||||||
|
loggerEasyHaproxy.info(f"Cleanup plugin: Performed {len(cleanup_actions)} cleanup action(s)")
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config="", # No HAProxy config needed for cleanup
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata={
|
||||||
|
"actions_performed": len(cleanup_actions),
|
||||||
|
"actions": cleanup_actions
|
||||||
|
}
|
||||||
|
)
|
||||||
148
src/plugins/builtin/cloudflare.py
Normal file
148
src/plugins/builtin/cloudflare.py
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
"""
|
||||||
|
Cloudflare Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin restores the original visitor IP address from Cloudflare's
|
||||||
|
CF-Connecting-IP header when requests come through Cloudflare's CDN.
|
||||||
|
|
||||||
|
The plugin includes built-in Cloudflare IP ranges that are automatically
|
||||||
|
updated and written to the IP list file.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/haproxy/cloudflare_ips.lst)
|
||||||
|
- use_builtin_ips: Use built-in Cloudflare IP ranges (default: true)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
cloudflare:
|
||||||
|
enabled: true
|
||||||
|
ip_list_path: /etc/haproxy/cloudflare_ips.lst
|
||||||
|
use_builtin_ips: true
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "cloudflare"
|
||||||
|
|
||||||
|
HAProxy Config Generated:
|
||||||
|
# Cloudflare - Restore original visitor IP
|
||||||
|
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
|
||||||
|
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflarePlugin(PluginInterface):
|
||||||
|
"""Plugin to restore original visitor IP from Cloudflare"""
|
||||||
|
|
||||||
|
# Current Cloudflare IP ranges (IPv4 and IPv6)
|
||||||
|
# Source: https://www.cloudflare.com/ips/
|
||||||
|
CLOUDFLARE_IPS = [
|
||||||
|
# IPv4
|
||||||
|
"173.245.48.0/20",
|
||||||
|
"103.21.244.0/22",
|
||||||
|
"103.22.200.0/22",
|
||||||
|
"103.31.4.0/22",
|
||||||
|
"141.101.64.0/18",
|
||||||
|
"108.162.192.0/18",
|
||||||
|
"190.93.240.0/20",
|
||||||
|
"188.114.96.0/20",
|
||||||
|
"197.234.240.0/22",
|
||||||
|
"198.41.128.0/17",
|
||||||
|
"162.158.0.0/15",
|
||||||
|
"104.16.0.0/13",
|
||||||
|
"104.24.0.0/14",
|
||||||
|
"172.64.0.0/13",
|
||||||
|
"131.0.72.0/22",
|
||||||
|
# IPv6
|
||||||
|
"2400:cb00::/32",
|
||||||
|
"2606:4700::/32",
|
||||||
|
"2803:f800::/32",
|
||||||
|
"2405:b500::/32",
|
||||||
|
"2405:8100::/32",
|
||||||
|
"2a06:98c0::/29",
|
||||||
|
"2c0f:f248::/32",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ip_list_path = "/etc/haproxy/cloudflare_ips.lst"
|
||||||
|
self.enabled = True
|
||||||
|
self.use_builtin_ips = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "cloudflare"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- ip_list_path: Path to Cloudflare IP list file
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- use_builtin_ips: Use built-in Cloudflare IP ranges (default: true)
|
||||||
|
"""
|
||||||
|
if "ip_list_path" in config:
|
||||||
|
self.ip_list_path = config["ip_list_path"]
|
||||||
|
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "use_builtin_ips" in config:
|
||||||
|
self.use_builtin_ips = str(config["use_builtin_ips"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Generate HAProxy config to restore original IP from Cloudflare
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context with domain information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy configuration snippet
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Write built-in Cloudflare IPs to file if using built-in IPs
|
||||||
|
if self.use_builtin_ips:
|
||||||
|
try:
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
ip_list_dir = os.path.dirname(self.ip_list_path)
|
||||||
|
if ip_list_dir and not os.path.exists(ip_list_dir):
|
||||||
|
os.makedirs(ip_list_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Write Cloudflare IPs to file
|
||||||
|
with open(self.ip_list_path, 'w') as f:
|
||||||
|
for ip_range in self.CLOUDFLARE_IPS:
|
||||||
|
f.write(f"{ip_range}\n")
|
||||||
|
|
||||||
|
loggerEasyHaproxy.info(f"Cloudflare plugin: Written {len(self.CLOUDFLARE_IPS)} IP ranges to {self.ip_list_path}")
|
||||||
|
except Exception as e:
|
||||||
|
loggerEasyHaproxy.warning(f"Cloudflare plugin: Failed to write IP list to {self.ip_list_path}: {e}")
|
||||||
|
|
||||||
|
# Generate HAProxy config snippet
|
||||||
|
haproxy_config = f"""# Cloudflare - Restore original visitor IP
|
||||||
|
acl from_cloudflare src -f {self.ip_list_path}
|
||||||
|
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=haproxy_config,
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata={
|
||||||
|
"domain": context.domain,
|
||||||
|
"ip_list_path": self.ip_list_path,
|
||||||
|
"use_builtin_ips": self.use_builtin_ips,
|
||||||
|
"ip_count": len(self.CLOUDFLARE_IPS) if self.use_builtin_ips else None
|
||||||
|
}
|
||||||
|
)
|
||||||
106
src/plugins/builtin/deny_pages.py
Normal file
106
src/plugins/builtin/deny_pages.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
Deny Pages Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin blocks access to specific paths for a domain.
|
||||||
|
It runs as a DOMAIN plugin (once per domain).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- paths: Comma-separated list of paths to deny (e.g., "/admin,/private")
|
||||||
|
- status_code: HTTP status code to return (default: 403)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
deny_pages:
|
||||||
|
enabled: true
|
||||||
|
paths: "/admin,/private,/internal"
|
||||||
|
status_code: 403
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "deny_pages"
|
||||||
|
easyhaproxy.http.plugin.deny_pages.paths: "/admin,/private"
|
||||||
|
|
||||||
|
HAProxy Config Generated:
|
||||||
|
# Deny Pages - Block specific paths
|
||||||
|
acl denied_path path_beg /admin /private
|
||||||
|
http-request deny deny_status 403 if denied_path
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
|
||||||
|
|
||||||
|
class DenyPagesPlugin(PluginInterface):
|
||||||
|
"""Plugin to deny access to specific paths"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.paths = []
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "deny_pages"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- paths: Comma-separated list of paths to deny
|
||||||
|
- status_code: HTTP status code to return
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "paths" in config:
|
||||||
|
paths_str = str(config["paths"])
|
||||||
|
self.paths = [p.strip() for p in paths_str.split(",") if p.strip()]
|
||||||
|
|
||||||
|
if "status_code" in config:
|
||||||
|
try:
|
||||||
|
self.status_code = int(config["status_code"])
|
||||||
|
except ValueError:
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Generate HAProxy config to deny specific paths
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context with domain information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy configuration snippet
|
||||||
|
"""
|
||||||
|
if not self.enabled or not self.paths:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Create path list for ACL
|
||||||
|
paths_str = " ".join(self.paths)
|
||||||
|
|
||||||
|
# Generate HAProxy config snippet
|
||||||
|
haproxy_config = f"""# Deny Pages - Block specific paths
|
||||||
|
acl denied_path path_beg {paths_str}
|
||||||
|
http-request deny deny_status {self.status_code} if denied_path"""
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=haproxy_config,
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata={
|
||||||
|
"domain": context.domain,
|
||||||
|
"blocked_paths": self.paths,
|
||||||
|
"status_code": self.status_code
|
||||||
|
}
|
||||||
|
)
|
||||||
155
src/plugins/builtin/fastcgi.py
Normal file
155
src/plugins/builtin/fastcgi.py
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
"""
|
||||||
|
FastCGI Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin generates HAProxy fcgi-app configuration for PHP-FPM and other FastCGI applications.
|
||||||
|
It runs as a DOMAIN plugin (once per domain).
|
||||||
|
|
||||||
|
The plugin creates:
|
||||||
|
1. A top-level fcgi-app section with CGI parameter definitions
|
||||||
|
2. A use-fcgi-app directive in the backend
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- document_root: Document root path (default: /var/www/html)
|
||||||
|
- script_filename: Pattern for SCRIPT_FILENAME (default: %[path])
|
||||||
|
- index_file: Default index file (default: index.php)
|
||||||
|
- path_info: Enable PATH_INFO support (default: true)
|
||||||
|
- custom_params: Dictionary of custom FastCGI parameters (optional)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
fastcgi:
|
||||||
|
enabled: true
|
||||||
|
document_root: /var/www/html
|
||||||
|
index_file: index.php
|
||||||
|
path_info: true
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "fastcgi"
|
||||||
|
easyhaproxy.http.plugin.fastcgi.document_root: /var/www/myapp
|
||||||
|
easyhaproxy.http.plugin.fastcgi.index_file: index.php
|
||||||
|
easyhaproxy.http.plugin.fastcgi.path_info: true
|
||||||
|
|
||||||
|
Example Kubernetes Annotation:
|
||||||
|
easyhaproxy.plugins: "fastcgi"
|
||||||
|
easyhaproxy.plugin.fastcgi.document_root: /var/www/myapp
|
||||||
|
easyhaproxy.plugin.fastcgi.index_file: index.php
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
|
class FastcgiPlugin(PluginInterface):
|
||||||
|
"""Plugin to configure FastCGI parameters for PHP-FPM"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.document_root = "/var/www/html"
|
||||||
|
self.script_filename = "%[path]"
|
||||||
|
self.index_file = "index.php"
|
||||||
|
self.path_info = True
|
||||||
|
self.custom_params = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "fastcgi"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- document_root: Document root path
|
||||||
|
- script_filename: Pattern for SCRIPT_FILENAME
|
||||||
|
- index_file: Default index file
|
||||||
|
- path_info: Enable PATH_INFO support
|
||||||
|
- custom_params: Dictionary of custom FastCGI parameters
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "document_root" in config:
|
||||||
|
self.document_root = config["document_root"]
|
||||||
|
|
||||||
|
if "script_filename" in config:
|
||||||
|
self.script_filename = config["script_filename"]
|
||||||
|
|
||||||
|
if "index_file" in config:
|
||||||
|
self.index_file = config["index_file"]
|
||||||
|
|
||||||
|
if "path_info" in config:
|
||||||
|
self.path_info = str(config["path_info"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "custom_params" in config:
|
||||||
|
self.custom_params = config["custom_params"]
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Process the plugin and generate FastCGI configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy FastCGI configuration
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Generate a unique fcgi-app name based on the domain
|
||||||
|
# Replace dots and colons with underscores for valid HAProxy identifier
|
||||||
|
domain_safe = context.domain.replace(".", "_").replace(":", "_")
|
||||||
|
fcgi_app_name = f"fcgi_{domain_safe}"
|
||||||
|
|
||||||
|
# Generate the use-fcgi-app directive for the backend
|
||||||
|
backend_config = f"use-fcgi-app {fcgi_app_name}"
|
||||||
|
|
||||||
|
# Generate the fcgi-app section (to be inserted at top level)
|
||||||
|
fcgi_app_lines = [f"fcgi-app {fcgi_app_name}"]
|
||||||
|
fcgi_app_lines.append(f" docroot {self.document_root}")
|
||||||
|
fcgi_app_lines.append(f" index {self.index_file}")
|
||||||
|
|
||||||
|
# PATH_INFO support
|
||||||
|
if self.path_info:
|
||||||
|
fcgi_app_lines.append(f" path-info ^(/.+\\.php)(/.*)?$")
|
||||||
|
|
||||||
|
# Set SCRIPT_FILENAME if customized
|
||||||
|
if self.script_filename and self.script_filename != "%[path]":
|
||||||
|
fcgi_app_lines.append(f" set-param SCRIPT_FILENAME {self.script_filename}")
|
||||||
|
|
||||||
|
# Custom parameters
|
||||||
|
if self.custom_params:
|
||||||
|
for param_name, param_value in self.custom_params.items():
|
||||||
|
fcgi_app_lines.append(f" set-param {param_name.upper()} {param_value}")
|
||||||
|
|
||||||
|
fcgi_app_definition = "\n".join(fcgi_app_lines)
|
||||||
|
|
||||||
|
# Build metadata - store fcgi_app_definition to be extracted and added to global configs
|
||||||
|
metadata = {
|
||||||
|
"domain": context.domain,
|
||||||
|
"fcgi_app_name": fcgi_app_name,
|
||||||
|
"fcgi_app_definition": fcgi_app_definition, # For top-level injection
|
||||||
|
"document_root": self.document_root,
|
||||||
|
"index_file": self.index_file,
|
||||||
|
"path_info": self.path_info,
|
||||||
|
"custom_params_count": len(self.custom_params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=backend_config, # use-fcgi-app directive for the backend
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata=metadata
|
||||||
|
)
|
||||||
107
src/plugins/builtin/ip_whitelist.py
Normal file
107
src/plugins/builtin/ip_whitelist.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""
|
||||||
|
IP Whitelist Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin restricts access to a domain to only specific IP addresses or CIDR ranges.
|
||||||
|
It runs as a DOMAIN plugin (once per domain).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- allowed_ips: Comma-separated list of IPs/CIDR ranges to allow
|
||||||
|
- status_code: HTTP status code to return for blocked IPs (default: 403)
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
ip_whitelist:
|
||||||
|
enabled: true
|
||||||
|
allowed_ips: "192.168.1.0/24,10.0.0.1,172.16.0.0/16"
|
||||||
|
status_code: 403
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "ip_whitelist"
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.1"
|
||||||
|
easyhaproxy.http.plugin.ip_whitelist.status_code: 403
|
||||||
|
|
||||||
|
HAProxy Config Generated:
|
||||||
|
# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src 192.168.1.0/24 10.0.0.1
|
||||||
|
http-request deny deny_status 403 if !whitelisted_ip
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
|
||||||
|
|
||||||
|
class IpWhitelistPlugin(PluginInterface):
|
||||||
|
"""Plugin to restrict access to specific IP addresses"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.allowed_ips = []
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "ip_whitelist"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- allowed_ips: Comma-separated list of IPs/CIDR ranges
|
||||||
|
- status_code: HTTP status code to return for denied requests
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "allowed_ips" in config:
|
||||||
|
ips_str = str(config["allowed_ips"])
|
||||||
|
self.allowed_ips = [ip.strip() for ip in ips_str.split(",") if ip.strip()]
|
||||||
|
|
||||||
|
if "status_code" in config:
|
||||||
|
try:
|
||||||
|
self.status_code = int(config["status_code"])
|
||||||
|
except ValueError:
|
||||||
|
self.status_code = 403
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Generate HAProxy config to whitelist specific IPs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context with domain information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy configuration snippet
|
||||||
|
"""
|
||||||
|
if not self.enabled or not self.allowed_ips:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Create space-separated list of IPs for ACL
|
||||||
|
ips_str = " ".join(self.allowed_ips)
|
||||||
|
|
||||||
|
# Generate HAProxy config snippet
|
||||||
|
haproxy_config = f"""# IP Whitelist - Only allow specific IPs
|
||||||
|
acl whitelisted_ip src {ips_str}
|
||||||
|
http-request deny deny_status {self.status_code} if !whitelisted_ip"""
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=haproxy_config,
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata={
|
||||||
|
"domain": context.domain,
|
||||||
|
"allowed_ips": self.allowed_ips,
|
||||||
|
"status_code": self.status_code
|
||||||
|
}
|
||||||
|
)
|
||||||
282
src/plugins/builtin/jwt_validator.py
Normal file
282
src/plugins/builtin/jwt_validator.py
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
"""
|
||||||
|
JWT Validator Plugin for EasyHAProxy
|
||||||
|
|
||||||
|
This plugin validates JWT tokens using HAProxy's built-in JWT functionality.
|
||||||
|
It runs as a DOMAIN plugin (once per domain).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- enabled: Enable/disable the plugin (default: true)
|
||||||
|
- algorithm: JWT signing algorithm (default: RS256)
|
||||||
|
- issuer: Expected JWT issuer (optional, set to "none"/"null" to skip validation)
|
||||||
|
- audience: Expected JWT audience (optional, set to "none"/"null" to skip validation)
|
||||||
|
- pubkey_path: Path to public key file (required if pubkey not provided)
|
||||||
|
- pubkey: Public key content as base64-encoded string (required if pubkey_path not provided)
|
||||||
|
- paths: List of paths that require JWT validation (optional, if not set ALL domain is protected)
|
||||||
|
- only_paths: If true, only specified paths are accessible; if false (default), only specified paths require JWT validation
|
||||||
|
- allow_anonymous: If true, allows requests without Authorization header (validates JWT if present); if false (default), requires Authorization header
|
||||||
|
|
||||||
|
Path Validation Logic:
|
||||||
|
- No paths configured: ALL requests to the domain require JWT validation (default behavior)
|
||||||
|
- Paths configured + only_paths=false: Only specified paths require JWT validation, others pass through
|
||||||
|
- Paths configured + only_paths=true: Only specified paths are accessible (with JWT), all others are denied
|
||||||
|
|
||||||
|
Anonymous Access Logic:
|
||||||
|
- allow_anonymous=false (default): Requests without Authorization header are denied
|
||||||
|
- allow_anonymous=true: Requests without Authorization header are allowed, but JWTs are validated if present
|
||||||
|
|
||||||
|
Example YAML config:
|
||||||
|
plugins:
|
||||||
|
jwt_validator:
|
||||||
|
enabled: true
|
||||||
|
algorithm: RS256
|
||||||
|
issuer: https://myaccount.auth0.com/
|
||||||
|
audience: https://api.mywebsite.com
|
||||||
|
pubkey_path: /etc/haproxy/jwt_keys/pubkey.pem
|
||||||
|
paths:
|
||||||
|
- /api/admin
|
||||||
|
- /api/sensitive
|
||||||
|
only_paths: false
|
||||||
|
|
||||||
|
Example Container Label:
|
||||||
|
easyhaproxy.http.plugins: "jwt_validator"
|
||||||
|
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
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
|
||||||
|
easyhaproxy.http.plugin.jwt_validator.only_paths: true
|
||||||
|
|
||||||
|
HAProxy Config Generated:
|
||||||
|
# JWT Validator - Validate JWT tokens
|
||||||
|
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }
|
||||||
|
|
||||||
|
# Extract JWT header and payload
|
||||||
|
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
|
||||||
|
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
|
||||||
|
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
|
||||||
|
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')
|
||||||
|
|
||||||
|
# Validate JWT
|
||||||
|
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com }
|
||||||
|
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }
|
||||||
|
|
||||||
|
# Validate expiration
|
||||||
|
http-request set-var(txn.now) date()
|
||||||
|
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path for imports
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
|
class JwtValidatorPlugin(PluginInterface):
|
||||||
|
"""Plugin to validate JWT tokens"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = True
|
||||||
|
self.algorithm = "RS256"
|
||||||
|
self.issuer = None # Optional
|
||||||
|
self.audience = None # Optional
|
||||||
|
self.pubkey_path = None # Path to public key file
|
||||||
|
self.pubkey = None # Public key content (alternative to pubkey_path)
|
||||||
|
self.paths = [] # List of paths that require JWT validation
|
||||||
|
self.only_paths = False # If true, only specified paths are accessible
|
||||||
|
self.allow_anonymous = False # If true, allow requests without Authorization header
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "jwt_validator"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_type(self) -> PluginType:
|
||||||
|
return PluginType.DOMAIN
|
||||||
|
|
||||||
|
def configure(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Configure the plugin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Dictionary with configuration options
|
||||||
|
- enabled: Whether plugin is enabled
|
||||||
|
- algorithm: JWT signing algorithm (default: RS256)
|
||||||
|
- issuer: Expected JWT issuer (optional)
|
||||||
|
- audience: Expected JWT audience (optional)
|
||||||
|
- pubkey_path: Path to public key file
|
||||||
|
- pubkey: Public key content as base64-encoded string
|
||||||
|
- paths: List of paths that require JWT validation (optional)
|
||||||
|
- only_paths: If true, only specified paths are accessible (default: false)
|
||||||
|
- allow_anonymous: If true, allow requests without Authorization header (default: false)
|
||||||
|
"""
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "algorithm" in config:
|
||||||
|
self.algorithm = config["algorithm"]
|
||||||
|
|
||||||
|
# Parse issuer (optional - if not set, issuer validation is skipped)
|
||||||
|
if "issuer" in config:
|
||||||
|
issuer = str(config["issuer"]).strip()
|
||||||
|
if issuer: # Only set if not empty
|
||||||
|
self.issuer = issuer
|
||||||
|
|
||||||
|
# Parse audience (optional - if not set, audience validation is skipped)
|
||||||
|
if "audience" in config:
|
||||||
|
audience = str(config["audience"]).strip()
|
||||||
|
if audience: # Only set if not empty
|
||||||
|
self.audience = audience
|
||||||
|
|
||||||
|
# Public key configuration
|
||||||
|
if "pubkey_path" in config:
|
||||||
|
self.pubkey_path = config["pubkey_path"]
|
||||||
|
|
||||||
|
if "pubkey" in config:
|
||||||
|
# Decode from base64 (consistent with sslcert parameter)
|
||||||
|
self.pubkey = base64.b64decode(config["pubkey"]).decode('ascii')
|
||||||
|
|
||||||
|
# Path configuration
|
||||||
|
if "paths" in config:
|
||||||
|
paths_config = config["paths"]
|
||||||
|
if isinstance(paths_config, list):
|
||||||
|
self.paths = [str(p).strip() for p in paths_config if str(p).strip()]
|
||||||
|
elif isinstance(paths_config, str):
|
||||||
|
# Support comma-separated paths for container labels
|
||||||
|
self.paths = [p.strip() for p in paths_config.split(",") if p.strip()]
|
||||||
|
else:
|
||||||
|
self.paths = []
|
||||||
|
|
||||||
|
if "only_paths" in config:
|
||||||
|
self.only_paths = str(config["only_paths"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
if "allow_anonymous" in config:
|
||||||
|
self.allow_anonymous = str(config["allow_anonymous"]).lower() in ["true", "1", "yes"]
|
||||||
|
|
||||||
|
def process(self, context: PluginContext) -> PluginResult:
|
||||||
|
"""
|
||||||
|
Generate HAProxy config to validate JWT tokens
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Plugin execution context with domain information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PluginResult with HAProxy configuration snippet
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Determine public key file path
|
||||||
|
if self.pubkey_path:
|
||||||
|
pubkey_file = self.pubkey_path
|
||||||
|
elif self.pubkey:
|
||||||
|
# Generate path for pubkey based on domain
|
||||||
|
domain_safe = context.domain.replace(".", "_").replace(":", "_")
|
||||||
|
pubkey_file = f"/etc/haproxy/jwt_keys/{domain_safe}_pubkey.pem"
|
||||||
|
else:
|
||||||
|
loggerEasyHaproxy.warning(f"JWT validator plugin for {context.domain}: No pubkey or pubkey_path configured")
|
||||||
|
return PluginResult()
|
||||||
|
|
||||||
|
# Build HAProxy configuration
|
||||||
|
lines = ["# JWT Validator - Validate JWT tokens"]
|
||||||
|
|
||||||
|
# Determine path condition suffix
|
||||||
|
path_condition = ""
|
||||||
|
if self.paths:
|
||||||
|
# Define ACL for protected paths
|
||||||
|
lines.append("")
|
||||||
|
lines.append("# Define paths that require JWT validation")
|
||||||
|
for path in self.paths:
|
||||||
|
lines.append(f"acl jwt_protected_path path_beg {path}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if self.only_paths:
|
||||||
|
# Deny all paths that are not in the protected list
|
||||||
|
lines.append("# Deny access to paths not in the protected list")
|
||||||
|
lines.append("http-request deny content-type 'text/html' string 'Access denied' unless jwt_protected_path")
|
||||||
|
lines.append("")
|
||||||
|
# All remaining requests are on protected paths, no condition needed
|
||||||
|
path_condition = ""
|
||||||
|
else:
|
||||||
|
# Only validate JWT on protected paths
|
||||||
|
path_condition = " if jwt_protected_path"
|
||||||
|
|
||||||
|
# Check for Authorization header
|
||||||
|
if not self.allow_anonymous:
|
||||||
|
# Require Authorization header (default behavior)
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless {{ req.hdr(authorization) -m found }}{path_condition}")
|
||||||
|
jwt_condition = path_condition
|
||||||
|
else:
|
||||||
|
# Allow anonymous access - only validate JWT if Authorization header is present
|
||||||
|
lines.append("")
|
||||||
|
lines.append("# Allow anonymous access - validate JWT only if Authorization header is present")
|
||||||
|
if path_condition:
|
||||||
|
# Combine path condition with Authorization header check
|
||||||
|
jwt_condition = f"{path_condition} if {{ req.hdr(authorization) -m found }}"
|
||||||
|
else:
|
||||||
|
jwt_condition = " if { req.hdr(authorization) -m found }"
|
||||||
|
|
||||||
|
# Extract JWT parts
|
||||||
|
lines.append("")
|
||||||
|
lines.append("# Extract JWT header and payload")
|
||||||
|
lines.append(f"http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg'){jwt_condition}")
|
||||||
|
lines.append(f"http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss'){jwt_condition}")
|
||||||
|
lines.append(f"http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud'){jwt_condition}")
|
||||||
|
lines.append(f"http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int'){jwt_condition}")
|
||||||
|
|
||||||
|
# Validate JWT
|
||||||
|
lines.append("")
|
||||||
|
lines.append("# Validate JWT")
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless {{ var(txn.alg) -m str {self.algorithm} }}{jwt_condition}")
|
||||||
|
|
||||||
|
# Validate issuer (if configured)
|
||||||
|
if self.issuer:
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless {{ var(txn.iss) -m str {self.issuer} }}{jwt_condition}")
|
||||||
|
|
||||||
|
# Validate audience (if configured)
|
||||||
|
if self.audience:
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT audience' unless {{ var(txn.aud) -m str {self.audience} }}{jwt_condition}")
|
||||||
|
|
||||||
|
# Validate signature
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'Invalid JWT signature' unless {{ http_auth_bearer,jwt_verify(txn.alg,\"{pubkey_file}\") -m int 1 }}{jwt_condition}")
|
||||||
|
|
||||||
|
# Validate expiration
|
||||||
|
lines.append("")
|
||||||
|
lines.append("# Validate expiration")
|
||||||
|
lines.append(f"http-request set-var(txn.now) date(){jwt_condition}")
|
||||||
|
lines.append(f"http-request deny content-type 'text/html' string 'JWT has expired' if {{ var(txn.exp),sub(txn.now) -m int lt 0 }}{jwt_condition}")
|
||||||
|
|
||||||
|
haproxy_config = "\n".join(lines)
|
||||||
|
|
||||||
|
# Build metadata
|
||||||
|
metadata = {
|
||||||
|
"domain": context.domain,
|
||||||
|
"algorithm": self.algorithm,
|
||||||
|
"pubkey_file": pubkey_file,
|
||||||
|
"validates_issuer": self.issuer is not None,
|
||||||
|
"validates_audience": self.audience is not None,
|
||||||
|
"path_validation": len(self.paths) > 0,
|
||||||
|
"only_paths": self.only_paths,
|
||||||
|
"allow_anonymous": self.allow_anonymous
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.issuer:
|
||||||
|
metadata["issuer"] = self.issuer
|
||||||
|
if self.audience:
|
||||||
|
metadata["audience"] = self.audience
|
||||||
|
if self.pubkey:
|
||||||
|
metadata["pubkey_content"] = self.pubkey
|
||||||
|
if self.paths:
|
||||||
|
metadata["paths"] = self.paths
|
||||||
|
|
||||||
|
return PluginResult(
|
||||||
|
haproxy_config=haproxy_config,
|
||||||
|
modified_easymapping=None,
|
||||||
|
metadata=metadata
|
||||||
|
)
|
||||||
|
|
@ -116,6 +116,30 @@ class Static(ProcessorInterface):
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
self.static_content = yaml.load(Functions.load(self.filename), Loader=yaml.FullLoader)
|
self.static_content = yaml.load(Functions.load(self.filename), Loader=yaml.FullLoader)
|
||||||
|
|
||||||
|
# Merge plugin config from YAML with env vars
|
||||||
|
if "plugins" in self.static_content:
|
||||||
|
# Get env var config
|
||||||
|
container_env = ContainerEnv.read()
|
||||||
|
|
||||||
|
# Merge YAML plugins config with env config
|
||||||
|
# YAML config takes precedence over env vars
|
||||||
|
if "plugins" not in self.static_content:
|
||||||
|
self.static_content["plugins"] = container_env.get("plugins", {})
|
||||||
|
else:
|
||||||
|
# Merge configs - YAML overrides env vars
|
||||||
|
yaml_plugins = self.static_content["plugins"]
|
||||||
|
env_plugins = container_env.get("plugins", {})
|
||||||
|
|
||||||
|
# Merge individual plugin configs
|
||||||
|
for plugin_name, plugin_config in env_plugins.get("config", {}).items():
|
||||||
|
if plugin_name not in yaml_plugins:
|
||||||
|
yaml_plugins[plugin_name] = {}
|
||||||
|
# Env vars fill in missing keys, YAML takes precedence
|
||||||
|
for key, value in plugin_config.items():
|
||||||
|
if key not in yaml_plugins[plugin_name]:
|
||||||
|
yaml_plugins[plugin_name][key] = value
|
||||||
|
|
||||||
self.cfg = HaproxyConfigGenerator(self.static_content)
|
self.cfg = HaproxyConfigGenerator(self.static_content)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -227,6 +251,13 @@ class Kubernetes(ProcessorInterface):
|
||||||
redirect = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect")
|
redirect = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect")
|
||||||
mode = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.mode")
|
mode = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.mode")
|
||||||
listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port", 80)
|
listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port", 80)
|
||||||
|
plugins = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.plugins")
|
||||||
|
|
||||||
|
# Extract plugin-specific configurations
|
||||||
|
plugin_annotations = {}
|
||||||
|
for annotation_key, annotation_value in ingress.metadata.annotations.items():
|
||||||
|
if annotation_key.startswith("easyhaproxy.plugin."):
|
||||||
|
plugin_annotations[annotation_key] = annotation_value
|
||||||
|
|
||||||
data = {"creation_timestamp": ingress.metadata.creation_timestamp.strftime("%x %X"),
|
data = {"creation_timestamp": ingress.metadata.creation_timestamp.strftime("%x %X"),
|
||||||
"resource_version": ingress.metadata.resource_version, "namespace": ingress.metadata.namespace}
|
"resource_version": ingress.metadata.resource_version, "namespace": ingress.metadata.namespace}
|
||||||
|
|
@ -273,6 +304,16 @@ class Kubernetes(ProcessorInterface):
|
||||||
rule_data["%s.mode" % definition] = mode
|
rule_data["%s.mode" % definition] = mode
|
||||||
rule_data["%s.balance" % definition] = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.balance", "roundrobin")
|
rule_data["%s.balance" % definition] = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.balance", "roundrobin")
|
||||||
|
|
||||||
|
# Add plugin configuration
|
||||||
|
if plugins is not None:
|
||||||
|
rule_data["%s.plugins" % definition] = plugins
|
||||||
|
|
||||||
|
# Add plugin-specific configurations
|
||||||
|
for plugin_key, plugin_value in plugin_annotations.items():
|
||||||
|
# Convert easyhaproxy.plugin.X.Y to easyhaproxy.{definition}.plugin.X.Y
|
||||||
|
plugin_config_key = plugin_key.replace("easyhaproxy.plugin.", "%s.plugin." % definition)
|
||||||
|
rule_data[plugin_config_key] = plugin_value
|
||||||
|
|
||||||
service_name = rule.http.paths[0].backend.service.name
|
service_name = rule.http.paths[0].backend.service.name
|
||||||
try:
|
try:
|
||||||
api_response = self.api_instance.read_namespaced_service(service_name, ingress.metadata.namespace)
|
api_response = self.api_instance.read_namespaced_service(service_name, ingress.metadata.namespace)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ defaults
|
||||||
errorfile 504 /etc/haproxy/errors-custom/504.http
|
errorfile 504 /etc/haproxy/errors-custom/504.http
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if global_plugin_configs %}
|
||||||
|
# Global Plugin Configurations
|
||||||
|
{% for config in global_plugin_configs %}
|
||||||
|
{{ config }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% set data_stats = data["stats"] | default({}) %}
|
{% set data_stats = data["stats"] | default({}) %}
|
||||||
{% if data_stats["port"] | default(1936) | int > 0 %}
|
{% if data_stats["port"] | default(1936) | int > 0 %}
|
||||||
frontend stats
|
frontend stats
|
||||||
|
|
@ -75,6 +82,12 @@ frontend {{ mode }}_in_{{ o["port"] }}
|
||||||
backend srv_{{ host }}
|
backend srv_{{ host }}
|
||||||
balance {{ o["balance"] | default("roundrobin") }}
|
balance {{ o["balance"] | default("roundrobin") }}
|
||||||
mode {{ mode }}
|
mode {{ mode }}
|
||||||
|
{% if o["hosts"][k]["plugin_configs"] is defined and o["hosts"][k]["plugin_configs"] | length > 0 %}
|
||||||
|
# Domain Plugin Configurations for {{ k }}
|
||||||
|
{% for config in o["hosts"][k]["plugin_configs"] %}
|
||||||
|
{{ config | indent(4, first=True) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% if mode == "http" %}
|
{% if mode == "http" %}
|
||||||
option forwardfor
|
option forwardfor
|
||||||
http-request set-header X-Forwarded-Port %[dst_port]
|
http-request set-header X-Forwarded-Port %[dst_port]
|
||||||
|
|
@ -84,7 +97,7 @@ backend srv_{{ host }}
|
||||||
tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }}
|
tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for c in o["hosts"][k]["containers"] %}
|
{% for c in o["hosts"][k]["containers"] %}
|
||||||
server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }}
|
server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }}{{ " proto " + o["hosts"][k]["proto"] if o["hosts"][k].get("proto") }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ defaults
|
||||||
timeout client 10s
|
timeout client 10s
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:1936
|
bind *:1936
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
backend certbot_backend
|
backend certbot_backend
|
||||||
mode http
|
mode http
|
||||||
server certbot 127.0.0.1:2080
|
server certbot 127.0.0.1:2080
|
||||||
|
|
|
||||||
56
src/tests/expected/services-fcgi.txt
Normal file
56
src/tests/expected/services-fcgi.txt
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
global
|
||||||
|
log stdout format raw local0 info
|
||||||
|
maxconn 2000
|
||||||
|
tune.ssl.default-dh-param 2048
|
||||||
|
|
||||||
|
# intermediate configuration
|
||||||
|
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||||
|
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||||
|
|
||||||
|
ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||||
|
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
|
||||||
|
|
||||||
|
ssl-dh-param-file /etc/haproxy/dhparam
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
option httplog
|
||||||
|
|
||||||
|
timeout connect 3s
|
||||||
|
timeout client 10s
|
||||||
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
frontend http_in_80
|
||||||
|
bind *:80
|
||||||
|
mode http
|
||||||
|
|
||||||
|
acl is_rule_phpapp_local_80_1 hdr(host) -i phpapp.local
|
||||||
|
acl is_rule_phpapp_local_80_2 hdr(host) -i phpapp.local:80
|
||||||
|
use_backend srv_phpapp_local_80 if is_rule_phpapp_local_80_1 OR is_rule_phpapp_local_80_2
|
||||||
|
|
||||||
|
acl is_rule_phpapp-tcp_local_80_1 hdr(host) -i phpapp-tcp.local
|
||||||
|
acl is_rule_phpapp-tcp_local_80_2 hdr(host) -i phpapp-tcp.local:80
|
||||||
|
use_backend srv_phpapp-tcp_local_80 if is_rule_phpapp-tcp_local_80_1 OR is_rule_phpapp-tcp_local_80_2
|
||||||
|
|
||||||
|
backend srv_phpapp_local_80
|
||||||
|
balance roundrobin
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
http-request set-header X-Forwarded-Port %[dst_port]
|
||||||
|
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi
|
||||||
|
backend srv_phpapp-tcp_local_80
|
||||||
|
balance roundrobin
|
||||||
|
mode http
|
||||||
|
option forwardfor
|
||||||
|
http-request set-header X-Forwarded-Port %[dst_port]
|
||||||
|
http-request add-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
server srv-0 172.17.0.3:9000 check weight 1 proto fcgi
|
||||||
|
|
||||||
|
backend certbot_backend
|
||||||
|
mode http
|
||||||
|
server certbot 127.0.0.1:2080
|
||||||
|
|
@ -29,6 +29,7 @@ defaults
|
||||||
errorfile 503 /etc/haproxy/errors-custom/503.http
|
errorfile 503 /etc/haproxy/errors-custom/503.http
|
||||||
errorfile 504 /etc/haproxy/errors-custom/504.http
|
errorfile 504 /etc/haproxy/errors-custom/504.http
|
||||||
|
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:1936
|
bind *:1936
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
frontend http_in_19901
|
frontend http_in_19901
|
||||||
bind *:19901
|
bind *:19901
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ defaults
|
||||||
errorfile 503 /etc/haproxy/errors-custom/503.http
|
errorfile 503 /etc/haproxy/errors-custom/503.http
|
||||||
errorfile 504 /etc/haproxy/errors-custom/504.http
|
errorfile 504 /etc/haproxy/errors-custom/504.http
|
||||||
|
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:1937
|
bind *:1937
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
frontend http_in_80
|
frontend http_in_80
|
||||||
bind *:80
|
bind *:80
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
frontend tcp_in_31339
|
frontend tcp_in_31339
|
||||||
bind *:31339
|
bind *:31339
|
||||||
mode tcp
|
mode tcp
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
frontend tcp_in_31339
|
frontend tcp_in_31339
|
||||||
bind *:31339
|
bind *:31339
|
||||||
mode tcp
|
mode tcp
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ defaults
|
||||||
timeout client 10s
|
timeout client 10s
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:1936
|
bind *:1936
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ defaults
|
||||||
timeout server 10m
|
timeout server 10m
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
backend certbot_backend
|
backend certbot_backend
|
||||||
mode http
|
mode http
|
||||||
server certbot 127.0.0.1:2080
|
server certbot 127.0.0.1:2080
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ defaults
|
||||||
errorfile 503 /etc/haproxy/errors-custom/503.http
|
errorfile 503 /etc/haproxy/errors-custom/503.http
|
||||||
errorfile 504 /etc/haproxy/errors-custom/504.http
|
errorfile 504 /etc/haproxy/errors-custom/504.http
|
||||||
|
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:1936
|
bind *:1936
|
||||||
mode http
|
mode http
|
||||||
|
|
|
||||||
16
src/tests/fixtures/services-fcgi
vendored
Normal file
16
src/tests/fixtures/services-fcgi
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"172.17.0.2": {
|
||||||
|
"easyhaproxy.definitions": "fcgi",
|
||||||
|
"easyhaproxy.fcgi.host": "phpapp.local",
|
||||||
|
"easyhaproxy.fcgi.port": "80",
|
||||||
|
"easyhaproxy.fcgi.socket": "/run/php/php-fpm.sock",
|
||||||
|
"easyhaproxy.fcgi.proto": "fcgi"
|
||||||
|
},
|
||||||
|
"172.17.0.3": {
|
||||||
|
"easyhaproxy.definitions": "fcgi-tcp",
|
||||||
|
"easyhaproxy.fcgi-tcp.host": "phpapp-tcp.local",
|
||||||
|
"easyhaproxy.fcgi-tcp.port": "80",
|
||||||
|
"easyhaproxy.fcgi-tcp.localport": "9000",
|
||||||
|
"easyhaproxy.fcgi-tcp.proto": "fcgi"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/tests/fixtures/services-with-cloudflare
vendored
Normal file
8
src/tests/fixtures/services-with-cloudflare
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"192.168.1.10": {
|
||||||
|
"easyhaproxy.http.host": "example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "cloudflare"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/tests/fixtures/services-with-deny-pages
vendored
Normal file
10
src/tests/fixtures/services-with-deny-pages
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"192.168.1.20": {
|
||||||
|
"easyhaproxy.http.host": "secure.example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "deny_pages",
|
||||||
|
"easyhaproxy.http.plugin.deny_pages.paths": "/admin,/wp-admin",
|
||||||
|
"easyhaproxy.http.plugin.deny_pages.status_code": "404"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
10
src/tests/fixtures/services-with-ip-whitelist
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"192.168.1.40": {
|
||||||
|
"easyhaproxy.http.host": "secure.example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "ip_whitelist",
|
||||||
|
"easyhaproxy.http.plugin.ip_whitelist.allowed_ips": "192.168.1.0/24,10.0.0.5",
|
||||||
|
"easyhaproxy.http.plugin.ip_whitelist.status_code": "403"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/tests/fixtures/services-with-jwt-validator
vendored
Normal file
12
src/tests/fixtures/services-with-jwt-validator
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"192.168.1.50": {
|
||||||
|
"easyhaproxy.http.host": "api.example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "jwt_validator",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/tests/fixtures/services-with-multiple-plugins
vendored
Normal file
11
src/tests/fixtures/services-with-multiple-plugins
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"192.168.1.30": {
|
||||||
|
"easyhaproxy.http.host": "multi.example.com",
|
||||||
|
"easyhaproxy.http.port": "80",
|
||||||
|
"easyhaproxy.http.localport": "8080",
|
||||||
|
"easyhaproxy.http.plugins": "cloudflare,deny_pages",
|
||||||
|
"easyhaproxy.http.plugin.cloudflare.ip_list_path": "/etc/haproxy/cloudflare_ips.lst",
|
||||||
|
"easyhaproxy.http.plugin.deny_pages.paths": "/admin,/private",
|
||||||
|
"easyhaproxy.http.plugin.deny_pages.status_code": "403"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,12 @@ def test_container_env_empty():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
|
|
||||||
# os.environ['CERTBOT_LOG_LEVEL'] = 'warn'
|
# os.environ['CERTBOT_LOG_LEVEL'] = 'warn'
|
||||||
|
|
@ -45,7 +50,12 @@ def test_container_env_customerrors():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
del os.environ['HAPROXY_CUSTOMERRORS']
|
del os.environ['HAPROXY_CUSTOMERRORS']
|
||||||
|
|
@ -70,7 +80,12 @@ def test_container_env_sslmode():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
del os.environ['EASYHAPROXY_SSL_MODE']
|
del os.environ['EASYHAPROXY_SSL_MODE']
|
||||||
|
|
@ -96,7 +111,12 @@ def test_container_env_stats():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
del os.environ['HAPROXY_USERNAME']
|
del os.environ['HAPROXY_USERNAME']
|
||||||
|
|
@ -128,7 +148,12 @@ def test_container_env_stats_password():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
del os.environ['HAPROXY_PASSWORD']
|
del os.environ['HAPROXY_PASSWORD']
|
||||||
|
|
@ -160,7 +185,12 @@ def test_container_env_stats_password_2():
|
||||||
"server": False,
|
"server": False,
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False}
|
"manual_auth_hook": False},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
del os.environ['HAPROXY_USERNAME']
|
del os.environ['HAPROXY_USERNAME']
|
||||||
|
|
@ -189,6 +219,11 @@ def test_container_env_certbot_email():
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False
|
"manual_auth_hook": False
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
}
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -222,6 +257,11 @@ def test_container_env_certbot_full():
|
||||||
'retry_count': 10,
|
'retry_count': 10,
|
||||||
"preferred_challenges": "dns",
|
"preferred_challenges": "dns",
|
||||||
"manual_auth_hook": "something_manual_auth_hook"
|
"manual_auth_hook": "something_manual_auth_hook"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
}
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -257,6 +297,11 @@ def test_container_log_level():
|
||||||
"retry_count": 60,
|
"retry_count": 60,
|
||||||
"preferred_challenges": "http",
|
"preferred_challenges": "http",
|
||||||
"manual_auth_hook": False
|
"manual_auth_hook": False
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"abort_on_error": False,
|
||||||
|
"config": {},
|
||||||
|
"enabled": []
|
||||||
}
|
}
|
||||||
} == ContainerEnv.read()
|
} == ContainerEnv.read()
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,9 @@ def test_parser_finds_services_raw():
|
||||||
"my-stack_agent:9001"
|
"my-stack_agent:9001"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirect": {
|
"redirect": {
|
||||||
|
|
@ -142,7 +144,9 @@ def test_parser_finds_services_raw():
|
||||||
"my-stack_cadvisor:8080"
|
"my-stack_cadvisor:8080"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
},
|
},
|
||||||
"node-exporter.quantum.example.org":{
|
"node-exporter.quantum.example.org":{
|
||||||
"balance": "roundrobin",
|
"balance": "roundrobin",
|
||||||
|
|
@ -150,7 +154,9 @@ def test_parser_finds_services_raw():
|
||||||
"my-stack_node-exporter:9100"
|
"my-stack_node-exporter:9100"
|
||||||
],
|
],
|
||||||
"certbot": True,
|
"certbot": True,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirect": {
|
"redirect": {
|
||||||
|
|
@ -168,7 +174,9 @@ def test_parser_finds_services_raw():
|
||||||
"my-stack_node-exporter:9100"
|
"my-stack_node-exporter:9100"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
},
|
},
|
||||||
"www.somehost.com.br":{
|
"www.somehost.com.br":{
|
||||||
"balance": "roundrobin",
|
"balance": "roundrobin",
|
||||||
|
|
@ -176,7 +184,9 @@ def test_parser_finds_services_raw():
|
||||||
"some-service:80"
|
"some-service:80"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirect": {
|
"redirect": {
|
||||||
|
|
@ -199,7 +209,9 @@ def test_parser_finds_services_raw():
|
||||||
"some-service:80"
|
"some-service:80"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirect": {
|
"redirect": {
|
||||||
|
|
@ -465,7 +477,9 @@ def test_parser_finds_services_clone_to_ssl_raw():
|
||||||
"10.152.183.215:8080"
|
"10.152.183.215:8080"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
},
|
},
|
||||||
"valida.me":{
|
"valida.me":{
|
||||||
"balance":"roundrobin",
|
"balance":"roundrobin",
|
||||||
|
|
@ -473,7 +487,9 @@ def test_parser_finds_services_clone_to_ssl_raw():
|
||||||
"10.152.183.62:8080"
|
"10.152.183.62:8080"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
},
|
},
|
||||||
"www.valida.me":{
|
"www.valida.me":{
|
||||||
"balance":"roundrobin",
|
"balance":"roundrobin",
|
||||||
|
|
@ -481,7 +497,9 @@ def test_parser_finds_services_clone_to_ssl_raw():
|
||||||
"10.152.183.62:8080"
|
"10.152.183.62:8080"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mode": "http",
|
"mode": "http",
|
||||||
|
|
@ -499,7 +517,9 @@ def test_parser_finds_services_clone_to_ssl_raw():
|
||||||
"10.152.183.215:8080"
|
"10.152.183.215:8080"
|
||||||
],
|
],
|
||||||
"certbot": False,
|
"certbot": False,
|
||||||
"redirect_ssl": False
|
"proto": "",
|
||||||
|
"redirect_ssl": False,
|
||||||
|
"plugin_configs": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mode": "http",
|
"mode": "http",
|
||||||
|
|
@ -515,6 +535,37 @@ def test_parser_finds_services_clone_to_ssl_raw():
|
||||||
assert parsed_object == processed
|
assert parsed_object == processed
|
||||||
assert [] == cfg.certbot_hosts
|
assert [] == cfg.certbot_hosts
|
||||||
|
|
||||||
|
def test_parser_fcgi():
|
||||||
|
"""Test FastCGI support with proto and socket parameters"""
|
||||||
|
line_list = load_fixture("services-fcgi")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"customerrors": False,
|
||||||
|
"stats": {
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = easymapping.HaproxyConfigGenerator(result)
|
||||||
|
haproxy_config = cfg.generate(line_list)
|
||||||
|
|
||||||
|
assert len(haproxy_config) > 0
|
||||||
|
|
||||||
|
# Verify proto fcgi is in the output
|
||||||
|
assert "proto fcgi" in haproxy_config
|
||||||
|
|
||||||
|
# Verify Unix socket path is used
|
||||||
|
assert "/run/php/php-fpm.sock" in haproxy_config
|
||||||
|
|
||||||
|
# Verify TCP connection is also present
|
||||||
|
assert "172.17.0.3:9000" in haproxy_config
|
||||||
|
|
||||||
|
path = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
with open(path + "/expected/services-fcgi.txt", 'r') as expected_file:
|
||||||
|
assert expected_file.read() == haproxy_config
|
||||||
|
assert [] == cfg.certbot_hosts
|
||||||
|
|
||||||
|
|
||||||
# test_parser_finds_services_raw()
|
# test_parser_finds_services_raw()
|
||||||
# test_parser_tcp()
|
# test_parser_tcp()
|
||||||
# test_parser_multiple_hosts()
|
# test_parser_multiple_hosts()
|
||||||
|
|
|
||||||
1078
src/tests/test_plugins.py
Normal file
1078
src/tests/test_plugins.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue