Documentation Refactor
This commit is contained in:
parent
3b8818e636
commit
a410b34521
47 changed files with 2065 additions and 3911 deletions
4
docs/guides/_category_.json
Normal file
4
docs/guides/_category_.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"label": "Guides",
|
||||
"position": 2
|
||||
}
|
||||
268
docs/guides/acme.md
Normal file
268
docs/guides/acme.md
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
sidebar_label: "ACME / Let's Encrypt"
|
||||
---
|
||||
|
||||
# SSL - Automatic Certificate Management Environment (ACME)
|
||||
|
||||
The Automatic Certificate Management Environment (ACME) protocol
|
||||
allow automating interactions between certificate authorities and their users' servers,
|
||||
allowing the automated deployment of public key infrastructure.
|
||||
|
||||
Most of the issuers offers Automatic Issuing free of cost.
|
||||
|
||||
## Supported ACME Challenge Methods
|
||||
|
||||
Easy HAProxy supports the following ACME challenge types:
|
||||
|
||||
- **HTTP-01 Challenge (Default and Only)**
|
||||
The ACME server validates ownership by making an HTTP request to a temporary endpoint served on port 80. Easy HAProxy provisions a standalone Certbot responder on an internal port and routes `/.well-known/acme-challenge/` traffic to it.
|
||||
|
||||
:::info Challenge Support
|
||||
- **HTTP-01**: Fully supported (default)
|
||||
- **TLS-ALPN-01**: Not supported natively by Easy HAProxy
|
||||
- **DNS-01**: Not supported natively. If you need DNS-01 for wildcard certificates, obtain certificates externally and mount them via `sslcert` as static certificates.
|
||||
:::
|
||||
|
||||
## How ACME works with Easy HAProxy
|
||||
|
||||
At a high level, ACME with Easy HAProxy works in two stages:
|
||||
|
||||
1. Global ACME/Certbot setup (one-time per EasyHAProxy instance)
|
||||
- Choose your Certificate Authority (CA) either by:
|
||||
- Using AUTOCONFIG with `EASYHAPROXY_CERTBOT_AUTOCONFIG` (e.g., zerossl, letsencrypt_test, google, etc.), or
|
||||
- Manually setting `EASYHAPROXY_CERTBOT_SERVER` (and `EASYHAPROXY_CERTBOT_EAB_KID` / `EASYHAPROXY_CERTBOT_EAB_HMAC_KEY` when your CA requires EAB).
|
||||
- Always set your contact email via `EASYHAPROXY_CERTBOT_EMAIL`.
|
||||
- Ensure ports 80 and 443 are publicly reachable on the EasyHAProxy host.
|
||||
- Persist the folder `/etc/easyhaproxy/certs/certbot` on a durable volume so issued/renewed certificates survive container restarts and avoid hitting CA rate limits.
|
||||
- Challenge method is HTTP-01 only; EasyHAProxy configures a standalone Certbot responder internally.
|
||||
|
||||
2. Enable ACME per domain (per service/app)
|
||||
- Add the label `easyhaproxy.<definition>.certbot=true` to the service you want a certificate for.
|
||||
- Ensure the service is exposed on HTTP port 80 from EasyHAProxy's perspective (e.g., `easyhaproxy.<definition>.port=80`). ACME HTTP-01 will not work if the front port is not 80.
|
||||
- Provide the domain via `easyhaproxy.<definition>.host=yourdomain.tld` (and additional labels per your install method).
|
||||
|
||||
What happens under the hood
|
||||
- When a labeled domain is detected and a certificate is needed, EasyHAProxy runs Certbot with `--preferred-challenges http` and a standalone responder bound to internal port 2080.
|
||||
- HAProxy temporarily routes `/.well-known/acme-challenge/` for that domain to the Certbot responder, allowing the CA to validate via HTTP-01.
|
||||
- On success, EasyHAProxy merges the issued cert and key and stores them under `/etc/easyhaproxy/certs/certbot` (one PEM per domain), then reloads HAProxy to serve HTTPS for that domain.
|
||||
- Certificates are monitored and renewed automatically before expiry.
|
||||
|
||||
Tips
|
||||
- Do not map port 443 for your backend app; EasyHAProxy will terminate TLS at the proxy once the certificate is issued.
|
||||
- If you do not set `EASYHAPROXY_CERTBOT_EMAIL`, EasyHAProxy will not request certificates.
|
||||
- DNS-01 is not supported natively; for wildcards or DNS-only environments, issue certificates externally and mount them via `sslcert` as static certificates.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
To enable the ACME protocol we need to enable Certbot in EasyHAProxy by setting up the following environment variables:
|
||||
|
||||
| Environment Variable | Required? | Description |
|
||||
|------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| EASYHAPROXY_CERTBOT_EMAIL | **YES** | Your email for the certificate authority. Required for certificate issuance. |
|
||||
| EASYHAPROXY_CERTBOT_AUTOCONFIG | **YES\*** | Pre-configured settings for your Certificate Authority (CA). See table below. **Required if CERTBOT_SERVER is not set.** |
|
||||
| EASYHAPROXY_CERTBOT_SERVER | **YES\*** | The ACME endpoint URL of your certificate authority. **Required if AUTOCONFIG is not set.** Auto-set when using AUTOCONFIG. |
|
||||
| EASYHAPROXY_CERTBOT_EAB_KID | - | External Account Binding (EAB) Key Identifier (KID) provided by your certificate authority. Some CA require it. See table below. |
|
||||
| EASYHAPROXY_CERTBOT_EAB_HMAC_KEY | - | External Account Binding (EAB) HMAC Key provided by your certificate authority. Some CA require it. See table below. |
|
||||
| EASYHAPROXY_CERTBOT_RETRY_COUNT | - | Wait 'n' requests before retrying issue invalid requests. Default 60. |
|
||||
| EASYHAPROXY_CERTBOT_PREFERRED_CHALLENGES | - | The preferred challenges for Certbot. Available: `http` |
|
||||
| EASYHAPROXY_CERTBOT_MANUAL_AUTH_HOOK | - | The path to a script that will be executed (default: None) |
|
||||
|
||||
**\*Important:** You must set **either** `EASYHAPROXY_CERTBOT_AUTOCONFIG` **or** `EASYHAPROXY_CERTBOT_SERVER` (not both). Using `AUTOCONFIG` is recommended as it automatically configures the server URL for popular certificate authorities.
|
||||
|
||||
## Auto Config Certificate Authority (CA)
|
||||
|
||||
Here are detailed instructions per Certificate Authority (CA). If anyone is missing, please let's know.
|
||||
|
||||
Possible values for: `EASYHAPROXY_CERTBOT_AUTOCONFIG`
|
||||
|
||||
| CA | Auto Config | Free? | Account Required? | EAB KID? | EAB HMAC Key? | More Info |
|
||||
|----------------------|------------------|-------|--------------------|----------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Let's Encrypt | letsencrypt | Yes | No | No | No | Default when no AUTOCONFIG is set |
|
||||
| Let's Encrypt (Test) | letsencrypt_test | Yes | No | No | No | - |
|
||||
| ZeroSSL | zerossl | Yes | No | No | No | [Link](https://zerossl.com/documentation/acme/) |
|
||||
| BuyPass | buypass | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) |
|
||||
| BuyPass (test) | buypass_test | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) |
|
||||
| Google | google | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) |
|
||||
| Google Test | google_test | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) |
|
||||
| SSLCOM RCA | sslcom_rca | Trial | EAB Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) |
|
||||
| SSLCOM ECC | sslcom_ecc | Trial | EAB Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) |
|
||||
| Digicert | - | No | Yes | Yes | Yes | [Link](https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/use-a-third-party-acme-client-for-host-automations.html) |
|
||||
| Entrust | - | No | Yes | Yes | Yes | [Link](https://www.entrust.com/knowledgebase/ssl/how-to-use-acme-to-install-ssl-tls-certificates-in-entrust-certificate-services-apache) |
|
||||
| Sectigo | - | No | Yes | Yes | Yes | [Link](https://www.sectigo.com/resource-library/sectigos-acme-automation) |
|
||||
|
||||
This configuration is global. After set up ACME properly, is necessary enable for each domain the certificate request.
|
||||
|
||||
To do that add the label: `easyhaproxy.<definition>.certbot=true`. See the method of installation you are using to learn how to set up properly.
|
||||
|
||||
## Example
|
||||
|
||||
### Setting up EasyHAProxy
|
||||
|
||||
Run the EasyHAProxy container:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
... \
|
||||
-e EASYHAPROXY_CERTBOT_AUTOCONFIG=zerossl \
|
||||
-e EASYHAPROXY_CERTBOT_EMAIL=john@doe.com \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-v /path/to/guest/certbot/certs:/etc/easyhaproxy/certs/certbot \
|
||||
... \
|
||||
byjg/easy-haproxy
|
||||
```
|
||||
|
||||
:::note Configuration Notes
|
||||
- The `EASYHAPROXY_CERTBOT_AUTOCONFIG` is not required for Let's Encrypt (it's the default). In this example, the certificate will be issued by ZeroSSL.
|
||||
- If you don't set the `EASYHAPROXY_CERTBOT_EMAIL` environment variable, EasyHAProxy will fail silently and **will not request** certificates.
|
||||
- Ports 80 and 443 must be accessible through the internet as a [Let's Encrypt requirement](https://letsencrypt.org/docs/allow-port-80/)
|
||||
:::
|
||||
|
||||
:::danger Important: Persist Certbot Certificates
|
||||
To avoid hitting rate limits and certificate issuing problems:
|
||||
- **You must persist** the container folder `/etc/easyhaproxy/certs/certbot` outside the container
|
||||
- **Never delete or modify** its contents manually
|
||||
- If you don't persist this folder, or if you delete/modify its contents, certificate issuing may not work properly and you may hit rate limits
|
||||
:::
|
||||
|
||||
If you are using Let's Encrypt, be aware of it rate limits:
|
||||
|
||||
- https://letsencrypt.org/docs/duplicate-certificate-limit/
|
||||
- https://letsencrypt.org/docs/rate-limits/
|
||||
|
||||
|
||||
## Setting up your container to use the ACME CA
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
... \
|
||||
--label easyhaproxy.express.port=80 \
|
||||
--label easyhaproxy.express.localport=3000 \
|
||||
--label easyhaproxy.express.host=example.org \
|
||||
--label easyhaproxy.express.certbot=true \
|
||||
... \
|
||||
some/myimage
|
||||
```
|
||||
|
||||
:::warning ACME Requirements
|
||||
- Your container **must** be configured to listen on port 80 (`easyhaproxy.<definition>.port=80`). The CA will not issue certificates if using another port, and EasyHAProxy will fail silently.
|
||||
- Do not set port 443 for the container when using ACME, because EasyHAProxy will create the HTTPS binding automatically once the certificate is issued.
|
||||
:::
|
||||
|
||||
## Complete Docker Compose Example
|
||||
|
||||
Here's a complete `docker-compose.yml` showing proper ACME configuration:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
easyhaproxy:
|
||||
image: byjg/easy-haproxy:6.0.0
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# REQUIRED: Persist Certbot certificates (ACME)
|
||||
- certs_certbot:/etc/easyhaproxy/certs/certbot
|
||||
# OPTIONAL: For manual certificates (see SSL documentation)
|
||||
- certs_haproxy:/etc/easyhaproxy/certs/haproxy
|
||||
environment:
|
||||
# Service discovery
|
||||
EASYHAPROXY_DISCOVER: docker
|
||||
EASYHAPROXY_LABEL_PREFIX: easyhaproxy
|
||||
|
||||
# ACME/Certbot Configuration (Method 1: Recommended)
|
||||
EASYHAPROXY_CERTBOT_EMAIL: your-email@example.com
|
||||
EASYHAPROXY_CERTBOT_AUTOCONFIG: letsencrypt
|
||||
|
||||
# Other settings
|
||||
EASYHAPROXY_SSL_MODE: "default"
|
||||
HAPROXY_CUSTOMERRORS: "true"
|
||||
HAPROXY_USERNAME: admin
|
||||
HAPROXY_PASSWORD: password
|
||||
HAPROXY_STATS_PORT: 1936
|
||||
ports:
|
||||
- "80:80/tcp"
|
||||
- "443:443/tcp"
|
||||
- "1936:1936/tcp"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "-u", "admin:password", "http://localhost:1936"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 3
|
||||
|
||||
# Example backend service with ACME enabled
|
||||
myapp:
|
||||
image: nginx:alpine
|
||||
labels:
|
||||
easyhaproxy.http.host: example.com
|
||||
easyhaproxy.http.port: 80
|
||||
easyhaproxy.http.localport: 80
|
||||
easyhaproxy.http.certbot: "true" # Enable ACME for this domain
|
||||
|
||||
volumes:
|
||||
certs_certbot:
|
||||
# This volume MUST be persisted to avoid rate limits
|
||||
certs_haproxy:
|
||||
# Optional: only needed if using manual certificates
|
||||
```
|
||||
|
||||
## Certificate Storage Paths
|
||||
|
||||
EasyHAProxy uses different paths for different certificate types:
|
||||
|
||||
| Path | Purpose | When to Mount |
|
||||
|----------------------------------|-------------------------------------|----------------------------------------------|
|
||||
| `/etc/easyhaproxy/certs/certbot` | ACME/Certbot automatic certificates | **Required** when using ACME |
|
||||
| `/etc/easyhaproxy/certs/haproxy` | Manual/custom certificates | Optional - only if using custom certificates |
|
||||
|
||||
Both volumes can be mounted simultaneously. Per-domain certificate selection:
|
||||
- If a domain has `certbot=true` label, ACME certificate is used
|
||||
- Otherwise, manual certificate from `/etc/easyhaproxy/certs/haproxy` is used (if present)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Warning: "ACME environment not ready: ACME server not configured"
|
||||
|
||||
**Cause:** You set `EASYHAPROXY_CERTBOT_EMAIL` but forgot to configure the ACME server.
|
||||
|
||||
**Solution:** Add one of these to your environment variables:
|
||||
|
||||
```yaml
|
||||
# Option 1: Use AUTOCONFIG (recommended)
|
||||
EASYHAPROXY_CERTBOT_AUTOCONFIG: letsencrypt
|
||||
|
||||
# Option 2: Set server manually
|
||||
EASYHAPROXY_CERTBOT_SERVER: https://acme-v02.api.letsencrypt.org/directory
|
||||
```
|
||||
|
||||
### Certificates Not Being Issued
|
||||
|
||||
**Common causes:**
|
||||
1. Port 80 is not publicly accessible
|
||||
2. DNS doesn't point to your server
|
||||
3. Container label missing `certbot=true`
|
||||
4. Container port is not 80 (`easyhaproxy.<definition>.port` must be 80)
|
||||
5. Rate limits hit (check `/etc/easyhaproxy/certs/certbot` volume)
|
||||
|
||||
**Debug steps:**
|
||||
```bash
|
||||
# Check EasyHAProxy logs
|
||||
docker logs easyhaproxy
|
||||
|
||||
# Check if Certbot volume is persisted
|
||||
docker volume inspect certs_certbot
|
||||
|
||||
# Verify port 80 is accessible
|
||||
curl -I http://your-domain.com/.well-known/acme-challenge/test
|
||||
```
|
||||
|
||||
### Rate Limit Errors
|
||||
|
||||
If you hit Let's Encrypt rate limits:
|
||||
- Wait for the limit window to reset (usually 1 week)
|
||||
- Use staging server for testing: `EASYHAPROXY_CERTBOT_AUTOCONFIG: letsencrypt_test`
|
||||
- Ensure `/etc/easyhaproxy/certs/certbot` volume is properly persisted
|
||||
- See: https://letsencrypt.org/docs/rate-limits/
|
||||
|
||||
----
|
||||
[Open source ByJG](http://opensource.byjg.com)
|
||||
15
docs/guides/digitalocean.md
Normal file
15
docs/guides/digitalocean.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
sidebar_position: 9
|
||||
sidebar_label: "DigitalOcean"
|
||||
---
|
||||
|
||||
# DigitalOcean
|
||||
|
||||
DigitalOcean is a cloud infrastructure provider focused on simplifying web infrastructure for software developers.
|
||||
|
||||
EasyHAProxy is available on DigitalOcean Marketplace.
|
||||
You can install it with a few clicks directly from the DigitalOcean dashboard.
|
||||
|
||||
## Installing EasyHAProxy on DigitalOcean
|
||||
|
||||
Please refer the [EasyHAProxy page on DigitalOcean Marketplace](https://marketplace.digitalocean.com/apps/easyhaproxy-ingress-controller).
|
||||
12
docs/guides/dokku.md
Normal file
12
docs/guides/dokku.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: "Dokku"
|
||||
---
|
||||
|
||||
# Dokku
|
||||
|
||||
Dokku is a Docker powered mini-Heroku in around 100 lines of Bash. It is the smallest PaaS implementation you've ever seen, weighing in at a little over 100 MB of memory usage and about 80 MB of disk space.
|
||||
|
||||
## Installing EasyHAProxy on Dokku
|
||||
|
||||
Please refer to the [official documentation](https://github.com/dokku/dokku/blob/master/docs/networking/proxies/haproxy.md).
|
||||
50
docs/guides/helm.md
Normal file
50
docs/guides/helm.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
sidebar_label: "Helm"
|
||||
---
|
||||
|
||||
# Helm 3
|
||||
|
||||
Helm is a package manager for Kubernetes. It allows you to install and manage applications on Kubernetes.
|
||||
|
||||
## Setup EasyHAProxy with Helm 3
|
||||
|
||||
### 1) Install EasyHAProxy
|
||||
|
||||
```bash title="Add the Helm repository"
|
||||
helm repo add byjg https://opensource.byjg.com/helm
|
||||
helm repo update byjg
|
||||
kubectl create namespace easyhaproxy
|
||||
```
|
||||
|
||||
```bash title="Install with Helm"
|
||||
helm upgrade --install ingress byjg/easyhaproxy \
|
||||
--namespace easyhaproxy \
|
||||
--set resources.requests.cpu=100m \
|
||||
--set resources.requests.memory=128Mi
|
||||
```
|
||||
|
||||
### 2) Choose a deployment mode
|
||||
|
||||
By default, EasyHAProxy installs as a **DaemonSet** (`service.create: false`). To use the recommended **NodePort** or **ClusterIP** modes instead, set `service.create: true`:
|
||||
|
||||
```bash title="NodePort (recommended)"
|
||||
helm upgrade --install ingress byjg/easyhaproxy \
|
||||
--namespace easyhaproxy \
|
||||
--set service.create=true \
|
||||
--set service.type=NodePort
|
||||
```
|
||||
|
||||
```bash title="ClusterIP (behind LoadBalancer)"
|
||||
helm upgrade --install ingress byjg/easyhaproxy \
|
||||
--namespace easyhaproxy \
|
||||
--set service.create=true \
|
||||
--set service.type=ClusterIP
|
||||
```
|
||||
|
||||
See [Deployment Modes](../getting-started/kubernetes.md#deployment-modes) for a comparison of all three modes.
|
||||
|
||||
For the complete list of configurable values, see the [Helm Values reference](../reference/helm.md).
|
||||
|
||||
----
|
||||
[Open source ByJG](http://opensource.byjg.com)
|
||||
81
docs/guides/microk8s.md
Normal file
81
docs/guides/microk8s.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
sidebar_position: 7
|
||||
sidebar_label: "MicroK8s"
|
||||
---
|
||||
|
||||
# Microk8s Add-ons
|
||||
|
||||
Microk8s is a lightweight Kubernetes distribution that can run on a single machine. It is very easy to install and use.
|
||||
You can add custom addons to your Microk8s installation.
|
||||
|
||||
Here are the steps to install EasyHAProxy on your Microk8s.
|
||||
|
||||
## Enabling EasyHAProxy on MicroK8s
|
||||
|
||||
EasyHAProxy is being part of official MicroK8s Community edition since MicroK8s version 1.27.
|
||||
|
||||
Just enable the community add-on:
|
||||
|
||||
```bash title="Enable community addons"
|
||||
microk8s enable community
|
||||
```
|
||||
|
||||
and you'll see:
|
||||
|
||||
```
|
||||
$ microk8s status
|
||||
|
||||
microk8s is running
|
||||
...
|
||||
addons:
|
||||
...
|
||||
disabled:
|
||||
easyhaproxy # (community) EasyHAProxy can detect and configure HAProxy automatically based on ingress labels
|
||||
```
|
||||
|
||||
However, if you are using MicroK8s before 1.27 you need to enable it directly from the ByJG repository by accessing the microk8s host machine and run:
|
||||
|
||||
```bash title="Add ByJG addon repository"
|
||||
microk8s addons repo add byjg https://github.com/byjg/microk8s-addons.git
|
||||
```
|
||||
|
||||
And you should see:
|
||||
|
||||
```text
|
||||
$ microk8s status
|
||||
|
||||
microk8s is running
|
||||
...
|
||||
addons:
|
||||
...
|
||||
disabled:
|
||||
easyhaproxy # (byjg) EasyHAProxy can detect and configure HAProxy automatically based on ingress labels
|
||||
....
|
||||
```
|
||||
|
||||
## Installing EasyHAProxy addon
|
||||
|
||||
Once you have enable the EasyHAProxy from the community repository or from ByJG repository and can enable it by running:
|
||||
|
||||
Usage:
|
||||
|
||||
Install as a Daemonset:
|
||||
|
||||
```bash title="Install as DaemonSet"
|
||||
microk8s enable easyhaproxy
|
||||
```
|
||||
|
||||
Install as a NodePort:
|
||||
|
||||
```bash title="Install as NodePort"
|
||||
microk8s enable easyhaproxy --nodeport
|
||||
```
|
||||
|
||||
:::warning Disable Other Ingress Controllers
|
||||
You need to disable any ingress controller you have previously installed (e.g., nginx, traefik, etc.) before installing EasyHAProxy to avoid conflicts.
|
||||
:::
|
||||
|
||||
For more parameters you can refer to the [Kubernetes](../getting-started/kubernetes.md) page.
|
||||
|
||||
----
|
||||
[Open source ByJG](http://opensource.byjg.com)
|
||||
695
docs/guides/plugin-development.md
Normal file
695
docs/guides/plugin-development.md
Normal file
|
|
@ -0,0 +1,695 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
sidebar_label: "Plugin Development"
|
||||
---
|
||||
|
||||
# Plugin Development Guide
|
||||
|
||||
This comprehensive guide covers everything you need to know about developing plugins for EasyHAProxy. Plugins extend HAProxy configuration with custom functionality and can be integrated seamlessly with Docker, Kubernetes, and Swarm environments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Plugin Architecture](#plugin-architecture)
|
||||
3. [Quick Start Guide](#quick-start-guide)
|
||||
4. [API Reference](#api-reference)
|
||||
5. [Advanced Examples](#advanced-examples)
|
||||
6. [Best Practices](#best-practices)
|
||||
7. [Testing Guidelines](#testing-guidelines)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
9. [Distribution](#distribution)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### What is a Plugin?
|
||||
|
||||
A plugin is a Python class that implements the `PluginInterface` and extends HAProxy's configuration during the discovery cycle. Plugins can:
|
||||
|
||||
- **Inject HAProxy configuration** - Add custom HAProxy directives (ACLs, http-request rules, etc.)
|
||||
- **Modify discovery data** - Transform the easymapping structure before HAProxy config generation
|
||||
- **Perform maintenance tasks** - Execute cleanup, monitoring, or integration tasks
|
||||
- **Integrate with external services** - Connect to APIs, databases, or third-party systems
|
||||
|
||||
### Why Build a Plugin?
|
||||
|
||||
Build a plugin when you need to:
|
||||
|
||||
- Add domain-specific HAProxy configuration based on labels/annotations
|
||||
- Integrate with CDNs, load balancers, or security services
|
||||
- Implement custom authentication or authorization logic
|
||||
- Perform scheduled maintenance or monitoring tasks
|
||||
- Extend EasyHAProxy without modifying core code
|
||||
|
||||
### Plugin System Benefits
|
||||
|
||||
- **Zero code changes** - Plugins don't modify EasyHAProxy core
|
||||
- **Hot reload support** - Plugins reload on each discovery cycle
|
||||
- **Configuration flexibility** - Configure via YAML, environment variables, or container labels
|
||||
- **Error isolation** - Plugin errors don't crash the main application (configurable)
|
||||
- **Easy distribution** - Share plugins as single Python files
|
||||
|
||||
---
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
### Plugin Types
|
||||
|
||||
EasyHAProxy supports two plugin execution models:
|
||||
|
||||
#### 1. GLOBAL Plugins
|
||||
|
||||
Execute **once per discovery cycle**, regardless of discovered domains.
|
||||
|
||||
**Execution timing:** After discovery, before domain processing
|
||||
|
||||
**Use cases:**
|
||||
- Cleanup tasks (removing old temp files)
|
||||
- Global monitoring (health checks, metrics)
|
||||
- DNS updates (updating external DNS records)
|
||||
- Log rotation or archiving
|
||||
- Integration with global services
|
||||
|
||||
**Example:** CleanupPlugin - removes old temporary files once per cycle
|
||||
|
||||
#### 2. DOMAIN Plugins
|
||||
|
||||
Execute **once per discovered domain/host**.
|
||||
|
||||
**Execution timing:** During domain processing, before backend config generation
|
||||
|
||||
**Use cases:**
|
||||
- Domain-specific HAProxy rules (IP whitelisting, rate limiting)
|
||||
- CDN integration (Cloudflare IP restoration)
|
||||
- Path-based controls (blocking specific URLs)
|
||||
- Custom headers or redirects per domain
|
||||
- JWT validation or authentication
|
||||
|
||||
**Example:** CloudflarePlugin - restores visitor IP for each Cloudflare-enabled domain
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
```
|
||||
1. LOAD PHASE
|
||||
├─ PluginManager scans plugins directory
|
||||
├─ Imports plugin modules
|
||||
├─ Instantiates plugin classes
|
||||
└─ Categorizes by type (GLOBAL/DOMAIN)
|
||||
|
||||
2. CONFIGURE PHASE
|
||||
├─ Loads configuration from YAML/env
|
||||
├─ Calls plugin.configure(config) for each plugin
|
||||
└─ Validates configuration (plugin responsibility)
|
||||
|
||||
3. INITIALIZE PHASE
|
||||
├─ Calls plugin.initialize() for each plugin
|
||||
├─ Plugins request file system resources (directories, files)
|
||||
├─ PluginManager processes resource requests
|
||||
└─ Creates directories and files as needed
|
||||
|
||||
4. EXECUTION PHASE (per discovery cycle)
|
||||
├─ GLOBAL PLUGINS
|
||||
│ └─ Executes all global plugins once
|
||||
│
|
||||
└─ DOMAIN PLUGINS
|
||||
└─ For each discovered domain:
|
||||
└─ Executes all domain plugins
|
||||
|
||||
5. RESULT PROCESSING
|
||||
├─ Collects PluginResult from each plugin
|
||||
├─ Injects haproxy_config into backend sections
|
||||
├─ Injects global_configs into global section
|
||||
├─ Injects defaults_configs into defaults section
|
||||
├─ Applies modified_easymapping if provided
|
||||
└─ Logs metadata for debugging
|
||||
```
|
||||
|
||||
### Plugin Loading Order
|
||||
|
||||
1. **Builtin plugins** - Loaded from `/src/plugins/builtin/`
|
||||
2. **External plugins** - Loaded from `/etc/easyhaproxy/plugins/`
|
||||
|
||||
Plugins are discovered automatically by filename (`*.py` excluding `__*.py`).
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Container Labels/Annotations
|
||||
↓
|
||||
Discovery (Docker/K8s/Swarm)
|
||||
↓
|
||||
parsed_object: {IP: labels}
|
||||
↓
|
||||
[GLOBAL PLUGINS] ← PluginContext (parsed_object, easymapping, env)
|
||||
↓
|
||||
easymapping: [list of domain configs]
|
||||
↓
|
||||
For each domain:
|
||||
[DOMAIN PLUGINS] ← PluginContext (domain, port, host_config, ...)
|
||||
↓
|
||||
PluginResult → haproxy_config snippets
|
||||
↓
|
||||
HAProxy Configuration File
|
||||
↓
|
||||
HAProxy Reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start Guide
|
||||
|
||||
### Step 1: Create Plugin File
|
||||
|
||||
Create a new Python file in `/etc/easyhaproxy/plugins/` (or builtin location for core plugins):
|
||||
|
||||
```python
|
||||
# /etc/easyhaproxy/plugins/my_plugin.py
|
||||
|
||||
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 logger_easyhaproxy
|
||||
|
||||
|
||||
class MyPlugin(PluginInterface):
|
||||
"""My custom plugin description"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize default configuration
|
||||
self.enabled = True
|
||||
self.my_setting = "default_value"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return unique plugin name"""
|
||||
return "my_plugin"
|
||||
|
||||
@property
|
||||
def plugin_type(self) -> PluginType:
|
||||
"""Return plugin type (GLOBAL or DOMAIN)"""
|
||||
return PluginType.DOMAIN
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
"""
|
||||
Configure plugin from YAML/env/labels
|
||||
|
||||
Args:
|
||||
config: Dictionary with plugin configuration
|
||||
"""
|
||||
if "enabled" in config:
|
||||
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
|
||||
|
||||
if "my_setting" in config:
|
||||
self.my_setting = config["my_setting"]
|
||||
|
||||
def process(self, context: PluginContext) -> PluginResult:
|
||||
"""
|
||||
Process plugin logic and return result
|
||||
|
||||
Args:
|
||||
context: PluginContext with execution data
|
||||
|
||||
Returns:
|
||||
PluginResult with HAProxy config and metadata
|
||||
"""
|
||||
if not self.enabled:
|
||||
return PluginResult()
|
||||
|
||||
# Generate HAProxy configuration
|
||||
haproxy_config = f"""# My Plugin - Custom functionality
|
||||
http-request set-header X-My-Header {self.my_setting}"""
|
||||
|
||||
return PluginResult(
|
||||
haproxy_config=haproxy_config,
|
||||
metadata={
|
||||
"domain": context.domain,
|
||||
"setting_value": self.my_setting
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 2: Enable Plugin
|
||||
|
||||
**Via container label (Docker):**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
labels:
|
||||
easyhaproxy.http.host: example.com
|
||||
easyhaproxy.http.plugins: my_plugin
|
||||
easyhaproxy.http.plugin.my_plugin.my_setting: custom_value
|
||||
```
|
||||
|
||||
**Via YAML configuration:**
|
||||
|
||||
```yaml
|
||||
# /etc/easyhaproxy/static/config.yaml
|
||||
plugins:
|
||||
enabled: [my_plugin]
|
||||
config:
|
||||
my_plugin:
|
||||
enabled: true
|
||||
my_setting: custom_value
|
||||
```
|
||||
|
||||
**Via environment variable:**
|
||||
|
||||
```bash
|
||||
EASYHAPROXY_PLUGINS_ENABLED=my_plugin
|
||||
EASYHAPROXY_PLUGIN_MY_PLUGIN_MY_SETTING=custom_value
|
||||
```
|
||||
|
||||
### Step 3: Test Plugin
|
||||
|
||||
Restart EasyHAProxy and check logs:
|
||||
|
||||
```bash
|
||||
docker-compose restart haproxy
|
||||
docker-compose logs -f haproxy | grep my_plugin
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
[INFO] Loaded external plugin: my_plugin (domain)
|
||||
[DEBUG] Configured plugin: my_plugin with config: {'my_setting': 'custom_value'}
|
||||
[DEBUG] Executing domain plugin: my_plugin for domain: example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### PluginInterface
|
||||
|
||||
Base class all plugins must inherit from.
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
def initialize(self) -> InitializationResult:
|
||||
"""
|
||||
Initialize plugin resources (new in v2.0)
|
||||
|
||||
Optional method to request file system resources.
|
||||
Default implementation returns empty result (no-op).
|
||||
|
||||
Returns:
|
||||
InitializationResult with resource requests
|
||||
"""
|
||||
return InitializationResult()
|
||||
|
||||
@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
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
|
||||
- `name` - Unique identifier (used in configuration and logs)
|
||||
- `plugin_type` - Execution model (`PluginType.GLOBAL` or `PluginType.DOMAIN`)
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `configure(config)` - Receives plugin configuration during initialization
|
||||
- `initialize()` - **[New in v2.0]** Request file system resources (optional)
|
||||
- `process(context)` - Main execution logic, returns `PluginResult`
|
||||
|
||||
### PluginType
|
||||
|
||||
Enum defining plugin execution types.
|
||||
|
||||
```python
|
||||
class PluginType(Enum):
|
||||
"""Plugin execution types"""
|
||||
GLOBAL = "global" # Execute once per discovery cycle
|
||||
DOMAIN = "domain" # Execute per domain/host
|
||||
```
|
||||
|
||||
### PluginContext
|
||||
|
||||
Container for all plugin execution data.
|
||||
|
||||
```python
|
||||
@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
|
||||
```
|
||||
|
||||
### PluginResult
|
||||
|
||||
Plugin execution result containing configuration and metadata.
|
||||
|
||||
```python
|
||||
@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
|
||||
global_configs: list[str] = field(default_factory=list) # [New] Global-level configs
|
||||
defaults_configs: list[str] = field(default_factory=list) # [New] Defaults-level configs
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
- `haproxy_config` - HAProxy configuration snippet (injected into backend/frontend)
|
||||
- `modified_easymapping` - Modified easymapping structure (optional, advanced use)
|
||||
- `metadata` - Dictionary with debugging/logging information
|
||||
- `global_configs` - **[New in v2.0]** List of global-level HAProxy configs (e.g., fcgi-app definitions)
|
||||
- `defaults_configs` - **[New in v2.0]** List of defaults-level HAProxy configs (e.g., log-format)
|
||||
|
||||
### ResourceRequest
|
||||
|
||||
**[New in v2.0]** Request for file system resources during plugin initialization.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ResourceRequest:
|
||||
"""Request for file system resources"""
|
||||
resource_type: str # "directory" or "file"
|
||||
path: str
|
||||
content: str | None = None
|
||||
overwrite: bool = False
|
||||
```
|
||||
|
||||
### InitializationResult
|
||||
|
||||
**[New in v2.0]** Plugin initialization result with resource requests.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class InitializationResult:
|
||||
"""Plugin initialization result with resource requests"""
|
||||
resources: list[ResourceRequest] = field(default_factory=list)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Example 1: IP Whitelist Plugin (DOMAIN)
|
||||
|
||||
Restrict access to specific IP addresses per domain.
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||
|
||||
|
||||
class 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:
|
||||
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:
|
||||
if not self.enabled or not self.allowed_ips:
|
||||
return PluginResult()
|
||||
|
||||
ips_str = " ".join(self.allowed_ips)
|
||||
|
||||
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,
|
||||
metadata={
|
||||
"domain": context.domain,
|
||||
"allowed_ips": self.allowed_ips,
|
||||
"status_code": self.status_code
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Example 2: Cleanup Plugin (GLOBAL)
|
||||
|
||||
Perform cleanup tasks during each discovery cycle.
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
|
||||
from functions import logger_easyhaproxy
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
logger_easyhaproxy.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:
|
||||
if not self.enabled:
|
||||
return PluginResult()
|
||||
|
||||
cleanup_actions = []
|
||||
|
||||
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:
|
||||
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}")
|
||||
except Exception as e:
|
||||
logger_easyhaproxy.warning(f"Failed to remove temp file {filepath}: {e}")
|
||||
except Exception as e:
|
||||
logger_easyhaproxy.warning(f"Failed to cleanup {temp_dir}: {e}")
|
||||
|
||||
return PluginResult(
|
||||
haproxy_config="",
|
||||
metadata={
|
||||
"actions_performed": len(cleanup_actions),
|
||||
"actions": cleanup_actions
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `initialize()` for resource setup** - Request directories/files during init, not in `process()`
|
||||
2. **Use typed result fields** - Use `global_configs` and `defaults_configs` instead of metadata for config injection
|
||||
3. **Handle errors gracefully** - Use try/except and return `PluginResult()` on error
|
||||
4. **Validate in `configure()`** - Don't validate at `process()` time
|
||||
5. **Use metadata for debugging** - Include useful info in `metadata` dict
|
||||
6. **Support multiple boolean formats** - `str(config["enabled"]).lower() in ["true", "1", "yes"]`
|
||||
7. **Support list and string formats** - Handle both YAML lists and comma-separated strings
|
||||
8. **Use descriptive names** - Clear plugin name, ACL names, and config keys
|
||||
9. **Document your plugin** - Include docstring with YAML and label examples
|
||||
10. **Return empty result when disabled** - Check `self.enabled` first
|
||||
|
||||
---
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Unit Testing
|
||||
|
||||
```python
|
||||
from plugins import PluginContext
|
||||
from plugins.builtin.my_plugin import MyPlugin
|
||||
|
||||
|
||||
class TestMyPlugin:
|
||||
def test_plugin_initialization(self):
|
||||
plugin = MyPlugin()
|
||||
assert plugin.name == "my_plugin"
|
||||
assert plugin.enabled is True
|
||||
|
||||
def test_plugin_generates_config(self):
|
||||
plugin = MyPlugin()
|
||||
plugin.configure({"my_setting": "test_value"})
|
||||
|
||||
context = PluginContext(
|
||||
parsed_object={},
|
||||
easymapping=[],
|
||||
container_env={},
|
||||
domain="example.com",
|
||||
port="80",
|
||||
host_config={}
|
||||
)
|
||||
|
||||
result = plugin.process(context)
|
||||
assert result.haproxy_config is not None
|
||||
assert "X-My-Header test_value" in result.haproxy_config
|
||||
|
||||
def test_plugin_disabled(self):
|
||||
plugin = MyPlugin()
|
||||
plugin.configure({"enabled": "false"})
|
||||
|
||||
context = PluginContext(
|
||||
parsed_object={}, easymapping=[], container_env={}, domain="example.com"
|
||||
)
|
||||
|
||||
result = plugin.process(context)
|
||||
assert result.haproxy_config == ""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
1. File not in plugins directory: `ls -la /etc/easyhaproxy/plugins/`
|
||||
2. Invalid Python syntax: `python3 -m py_compile /etc/easyhaproxy/plugins/my_plugin.py`
|
||||
3. Class doesn't inherit `PluginInterface`
|
||||
4. Missing required imports
|
||||
|
||||
### Plugin Not Executing
|
||||
|
||||
1. Plugin not enabled in configuration
|
||||
2. Wrong plugin type for use case
|
||||
3. Plugin disabled via configuration
|
||||
|
||||
### HAProxy Configuration Invalid
|
||||
|
||||
```bash
|
||||
# Test configuration manually:
|
||||
haproxy -c -f /etc/easyhaproxy/haproxy/haproxy.cfg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Distribution
|
||||
|
||||
### Option 1: Single File
|
||||
|
||||
```bash
|
||||
cp my_plugin.py /etc/easyhaproxy/plugins/
|
||||
```
|
||||
|
||||
### Option 2: Docker Image with Plugin
|
||||
|
||||
```dockerfile
|
||||
FROM byjg/easy-haproxy:latest
|
||||
COPY my_plugin.py /app/src/plugins/builtin/
|
||||
```
|
||||
|
||||
### Contributing to EasyHAProxy
|
||||
|
||||
1. Fork the repository
|
||||
2. Add your plugin to `src/plugins/builtin/`
|
||||
3. Add tests in `src/tests/test_plugins.py`
|
||||
4. Add documentation in `docs/reference/plugins/`
|
||||
5. Create a pull request
|
||||
|
||||
---
|
||||
|
||||
For more examples, see the builtin plugins in `/src/plugins/builtin/`:
|
||||
- `cloudflare.py` - Simple DOMAIN plugin
|
||||
- `fastcgi.py` - Advanced DOMAIN plugin with complex config
|
||||
- `jwt_validator.py` - Security plugin with path-based logic
|
||||
- `ip_whitelist.py` - Access control plugin
|
||||
- `cleanup.py` - GLOBAL plugin example
|
||||
338
docs/guides/plugins.md
Normal file
338
docs/guides/plugins.md
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
sidebar_label: "Using Plugins"
|
||||
---
|
||||
|
||||
# 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](../reference/plugins/cloudflare.md) - Restore visitor IP from Cloudflare CDN
|
||||
- [Cleanup](../reference/plugins/cleanup.md) - Cleanup temporary files
|
||||
- [Deny Pages](../reference/plugins/deny-pages.md) - Block specific paths
|
||||
- [IP Whitelist](../reference/plugins/ip-whitelist.md) - Restrict access to IPs/CIDR ranges
|
||||
- [JWT Validator](../reference/plugins/jwt-validator.md) - Validate JWT tokens
|
||||
- [FastCGI](../reference/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/easyhaproxy/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](../getting-started/kubernetes.md) 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/easyhaproxy/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`
|
||||
|
||||
## 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/easyhaproxy/jwt_keys/api_pubkey.pem
|
||||
volumes:
|
||||
- ./auth_pubkey.pem:/etc/easyhaproxy/jwt_keys/api_pubkey.pem:ro
|
||||
```
|
||||
|
||||
### Restrict Admin Panel to Office IPs
|
||||
|
||||
```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
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
easyhaproxy.http.host: myapp.com
|
||||
easyhaproxy.http.plugins: cloudflare
|
||||
```
|
||||
|
||||
### Multiple Plugins Together
|
||||
|
||||
```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
|
||||
|
||||
```yaml
|
||||
# /etc/easyhaproxy/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
|
||||
```
|
||||
|
||||
### Abort on Error
|
||||
|
||||
Stop discovery cycle if any plugin fails:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
abort_on_error: true
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```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/easyhaproxy/cloudflare_ips.lst'}
|
||||
```
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
**Check:**
|
||||
1. Plugin file exists in `/etc/easyhaproxy/plugins/` or builtin directory
|
||||
2. Python syntax is valid
|
||||
3. Plugin class inherits from `PluginInterface`
|
||||
4. Check logs for load errors
|
||||
|
||||
### 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)
|
||||
|
||||
## 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
|
||||
|
||||
## 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](../reference/container-labels.md) - Label configuration reference
|
||||
- [Environment Variables](../reference/environment-variables.md) - Environment variable reference
|
||||
- [Static Configuration](../getting-started/static.md) - YAML configuration reference
|
||||
- [Kubernetes Guide](../getting-started/kubernetes.md) - Using plugins with Kubernetes
|
||||
145
docs/guides/ssl.md
Normal file
145
docs/guides/ssl.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
sidebar_label: "Custom SSL Certificates"
|
||||
---
|
||||
|
||||
# Setup custom certificates
|
||||
|
||||
You can use your own certificates with EasyHAProxy instead of (or in addition to) automatic ACME/Certbot certificates.
|
||||
|
||||
:::info How SSL Termination Works
|
||||
SSL termination happens at the **HAProxy level**, NOT in your backend containers.
|
||||
|
||||
- Your backend containers should **only** expose HTTP (port 80), not HTTPS
|
||||
- HAProxy handles all SSL/TLS encryption and decryption
|
||||
- Backend containers receive plain HTTP traffic from HAProxy
|
||||
- Do NOT configure SSL in your backend application when using EasyHAProxy
|
||||
|
||||
This is the **correct design** - it centralizes SSL management at the proxy layer.
|
||||
:::
|
||||
|
||||
:::info Certificate Types
|
||||
EasyHAProxy supports two certificate sources:
|
||||
- **ACME/Certbot automatic certificates** - Issued automatically via Let's Encrypt or other ACME providers (see [ACME documentation](./acme.md))
|
||||
- **Manual/custom certificates** - Your own certificates loaded via volume mount (recommended) or labels (this page)
|
||||
|
||||
Both can be used simultaneously. Per domain, ACME certificates (if `certbot=true` label is set) take precedence over manual certificates.
|
||||
:::
|
||||
|
||||
There are two ways to provide custom certificates:
|
||||
|
||||
- [Map the certificate as a docker volume](#map-the-certificate-as-a-docker-volume)
|
||||
- [Setup certificate as a label definition](#setup-certificate-as-a-label-definition-in-docker-container)
|
||||
|
||||
## Map the certificate as a docker volume
|
||||
|
||||
EasyHAProxy stores the certificates inside the container folder `/etc/easyhaproxy/certs/haproxy`.
|
||||
|
||||
1. Run EasyHAProxy with the volume for the certificates:
|
||||
|
||||
```bash title="Create and mount certificate volume"
|
||||
docker volume create certs_haproxy
|
||||
|
||||
docker run \
|
||||
/* other parameters */
|
||||
-v certs_haproxy:/etc/easyhaproxy/certs/haproxy \
|
||||
-d byjg/easy-haproxy
|
||||
```
|
||||
|
||||
2. Create a single PEM from the certificate and the key.
|
||||
|
||||
```bash title="Combine certificate and key"
|
||||
cat example.com.crt example.com.key > single.pem
|
||||
|
||||
cat single.pem
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+
|
||||
U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL
|
||||
3j4wz8/I5fdsk090j4s5KA==
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
3. Copy this certificate to EasyHAProxy volume:
|
||||
|
||||
```bash title="Copy certificate to container"
|
||||
# IMPORTANT: Filename must match the domain!
|
||||
docker cp single.pem easyhaproxy:/etc/easyhaproxy/certs/haproxy/example.com.pem
|
||||
```
|
||||
|
||||
:::warning Important Notes
|
||||
- The filename **must match the domain name**: `example.com.pem` for domain `example.com`
|
||||
- When using volume-mounted certificates, **do NOT** use the `easyhaproxy.[definition].sslcert` label
|
||||
- The volume mount method and the label method are **mutually exclusive** per domain
|
||||
- SSL termination happens at HAProxy - your backend containers should only serve HTTP
|
||||
:::
|
||||
|
||||
4. Configure your backend container (no sslcert label needed):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
easyhaproxy:
|
||||
image: byjg/easy-haproxy:6.0.0
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- certs_haproxy:/etc/easyhaproxy/certs/haproxy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
myapp:
|
||||
image: nginx
|
||||
labels:
|
||||
easyhaproxy.web.host: example.com
|
||||
easyhaproxy.web.port: 80 # Frontend port (HAProxy listens here)
|
||||
easyhaproxy.web.localport: 80 # Backend port (your container)
|
||||
# NO sslcert label when using volume method!
|
||||
|
||||
volumes:
|
||||
certs_haproxy:
|
||||
```
|
||||
|
||||
## Setup certificate as a label definition in docker container
|
||||
|
||||
:::info Alternative Method
|
||||
This method embeds certificates directly in container labels. Use it when you want certificates in version control or don't want to manage external files. **Volume method is recommended for most use cases.**
|
||||
:::
|
||||
|
||||
1. Create a single PEM from the certificate and key:
|
||||
|
||||
```bash title="Combine certificate and key"
|
||||
cat example.com.crt example.com.key > single.pem
|
||||
```
|
||||
|
||||
2. Convert the `single.pem` to BASE64 in a single line:
|
||||
|
||||
```bash title="Convert to BASE64"
|
||||
cat single.pem | base64 -w0
|
||||
```
|
||||
|
||||
3. Add the Base64 string to your container label:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
myapp:
|
||||
image: nginx
|
||||
labels:
|
||||
easyhaproxy.web.host: example.com
|
||||
easyhaproxy.web.port: 80
|
||||
easyhaproxy.web.localport: 80
|
||||
easyhaproxy.web.sslcert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..." # Base64 certificate
|
||||
```
|
||||
|
||||
:::warning When Using Label Method
|
||||
- **There is no necessary to** mount the `/etc/easyhaproxy/certs/haproxy` volume for this domain
|
||||
- Using `sslcert` label means the volume-mounted certificate will be **ignored**
|
||||
- Certificate is visible in `docker inspect` output (less secure)
|
||||
- Updating requires container redeployment
|
||||
:::
|
||||
|
||||
----
|
||||
[Open source ByJG](http://opensource.byjg.com)
|
||||
Loading…
Add table
Add a link
Reference in a new issue