├── src ├── tests │ ├── __init__.py │ ├── fixtures │ │ ├── 00_haproxy.cfg │ │ ├── 10_haproxy.cfg │ │ ├── run_bash.sh │ │ ├── services-with-cloudflare │ │ ├── services-tcp │ │ ├── services-with-deny-pages │ │ ├── services-with-ip-whitelist │ │ ├── services-multi-containers │ │ ├── services-with-multiple-plugins │ │ ├── services-fcgi │ │ ├── no-services │ │ ├── services-with-jwt-validator │ │ ├── static.yml │ │ ├── services-clone-to-ssl │ │ ├── services-changed-label │ │ ├── services │ │ ├── services-multiple-hosts │ │ └── services-letsencrypt │ ├── context.py │ ├── expected │ │ ├── ssl-strict.txt │ │ ├── no-services.txt │ │ ├── services-tcp.txt │ │ ├── services-multi-containers.txt │ │ ├── ssl-loose.txt │ │ ├── services-fcgi.txt │ │ ├── services-multiple-hosts.txt │ │ ├── services-redirect-ssl.txt │ │ ├── docker.txt │ │ └── services-letsencrypt.txt │ ├── test_labels.py │ ├── test_static.py │ └── test_daemonize.py ├── pytest.ini ├── plugins │ └── builtin │ │ ├── __init__.py │ │ ├── deny_pages.py │ │ └── ip_whitelist.py ├── requirements.txt ├── templates │ ├── frontend-mode-tcp.j2 │ ├── bind.j2 │ ├── ssl_strict.j2 │ ├── ssl_default.j2 │ ├── frontend-mode-http.j2 │ ├── ssl_loose.j2 │ └── haproxy.cfg.j2 └── main.py ├── .dockerignore ├── docs ├── icons.xcf ├── logo.png ├── logo.xcf ├── video-docker.png ├── video-static.png ├── video-tcp-mysql.png ├── easyhaproxy_dokku.png ├── easyhaproxy_helm.png ├── easyhaproxy_logo.png ├── easyhaproxy_swarm.png ├── video-docker-ssl.png ├── video-kubernetes.png ├── easyhaproxy_docker.png ├── easyhaproxy_microk8s.png ├── easyhaproxy_static.png ├── easyhaproxy_kubernetes.png ├── easyhaproxy_digitalocean.png ├── video-kubernetes-letsencrypt.png ├── dokku.md ├── digitalocean.md ├── volumes.md ├── limitations.md ├── other.md ├── helm.md ├── microk8s.md ├── ssl.md ├── docker.md ├── Plugins │ ├── cleanup.md │ ├── deny-pages.md │ └── ip-whitelist.md ├── environment-variable.md └── swarm.md ├── .github ├── FUNDING.yml └── dependabot.yml ├── .vscode ├── settings.json └── launch.json ├── examples ├── docker │ ├── php-app │ │ ├── info.php │ │ └── test-path-info.php │ ├── docker-compose-portainer-app-example.yml │ ├── docker-compose-multi-containers.yml │ ├── docker-compose-php-fpm.yml │ ├── docker-compose-ip-whitelist.yml │ ├── docker-compose-cloudflare.yml │ ├── docker-compose-jwt-validator.yml │ ├── docker-compose-acme.yml │ ├── README.md │ └── docker-compose-portainer.yml ├── static │ ├── conf │ │ ├── config-basic.yml │ │ ├── config-deny-pages.yml │ │ ├── config-certbot.yml │ │ └── config-jwt-validator.yml │ ├── docker-compose.yml │ └── README.md ├── swarm │ ├── portainer.yml │ ├── easyhaproxy.yml │ ├── README.md │ ├── ip-whitelist.yml │ └── cloudflare.yml ├── kubernetes │ ├── README.md │ ├── ip-whitelist.yml │ ├── service.yml │ └── cloudflare.yml └── generate-keys.sh ├── Makefile ├── .gitpod.yml ├── helm └── easyhaproxy │ ├── templates │ ├── serviceaccount.yaml │ ├── clusterrolebinding.yaml │ ├── service.yaml │ ├── clusterrole.yaml │ ├── _helpers.tpl │ └── deployment.yaml │ ├── .helmignore │ ├── values.schema.json │ ├── LICENSE │ ├── Chart.yaml │ ├── values.yaml │ └── README.md ├── .gitignore ├── setup.py ├── deploy └── docker │ ├── install.sh │ └── docker-compose.yml ├── LICENSE ├── .run └── pytest in tests.run.xml └── CONTRIBUTING.md /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -p no:warnings 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | *.pyc 4 | .* 5 | -------------------------------------------------------------------------------- /src/tests/fixtures/00_haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 4000 3 | -------------------------------------------------------------------------------- /src/tests/fixtures/10_haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 5000 3 | -------------------------------------------------------------------------------- /src/plugins/builtin/__init__.py: -------------------------------------------------------------------------------- 1 | # Built-in plugins for EasyHAProxy 2 | -------------------------------------------------------------------------------- /docs/icons.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/icons.xcf -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/logo.xcf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: byjg 4 | -------------------------------------------------------------------------------- /docs/video-docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-docker.png -------------------------------------------------------------------------------- /docs/video-static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-static.png -------------------------------------------------------------------------------- /docs/video-tcp-mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-tcp-mysql.png -------------------------------------------------------------------------------- /docs/easyhaproxy_dokku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_dokku.png -------------------------------------------------------------------------------- /docs/easyhaproxy_helm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_helm.png -------------------------------------------------------------------------------- /docs/easyhaproxy_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_logo.png -------------------------------------------------------------------------------- /docs/easyhaproxy_swarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_swarm.png -------------------------------------------------------------------------------- /docs/video-docker-ssl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-docker-ssl.png -------------------------------------------------------------------------------- /docs/video-kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-kubernetes.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "certonly", 4 | "letsencrypt" 5 | ] 6 | } -------------------------------------------------------------------------------- /docs/easyhaproxy_docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_docker.png -------------------------------------------------------------------------------- /docs/easyhaproxy_microk8s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_microk8s.png -------------------------------------------------------------------------------- /docs/easyhaproxy_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_static.png -------------------------------------------------------------------------------- /docs/easyhaproxy_kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_kubernetes.png -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml 2 | docker 3 | jinja2 4 | pytest 5 | docker 6 | kubernetes 7 | deepdiff 8 | pyopenssl 9 | psutil -------------------------------------------------------------------------------- /docs/easyhaproxy_digitalocean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/easyhaproxy_digitalocean.png -------------------------------------------------------------------------------- /docs/video-kubernetes-letsencrypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byjg/docker-easy-haproxy/HEAD/docs/video-kubernetes-letsencrypt.png -------------------------------------------------------------------------------- /src/tests/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 6 | -------------------------------------------------------------------------------- /src/tests/fixtures/run_bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Just return the exit code 4 | echo "Processing run_bash.sh" 5 | if [ -n "$1" ]; then 6 | exit "$1" 7 | fi 8 | -------------------------------------------------------------------------------- /src/templates/frontend-mode-tcp.j2: -------------------------------------------------------------------------------- 1 | mode tcp 2 | option tcplog 3 | log global 4 | {% set backend = (o["hosts"]|first) %} 5 | default_backend srv_{{ backend.replace(".", "_") + "_{0}".format(o["port"]) }} 6 | 7 | -------------------------------------------------------------------------------- /examples/docker/php-app/info.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | -------------------------------------------------------------------------------- /helm/easyhaproxy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: easyhaproxy 3 | description: "EasyHAProxy - A service discovery backed on HAProxy" 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 1.0.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "5.0.0" 25 | 26 | icon: https://opensource.byjg.com/img/easy_haproxy_logo.png 27 | -------------------------------------------------------------------------------- /src/tests/expected/no-services.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | 25 | 26 | 27 | backend certbot_backend 28 | mode http 29 | server certbot 127.0.0.1:2080 30 | -------------------------------------------------------------------------------- /helm/easyhaproxy/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | --- 3 | kind: ClusterRole 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | metadata: 6 | name: {{ include "easyhaproxy.fullname" . }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "easyhaproxy.labels" . | nindent 4 }} 10 | {{- with .Values.serviceAccount.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | rules: 15 | - apiGroups: 16 | - "" 17 | resources: 18 | # - configmaps 19 | # - endpoints 20 | # - nodes 21 | - pods 22 | - services 23 | - namespaces 24 | # - events 25 | - serviceaccounts 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - apiGroups: 31 | - "extensions" 32 | - "networking.k8s.io" 33 | resources: 34 | - ingresses 35 | # - ingresses/status 36 | # - ingressclasses 37 | verbs: 38 | - get 39 | - list 40 | - watch 41 | # - apiGroups: 42 | # - "extensions" 43 | # - "networking.k8s.io" 44 | # resources: 45 | # - ingresses/status 46 | # verbs: 47 | # - update 48 | - apiGroups: 49 | - "" 50 | resources: 51 | - secrets 52 | verbs: 53 | - get 54 | - list 55 | # - watch 56 | # - create 57 | # - patch 58 | # - update 59 | # - apiGroups: 60 | # - "discovery.k8s.io" 61 | # resources: 62 | # - endpointslices 63 | # verbs: 64 | # - get 65 | # - list 66 | # - watch 67 | {{- end }} -------------------------------------------------------------------------------- /docs/volumes.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 13 3 | --- 4 | 5 | # Volumes 6 | 7 | :::info Volume Mapping 8 | These volumes allow you to persist certificates, provide custom configurations, and extend EasyHAProxy functionality. 9 | ::: 10 | 11 | You can map the following volumes: 12 | 13 | | Volume | Description | 14 | |-----------------------------|-------------------------------------------------------------------------------------------------------------------------------| 15 | | /etc/haproxy/static/ | The folder that will contain the [config.yml](static.md) file for static configuration | 16 | | /certs/haproxy/ | The folder that will contain the certificates (`PEM`) for the [SSL](ssl.md) | 17 | | /certs/certbot/ | The folder that will contain the certificates (`PEM`) processed by Certbot (e.g. Let's Encrypt). More info: [acme](acme.md). | 18 | | /etc/haproxy/conf.d/ | The folder that will contain the [custom configuration](other.md) files. | 19 | | /etc/haproxy/errors-custom/ | The folder that will contain the [custom error](other.md) html files. | 20 | 21 | ---- 22 | [Open source ByJG](http://opensource.byjg.com) 23 | -------------------------------------------------------------------------------- /src/templates/ssl_loose.j2: -------------------------------------------------------------------------------- 1 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 2 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 3 | ssl-default-bind-options no-sslv3 no-tls-tickets 4 | 5 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 6 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 7 | ssl-default-server-options no-sslv3 no-tls-tickets 8 | 9 | ssl-dh-param-file /etc/haproxy/dhparam-1024 10 | 11 | -------------------------------------------------------------------------------- /docs/limitations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 23 3 | --- 4 | 5 | # Limitations and Considerations 6 | 7 | ## EasyHAProxy will not work with --network=host 8 | 9 | :::danger Network Mode Incompatibility 10 | The `--network=host` option **cannot** be used with EasyHAProxy due to its networking requirements. 11 | 12 | EasyHAProxy needs to inspect and interact with Docker containers from within the Docker network where it's running. Using the `--network=host` option bypasses Docker networking, preventing EasyHAProxy from accessing and configuring containers effectively. 13 | ::: 14 | 15 | ## Considerations for Multiple Replica Deployments in EasyHAProxy 16 | 17 | :::warning Single Replica Deployment Recommended 18 | EasyHAProxy is designed for single replica deployment. 19 | ::: 20 | 21 | ### What happens with multiple replicas? 22 | 23 | EasyHAProxy can still operate with multiple replicas; however: 24 | 25 | 1. **Service Discovery**: Each replica will independently discover services, which may lead to temporary inconsistencies among replicas (out-of-sync for a few seconds). 26 | 27 | 2. **ACME/Certificate Issues**: Running multiple replicas creates significant problems with Let's Encrypt and other ACME certificate issuance: 28 | - Each replica will attempt to obtain its own certificate 29 | - ACME challenges may be directed to different replicas, causing failures 30 | - You may quickly hit certificate issuance rate limits 31 | - Certificate renewal may fail unpredictably 32 | 33 | :::danger Recommendation 34 | If you need to run multiple replicas for high availability, **do not activate ACME/Let's Encrypt**. Instead, use manually managed certificates or an external certificate management solution. 35 | ::: 36 | 37 | ---- 38 | [Open source ByJG](http://opensource.byjg.com) 39 | -------------------------------------------------------------------------------- /src/tests/expected/services-tcp.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | 25 | 26 | 27 | frontend tcp_in_31339 28 | bind *:31339 29 | mode tcp 30 | option tcplog 31 | log global 32 | default_backend srv_agent_quantum_local_31339 33 | 34 | backend srv_agent_quantum_local_31339 35 | balance roundrobin 36 | mode tcp 37 | option tcp-check 38 | tcp-check connect ssl 39 | server srv-0 test_agent:9001 check weight 1 verify none 40 | 41 | backend certbot_backend 42 | mode http 43 | server certbot 127.0.0.1:2080 44 | -------------------------------------------------------------------------------- /docs/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 22 3 | --- 4 | 5 | # Other configurations 6 | 7 | ## Exposing Ports 8 | 9 | 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. 10 | 11 | - The ports `80` and `443`. 12 | - 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`. 13 | - Every port defined in `easyhaproxy.[definitions].port` also should be exposed. 14 | 15 | For example: 16 | 17 | ```bash title="Expose required ports" 18 | docker run \ 19 | /* other parameters */ 20 | -p 80:80 \ 21 | -p 443:443 \ 22 | -p 1936:1936 \ 23 | -d byjg/easy-haproxy 24 | ``` 25 | 26 | ## Mapping Docker Volume 27 | 28 | The docker volume or a way to call the API needs to pass to the EasyHAProxy container. 29 | 30 | ```bash title="Mount Docker socket" 31 | docker run \ 32 | /* other parameters */ 33 | -v /var/run/docker.sock:/var/run/docker.sock \ 34 | -d byjg/easy-haproxy 35 | ``` 36 | 37 | ## Mapping custom .cfg files 38 | 39 | You can concatenate valid HAProxy `.cfg` files to the dynamically generated `haproxy.cfg` by mapping the folder `/etc/haproxy/conf.d`. 40 | 41 | ```bash title="Mount custom config directory" 42 | docker run \ 43 | /* other parameters */ 44 | -v /your/local/conf.d:/etc/haproxy/conf.d \ 45 | -d byjg/easy-haproxy 46 | ``` 47 | 48 | ## Setting Custom Errors 49 | 50 | If enabled, map the volume : `/etc/haproxy/errors-custom/` to your container and put a file named `ERROR_NUMBER.http` 51 | where ERROR_NUMBER is the HTTP error code (e.g., `503.http`) 52 | 53 | ---- 54 | [Open source ByJG](http://opensource.byjg.com) 55 | -------------------------------------------------------------------------------- /helm/easyhaproxy/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for easyhaproxy. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: byjg/easy-haproxy 9 | pullPolicy: Always # IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | service: 18 | create: false # If false, it will create a Daemonset with hostPort. The easiest. 19 | type: ClusterIP # or NodePort 20 | annotations: {} 21 | 22 | binding: 23 | ports: 24 | http: 80 25 | https: 443 26 | stats: 1936 27 | additionalPorts: [] 28 | 29 | serviceAccount: 30 | create: true 31 | annotations: {} 32 | name: "" 33 | 34 | podAnnotations: {} 35 | 36 | podSecurityContext: {} 37 | # fsGroup: 2000 38 | 39 | securityContext: {} 40 | # capabilities: 41 | # drop: 42 | # - ALL 43 | # readOnlyRootFilesystem: true 44 | # runAsNonRoot: true 45 | # runAsUser: 1000 46 | 47 | resources: {} 48 | # requests: 49 | # cpu: "100m" 50 | # memory: "128Mi" 51 | # limits: 52 | # cpu: 100m 53 | # memory: 128Mi 54 | 55 | nodeSelector: {} 56 | 57 | tolerations: [] 58 | 59 | affinity: {} 60 | 61 | easyhaproxy: 62 | stats: 63 | username: admin 64 | password: password 65 | refresh: "10" 66 | customErrors: "true" 67 | sslMode: loose 68 | logLevel: 69 | certbot: DEBUG 70 | easyhaproxy: DEBUG 71 | haproxy: DEBUG 72 | certbot: 73 | email: "" 74 | 75 | # Master node configuration - only used when service.create is false (DaemonSet mode) 76 | # When service.create is true (Deployment with ClusterIP/NodePort), this is ignored 77 | masterNode: 78 | label: easyhaproxy/node 79 | values: 80 | - master 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to byjg/docker-easy-haproxy 2 | 3 | First of all, thank you for taking the time to contribute! 4 | 5 | ## How to Contribute 6 | 7 | ### Issues 8 | 9 | If you encounter any issues, have questions, or need clarification, please open an issue on our [Issues page](https://github.com/your-repo/issues). This helps us track and prioritize bug fixes and enhancements. 10 | 11 | ### Branches 12 | 13 | We have three main branches in this project: 14 | 15 | - **master**: Contains the latest code. It is generally stable, but we recommend using it with caution. 16 | - **a.b**: Use this branch for creating PRs. The naming convention follows `a.b`, where `a` is the major release and `b` is the minor release of the current version. For example, if the current release is 4.9.2, use the branch `4.9` for your PR. You can also use `4.9.x-dev` in your composer for development purposes. 17 | - **future release**: This branch is typically `(a+1).0`. For instance, if the current release is 4.9.2, the future release branch will be `5.0`. 18 | 19 | 20 | ### Code Style and Guidelines 21 | 22 | - **Write Clear Commit Messages**: Use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. 23 | - **Documentation**: Update the documentation for any new features or changes. 24 | 25 | ### Common Practices 26 | 27 | - **Keep Pull Requests Small**: Smaller PRs are easier to review and merge. Focus on one feature or fix per PR. 28 | - **Write Tests**: Ensure your changes are covered by tests. We aim for a high level of test coverage. 29 | - **Respect Reviewers' Time**: Be responsive to feedback and willing to make necessary changes. 30 | 31 | ### Community 32 | 33 | - **Be Respectful**. 34 | - **Collaborate**: We encourage collaboration and open discussion. Don’t hesitate to ask for help or provide feedback. 35 | 36 | Thank you for contributing to byjg/docker-easy-haproxy! Your help is appreciated and makes a big difference. 37 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-portainer-app-example.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Additional Application with Portainer 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Adding more applications to an existing EasyHAProxy setup 7 | # - Using the shared "easyhaproxy" network 8 | # - Multiple applications behind the same HAProxy instance 9 | # 10 | # REQUIREMENTS (run these first): 11 | # ```bash 12 | # # 1. First start the Portainer stack (creates network and HAProxy) 13 | # docker compose -f docker-compose-portainer.yml up -d 14 | # 15 | # # 2. Edit this file and change: 16 | # # - Line 6: easyhaproxy.http.host to your real domain 17 | # ``` 18 | # 19 | # HOW TO START: 20 | # ```bash 21 | # docker compose -f docker-compose-portainer-app-example.yml up -d 22 | # ``` 23 | # 24 | # HOW TO VERIFY IT'S WORKING: 25 | # ```bash 26 | # # Check container is running 27 | # docker compose -f docker-compose-portainer-app-example.yml ps 28 | # 29 | # # Test the application 30 | # curl http://test.xpto.us 31 | # # OR with /etc/hosts: echo "127.0.0.1 test.xpto.us" | sudo tee -a /etc/hosts 32 | # 33 | # # Verify both apps in HAProxy stats (port 1936) 34 | # # You should see backends for both portainer.xpto.us and test.xpto.us 35 | # ``` 36 | # 37 | # CLEAN UP: 38 | # ```bash 39 | # docker compose -f docker-compose-portainer-app-example.yml down 40 | # ``` 41 | # 42 | # ============================================================================== 43 | 44 | services: 45 | container: 46 | image: byjg/static-httpserver 47 | labels: 48 | easyhaproxy.http.redirect_ssl: true 49 | easyhaproxy.http.host: test.xpto.us 50 | easyhaproxy.http.port: 80 51 | easyhaproxy.http.localport: 8080 52 | easyhaproxy.http.certbot: true 53 | 54 | 55 | networks: 56 | default: 57 | name: easyhaproxy 58 | external: true 59 | -------------------------------------------------------------------------------- /src/tests/fixtures/services-changed-label: -------------------------------------------------------------------------------- 1 | {"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, 2 | "my-stack_agent": {"haproxy.agent.host":"agent.quantum.example.org","haproxy.agent.localport":"9001","haproxy.agent.mode":"tcp","haproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 3 | "my-stack_cadvisor": {"haproxy.cadvisor.host":"cadvisor.quantum.example.org","haproxy.cadvisor.localport":"8080","haproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 4 | "my-stack_node-exporter": {"haproxy.exp.host":"node-exporter.quantum.example.org","haproxy.exp.localport":"9100","haproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","haproxy.exp.certbot":"yes"}, 5 | "my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 6 | "some-service": {"haproxy.http.port":"80","haproxy.http.host":"www.somehost.com.br","haproxy.http.localport":"80","haproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.port":"443","haproxy.https.host":"www.somehost.com.br","haproxy.https.localport":"80","haproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","haproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} -------------------------------------------------------------------------------- /src/tests/fixtures/services: -------------------------------------------------------------------------------- 1 | {"portainer-agent_agent": {"com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"portainer-agent"}, 2 | "my-stack_agent": {"easyhaproxy.agent.host":"agent.quantum.example.org","easyhaproxy.agent.localport":"9001","easyhaproxy.agent.mode":"tcp","easyhaproxy.agent.port":"31339","com.docker.stack.image":"portainer/agent:1.5.1","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 3 | "my-stack_cadvisor": {"easyhaproxy.cadvisor.host":"cadvisor.quantum.example.org","easyhaproxy.cadvisor.localport":"8080","easyhaproxy.cadvisor.port":"31337","com.docker.stack.image":"gcr.io/google-containers/cadvisor:v0.34.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 4 | "my-stack_node-exporter": {"easyhaproxy.exp.host":"node-exporter.quantum.example.org","easyhaproxy.exp.localport":"9100","easyhaproxy.exp.port":"31337","com.docker.stack.image":"stefanprodan/swarmprom-node-exporter:v0.16.0","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring","easyhaproxy.exp.certbot":"true"}, 5 | "my-stack_reverse-proxy": {"com.docker.stack.image":"quay.io/pngmbh/easy-haproxy:tcp-mode","com.docker.stack.namespace":"my-stack","com.planetary-quantum":"monitoring"}, 6 | "some-service": {"easyhaproxy.http.port":"80","easyhaproxy.http.host":"www.somehost.com.br","easyhaproxy.http.localport":"80","easyhaproxy.http.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.port":"443","easyhaproxy.https.host":"www.somehost.com.br","easyhaproxy.https.localport":"80","easyhaproxy.https.redirect":"{\"somehost.com.br\":\"https://www.somehost.com.br\",\"somehost.com\":\"https://www.somehost.com.br\",\"www.somehost.com\":\"https://www.somehost.com.br\",\"byjg.ca\":\"https://www.somehost.com.br\",\"www.byjg.ca\":\"https://www.somehost.com.br\"}","easyhaproxy.https.sslcert":"U29tZSBQRU0gQ2VydGlmaWNhdGU="}} -------------------------------------------------------------------------------- /examples/swarm/portainer.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Portainer Behind EasyHAProxy (Swarm) 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Running Portainer management UI in Swarm mode 7 | # - Service discovery with EasyHAProxy 8 | # - Using persistent volumes for Portainer data 9 | # 10 | # REQUIREMENTS (run these first): 11 | # ```bash 12 | # # Ensure EasyHAProxy is deployed 13 | # docker stack deploy -c easyhaproxy.yml easyhaproxy 14 | # 15 | # # Add to /etc/hosts (idempotent) 16 | # grep -q "portainer.local" /etc/hosts || echo "127.0.0.1 portainer.local" | sudo tee -a /etc/hosts 17 | # ``` 18 | # 19 | # HOW TO START: 20 | # ```bash 21 | # docker stack deploy -c portainer.yml portainer 22 | # ``` 23 | # 24 | # HOW TO VERIFY IT'S WORKING: 25 | # ```bash 26 | # # Check service is running 27 | # docker service ls | grep portainer 28 | # # Expected: portainer_portainer with 1/1 replicas 29 | # 30 | # # Access Portainer 31 | # curl http://portainer.local 32 | # # Or open in browser: http://portainer.local 33 | # # First time: Create admin user 34 | # ``` 35 | # 36 | # CLEAN UP: 37 | # ```bash 38 | # docker stack rm portainer 39 | # # To also remove data volume: 40 | # # docker volume rm portainer_portainer_data 41 | # ``` 42 | # 43 | # ============================================================================== 44 | 45 | services: 46 | portainer: 47 | image: portainer/portainer-ce:latest 48 | volumes: 49 | - portainer_data:/data portainer 50 | - /var/run/docker.sock:/var/run/docker.sock 51 | deploy: 52 | replicas: 1 53 | labels: 54 | # easyhaproxy.http.redirect_ssl: true 55 | # easyhaproxy.http.certbot: true 56 | easyhaproxy.http.host: portainer.local 57 | easyhaproxy.http.port: 80 58 | easyhaproxy.http.localport: 9000 59 | 60 | volumes: 61 | certs_certbot: 62 | external: true 63 | # certs_haproxy: 64 | # external: true 65 | portainer_data: 66 | # external: true 67 | 68 | -------------------------------------------------------------------------------- /helm/easyhaproxy/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "easyhaproxy.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "easyhaproxy.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "easyhaproxy.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "easyhaproxy.labels" -}} 37 | helm.sh/chart: {{ include "easyhaproxy.chart" . }} 38 | {{ include "easyhaproxy.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "easyhaproxy.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "easyhaproxy.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "easyhaproxy.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "easyhaproxy.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /src/tests/expected/services-multi-containers.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | 25 | 26 | 27 | frontend http_in_19901 28 | bind *:19901 29 | mode http 30 | 31 | acl is_rule_www_helloworld_com_19901_1 hdr(host) -i www.helloworld.com 32 | acl is_rule_www_helloworld_com_19901_2 hdr(host) -i www.helloworld.com:19901 33 | use_backend srv_www_helloworld_com_19901 if is_rule_www_helloworld_com_19901_1 OR is_rule_www_helloworld_com_19901_2 34 | 35 | backend srv_www_helloworld_com_19901 36 | balance roundrobin 37 | mode http 38 | option forwardfor 39 | http-request set-header X-Forwarded-Port %[dst_port] 40 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 41 | server srv-0 test_nginx.2.t5r94mjlced7m3t5orfjbowmm:80 check weight 1 42 | server srv-1 test_nginx.1.p552hqxkdx88narjrp5kouwb2:80 check weight 1 43 | 44 | backend certbot_backend 45 | mode http 46 | server certbot 127.0.0.1:2080 47 | -------------------------------------------------------------------------------- /docs/helm.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Helm 3 6 | 7 | Helm is a package manager for Kubernetes. It allows you to install and manage applications on Kubernetes. 8 | 9 | ## Setup EasyHAProxy with Helm 3 10 | 11 | ### 1) Identify the node where your EasyHAProxy container will run 12 | 13 | :::warning Single Node Deployment 14 | EasyHAProxy will be limited to a single node. To understand why, see the [limitations](limitations.md) page. 15 | ::: 16 | 17 | ```bash title="List available nodes" 18 | $ kubectl get nodes 19 | 20 | NAME STATUS ROLES AGE VERSION 21 | node-01 Ready 561d v1.21.13-3 22 | node-02 Ready 561d v1.21.13-3 23 | ``` 24 | 25 | Add the EasyHAProxy label to the node. 26 | 27 | ```bash title="Label the node for EasyHAProxy" 28 | kubectl label nodes node-01 "easyhaproxy/node=master" 29 | ``` 30 | 31 | ### 2) Install EasyHAProxy 32 | 33 | Minimal configuration: 34 | 35 | ```bash title="Install with Helm (minimal)" 36 | helm repo add byjg https://opensource.byjg.com/helm 37 | helm repo update byjg 38 | kubectl create namespace easyhaproxy 39 | 40 | helm upgrade --install ingress byjg/easyhaproxy \ 41 | --namespace easyhaproxy \ 42 | --set resources.requests.cpu=100m \ 43 | --set resources.requests.memory=128Mi 44 | ``` 45 | 46 | Customizing Helm Values: 47 | 48 | ```yaml title="values.yaml" 49 | easyhaproxy: 50 | stats: 51 | username: admin 52 | password: password 53 | refresh: "10" 54 | customErrors: "true" 55 | sslMode: loose 56 | logLevel: 57 | certbot: DEBUG 58 | easyhaproxy: DEBUG 59 | haproxy: DEBUG 60 | certbot: 61 | email: "" 62 | 63 | service: 64 | create: false # If false, it will create a DaemonSet with hostPort. The easiest. 65 | type: ClusterIP # or NodePort 66 | annotations: {} 67 | 68 | binding: 69 | ports: 70 | http: 80 71 | https: 443 72 | stats: 1936 73 | additionalPorts: [] 74 | 75 | # Make sure to create this 76 | masterNode: 77 | label: easyhaproxy/node 78 | values: 79 | - master 80 | ``` 81 | 82 | For more parameters you can refer to the [Kubernetes](kubernetes.md) page. 83 | 84 | ---- 85 | [Open source ByJG](http://opensource.byjg.com) 86 | -------------------------------------------------------------------------------- /docs/microk8s.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Microk8s Add-ons 6 | 7 | Microk8s is a lightweight Kubernetes distribution that can run on a single machine. It is very easy to install and use. 8 | You can add custom addons to your Microk8s installation. 9 | 10 | Here are the steps to install EasyHAProxy on your Microk8s. 11 | 12 | ## Enabling EasyHAProxy on MicroK8s 13 | 14 | EasyHAProxy is being part of official MicroK8s Community edition since MicroK8s version 1.27. 15 | 16 | Just enable the community add-on: 17 | 18 | ```bash title="Enable community addons" 19 | microk8s enable community 20 | ``` 21 | 22 | and you'll see: 23 | 24 | ``` 25 | $ microk8s status 26 | 27 | microk8s is running 28 | ... 29 | addons: 30 | ... 31 | disabled: 32 | easyhaproxy # (community) EasyHAProxy can detect and configure HAProxy automatically based on ingress labels 33 | ``` 34 | 35 | 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: 36 | 37 | ```bash title="Add ByJG addon repository" 38 | microk8s addons repo add byjg https://github.com/byjg/microk8s-addons.git 39 | ``` 40 | 41 | And you should see: 42 | 43 | ```text 44 | $ microk8s status 45 | 46 | microk8s is running 47 | ... 48 | addons: 49 | ... 50 | disabled: 51 | easyhaproxy # (byjg) EasyHAProxy can detect and configure HAProxy automatically based on ingress labels 52 | .... 53 | ``` 54 | 55 | ## Installing EasyHAProxy addon 56 | 57 | Once you have enable the EasyHAProxy from the community repository or from ByJG repository and can enable it by running: 58 | 59 | Usage: 60 | 61 | Install as a Daemonset: 62 | 63 | ```bash title="Install as DaemonSet" 64 | microk8s enable easyhaproxy 65 | ``` 66 | 67 | Install as a NodePort: 68 | 69 | ```bash title="Install as NodePort" 70 | microk8s enable easyhaproxy --nodeport 71 | ``` 72 | 73 | :::warning Disable Other Ingress Controllers 74 | You need to disable any ingress controller you have previously installed (e.g., nginx, traefik, etc.) before installing EasyHAProxy to avoid conflicts. 75 | ::: 76 | 77 | For more parameters you can refer to the [Kubernetes](kubernetes.md) page. 78 | 79 | ---- 80 | [Open source ByJG](http://opensource.byjg.com) 81 | -------------------------------------------------------------------------------- /helm/easyhaproxy/README.md: -------------------------------------------------------------------------------- 1 | # EasyHAProxy 2 | 3 | EasyHAProxy is a simple and easy way to automate the creation of HAProxy configuration file based on Docker containers. 4 | 5 | The Helm package makes it easy to deploy EasyHAProxy on Kubernetes. 6 | 7 | More information about [EasyHAProxy](https://github.com/byjg/docker-easy-haproxy/blob/master/README.md). 8 | 9 | ## Install 10 | 11 | ```bash 12 | helm repo add byjg https://opensource.byjg.com/helm 13 | helm repo update byjg 14 | helm upgrade --install --create-namespace -n easyhaproxy easyhaproxy byjg/easyhaproxy 15 | ``` 16 | 17 | ## Deployment Types 18 | 19 | EasyHAProxy can be deployed in two ways: 20 | 21 | 1. **DaemonSet (default)**: When `service.create: false`, EasyHAProxy is deployed as a DaemonSet with hostPort, and will only be scheduled on nodes with the masterNode label. 22 | 2. **Deployment**: When `service.create: true`, EasyHAProxy is deployed as a Deployment with a Service (ClusterIP or NodePort), and the `replicaCount` value determines the number of replicas (defaults to 1 if not specified). 23 | 24 | ## Parameters 25 | 26 | ```yaml 27 | replicaCount: 1 # Only applies when service.create is true (defaults to 1 if not specified) 28 | 29 | image: 30 | repository: byjg/easy-haproxy 31 | pullPolicy: Always # IfNotPresent 32 | # Overrides the image tag whose default is the chart appVersion. 33 | tag: "" 34 | 35 | imagePullSecrets: [] 36 | nameOverride: "" 37 | fullnameOverride: "" 38 | 39 | service: 40 | create: false # If false, it will create a Daemonset with hostPort. The easiest. 41 | type: ClusterIP # or NodePort 42 | annotations: {} 43 | 44 | binding: 45 | ports: 46 | http: 80 47 | https: 443 48 | stats: 1936 49 | additionalPorts: [] 50 | 51 | easyhaproxy: 52 | stats: 53 | username: admin 54 | password: password 55 | refresh: "10" 56 | customErrors: "true" 57 | sslMode: loose 58 | logLevel: 59 | certbot: DEBUG 60 | easyhaproxy: DEBUG 61 | haproxy: DEBUG 62 | certbot: 63 | email: "" 64 | 65 | # Master node configuration - only used when service.create is false (DaemonSet mode) 66 | # When service.create is true (Deployment with ClusterIP/NodePort), this is ignored 67 | masterNode: 68 | label: easyhaproxy/node 69 | values: 70 | - master 71 | ``` 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/tests/test_static.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from functions import Functions 4 | from processor import ProcessorInterface 5 | 6 | 7 | def test_processor_static(): 8 | ProcessorInterface.static_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "./fixtures/static.yml") 9 | static = ProcessorInterface.factory(ProcessorInterface.STATIC) 10 | 11 | parsed_object = [ 12 | { 13 | "hosts": { 14 | "host1.com.br": { 15 | "containers": [ 16 | "container:5000" 17 | ], 18 | "certbot": True 19 | }, 20 | "host2.com.br": { 21 | "containers": [ 22 | "other:3000" 23 | ] 24 | } 25 | }, 26 | "port": 80, 27 | "redirect": { 28 | "www.host1.com.br": "http://host1.com.br" 29 | } 30 | }, 31 | { 32 | "hosts": { 33 | "host1.com.br": { 34 | "containers": [ 35 | "container:80" 36 | ] 37 | } 38 | }, 39 | "port": 443, 40 | "ssl": True 41 | }, 42 | { 43 | "hosts": { 44 | "host3.com.br": { 45 | "containers": [ 46 | "domain:8181" 47 | ] 48 | } 49 | }, 50 | "port": 8080 51 | } 52 | ] 53 | hosts = [ 54 | 'host1.com.br:80', 55 | 'host2.com.br:80', 56 | 'host1.com.br:443', 57 | 'host3.com.br:8080' 58 | ] 59 | 60 | assert static.get_certbot_hosts() is None 61 | assert static.get_parsed_object() == parsed_object 62 | assert static.get_hosts() == hosts 63 | 64 | haproxy_cfg = static.get_haproxy_conf() 65 | 66 | assert haproxy_cfg == Functions.load( 67 | os.path.join(os.path.dirname(os.path.realpath(__file__)), "./expected/static.txt")) 68 | 69 | # @todo: Static doesnt populate this fields 70 | assert static.get_certbot_hosts() == [] 71 | assert static.get_parsed_object() == parsed_object 72 | assert static.get_hosts() == hosts 73 | 74 | # test_processor_static() 75 | -------------------------------------------------------------------------------- /src/tests/expected/ssl-loose.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 5 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 6 | ssl-default-bind-options no-sslv3 no-tls-tickets 7 | 8 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 9 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 10 | ssl-default-server-options no-sslv3 no-tls-tickets 11 | 12 | ssl-dh-param-file /etc/haproxy/dhparam-1024 13 | 14 | 15 | defaults 16 | log global 17 | option httplog 18 | 19 | timeout connect 3s 20 | timeout client 10s 21 | timeout server 10m 22 | 23 | 24 | frontend stats 25 | bind *:1936 26 | mode http 27 | http-request use-service prometheus-exporter if { path /metrics } 28 | stats enable 29 | stats hide-version 30 | stats realm Haproxy\ Statistics 31 | stats uri / 32 | default_backend srv_stats 33 | 34 | backend srv_stats 35 | mode http 36 | server Local 127.0.0.1:1936 37 | 38 | backend certbot_backend 39 | mode http 40 | server certbot 127.0.0.1:2080 41 | -------------------------------------------------------------------------------- /examples/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Examples 2 | 3 | Self-contained examples for EasyHAProxy ingress controller. **All documentation is in the YAML files as header comments.** 4 | 5 | ## Quick Start 6 | 7 | 1. Pick an example below 8 | 2. Open the YAML file 9 | 3. Read the header comments for complete instructions 10 | 4. Run the commands step-by-step 11 | 12 | ## Prerequisites 13 | 14 | All examples require: 15 | - EasyHAProxy installed in your Kubernetes cluster 16 | - Node labeled for EasyHAProxy deployment 17 | 18 | See header comments in each file for detailed setup instructions. 19 | 20 | ## Basic Examples 21 | 22 | | File | Description | 23 | |------------------------------------|--------------------------------------------| 24 | | [service.yml](service.yml) | Basic HTTP ingress with multiple domains | 25 | | [service_tls.yml](service_tls.yml) | HTTPS/TLS ingress with custom certificates | 26 | 27 | ## Plugin Examples 28 | 29 | | File | Description | 30 | |----------------------------------------------|-----------------------------------------------------| 31 | | [jwt-validator.yml](jwt-validator.yml) | JWT token validation for API protection | 32 | | [ip-whitelist.yml](ip-whitelist.yml) | IP whitelist for admin panels or sensitive services | 33 | | [cloudflare.yml](cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN | 34 | | [plugins-combined.yml](plugins-combined.yml) | Multiple plugins combined for layered security | 35 | 36 | ## Documentation Structure 37 | 38 | Each YAML file contains: 39 | - **WHAT THIS DEMONSTRATES** - Key features and concepts 40 | - **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times) 41 | - **HOW TO START** - Command to apply the manifest 42 | - **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs 43 | - **CLEAN UP** - Commands to remove resources 44 | 45 | ## Additional Documentation 46 | 47 | - [Kubernetes Installation Guide](../../docs/kubernetes.md) 48 | - [Helm Installation](../../docs/helm.md) 49 | - [Kubernetes Annotations Reference](../../docs/kubernetes.md#kubernetes-annotations) 50 | - [Using Plugins with Kubernetes](../../docs/kubernetes.md#using-plugins-with-kubernetes) 51 | -------------------------------------------------------------------------------- /src/tests/fixtures/services-multiple-hosts: -------------------------------------------------------------------------------- 1 | {"db79d3a910f4": {"com.docker.compose.config-hash":"5bde40f52451521ad201e70de1291397376a0498a7c955624a609da3b60e7e8e","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:ea39067705590557dd0cd951664a10970ceefcb725a3c1f43690d6d6d4ed5fce","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}, 2 | "3e63154954b0": {"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"}, 3 | "eb294c110eb1": {"com.docker.compose.config-hash":"4e0cbdd8372c6779863799e5021ed8178f74b55bd8e070abcdffaf87eb7baa36","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-multi-containers.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"hello.com\n, www.helloworld.com\n","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"19901","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}"}} -------------------------------------------------------------------------------- /examples/static/conf/config-deny-pages.yml: -------------------------------------------------------------------------------- 1 | # Deny Pages Plugin Configuration Example 2 | # 3 | # Demonstrates: 4 | # - Global plugin configuration (applies to all domains) 5 | # - Per-domain plugin override (custom settings per host) 6 | # 7 | # To use: 8 | # 1. Update container names and ports 9 | # 2. Mount this config: -v ./conf/config-deny-pages.yml:/etc/haproxy/static/config.yml 10 | # 3. Test blocked paths: 11 | # curl http://host1.local/admin # Should return 404 12 | # curl http://host2.local/wp-admin # Should return 403 (different config) 13 | 14 | stats: 15 | username: admin 16 | password: password 17 | port: 1936 18 | 19 | customerrors: true 20 | 21 | # Global plugin configuration 22 | # This applies to ALL domains unless overridden 23 | plugins: 24 | enabled: 25 | - deny_pages 26 | 27 | config: 28 | deny_pages: 29 | # Global default: block common admin paths with 404 30 | paths: 31 | - /admin 32 | - /.env 33 | - /config 34 | status_code: 404 # Hide existence of these paths 35 | 36 | easymapping: 37 | - port: 80 38 | hosts: 39 | # Domain 1: Uses global deny_pages configuration 40 | host1.local: 41 | containers: 42 | - webapp1:8080 43 | # No plugins specified = uses global configuration 44 | 45 | # Domain 2: WordPress site with custom blocked paths 46 | host2.local: 47 | containers: 48 | - wordpress:80 49 | # Override global plugin configuration for this domain 50 | plugins: 51 | - deny_pages 52 | plugin_config: 53 | deny_pages: 54 | paths: 55 | - /wp-admin 56 | - /wp-login.php 57 | - /xmlrpc.php 58 | - /wp-config.php 59 | status_code: 403 # Return forbidden instead of 404 60 | 61 | # Domain 3: Public site with stricter blocking 62 | host3.local: 63 | containers: 64 | - publicsite:3000 65 | plugins: 66 | - deny_pages 67 | plugin_config: 68 | deny_pages: 69 | paths: 70 | - /admin 71 | - /administrator 72 | - /manager 73 | - /phpmyadmin 74 | - /.git 75 | - /.env 76 | - /config 77 | - /backup 78 | status_code: 404 79 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-multi-containers.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Load Balancing with Multiple Container Replicas 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Multiple container replicas behind a single domain 7 | # - Round-robin load balancing across replicas 8 | # - Domain redirect functionality 9 | # - Custom port configuration 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # No special requirements - this example runs on localhost:19901 14 | # ``` 15 | # 16 | # HOW TO START: 17 | # ```bash 18 | # docker compose -f docker-compose-multi-containers.yml up -d 19 | # ``` 20 | # 21 | # HOW TO VERIFY IT'S WORKING: 22 | # ```bash 23 | # # Test load balancing - hostname should alternate between containers 24 | # curl -H "Host: www.helloworld.com" localhost:19901 25 | # # Expected: Container ID (e.g., f6d8d45b7411) 26 | # curl -H "Host: www.helloworld.com" localhost:19901 27 | # # Expected: Different container ID (e.g., 59b213cb8592) 28 | # 29 | # # Test domain redirect 30 | # curl -I -H "Host: google.helloworld.com" localhost:19901 31 | # # Expected: HTTP/1.1 301 Moved Permanently, Location: www.google.com/ 32 | # 33 | # # View HAProxy stats 34 | # # URL: http://localhost:1936 35 | # # Username: admin 36 | # # Password: password 37 | # # You should see 2 backend servers 38 | # ``` 39 | # 40 | # CLEAN UP: 41 | # ```bash 42 | # docker compose -f docker-compose-multi-containers.yml down 43 | # ``` 44 | # 45 | # ============================================================================== 46 | 47 | services: 48 | haproxy: 49 | image: byjg/easy-haproxy:5.0.0 50 | volumes: 51 | - /var/run/docker.sock:/var/run/docker.sock 52 | environment: 53 | EASYHAPROXY_DISCOVER: docker 54 | HAPROXY_CUSTOMERRORS: "true" 55 | HAPROXY_USERNAME: admin 56 | HAPROXY_PASSWORD: password 57 | HAPROXY_STATS_PORT: 1936 58 | ports: 59 | - 19901:19901 60 | 61 | 62 | nginx: 63 | #image: nginx 64 | image: stenote/nginx-hostname 65 | deploy: 66 | replicas: 2 67 | labels: 68 | easyhaproxy.http.redirect: '{"google.helloworld.com": "www.google.com"}' 69 | easyhaproxy.http.host: www.helloworld.com 70 | easyhaproxy.http.port: 19901 71 | easyhaproxy.http.localport: 80 72 | 73 | -------------------------------------------------------------------------------- /examples/swarm/easyhaproxy.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: EasyHAProxy for Docker Swarm 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - EasyHAProxy running in Swarm mode with service discovery 7 | # - HAProxy stats interface 8 | # - Let's Encrypt/Certbot support 9 | # - Shared overlay network for services 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Initialize Docker Swarm (if not already initialized) 14 | # docker swarm init 15 | # 16 | # # Create overlay network (idempotent) 17 | # docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy 18 | # 19 | # # Edit this file and change: 20 | # # Line 18: EASYHAPROXY_CERTBOT_EMAIL to your email 21 | # ``` 22 | # 23 | # HOW TO START: 24 | # ```bash 25 | # docker stack deploy -c easyhaproxy.yml easyhaproxy 26 | # ``` 27 | # 28 | # HOW TO VERIFY IT'S WORKING: 29 | # ```bash 30 | # # Check stack is deployed 31 | # docker stack ls 32 | # # Expected: easyhaproxy stack listed 33 | # 34 | # # Check service is running 35 | # docker service ls 36 | # # Expected: easyhaproxy_haproxy with 1/1 replicas 37 | # 38 | # # View HAProxy stats 39 | # # URL: http://localhost:1936 40 | # # Username: admin 41 | # # Password: password 42 | # 43 | # # Check logs 44 | # docker service logs -f easyhaproxy_haproxy 45 | # ``` 46 | # 47 | # CLEAN UP: 48 | # ```bash 49 | # docker stack rm easyhaproxy 50 | # ``` 51 | # 52 | # ============================================================================== 53 | 54 | 55 | services: 56 | haproxy: 57 | image: byjg/easy-haproxy:5.0.0 58 | volumes: 59 | - /var/run/docker.sock:/var/run/docker.sock 60 | - ./certs:/certs/haproxy 61 | - certs_certbot:/certs/certbot 62 | deploy: 63 | replicas: 1 64 | environment: 65 | EASYHAPROXY_DISCOVER: swarm 66 | EASYHAPROXY_SSL_MODE: "loose" 67 | EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org 68 | HAPROXY_CUSTOMERRORS: "true" 69 | HAPROXY_USERNAME: admin 70 | HAPROXY_PASSWORD: password 71 | HAPROXY_STATS_PORT: 1936 72 | ports: 73 | - "80:80/tcp" 74 | - "443:443/tcp" 75 | - "1936:1936/tcp" 76 | networks: 77 | - easyhaproxy 78 | 79 | networks: 80 | easyhaproxy: 81 | external: true 82 | 83 | volumes: 84 | certs_certbot: 85 | # external: true 86 | # certs_haproxy: 87 | # external: true -------------------------------------------------------------------------------- /examples/swarm/README.md: -------------------------------------------------------------------------------- 1 | # Docker Swarm Examples 2 | 3 | Self-contained examples for EasyHAProxy in Docker Swarm mode. **All documentation is in the YAML files as header comments.** 4 | 5 | ## Quick Start 6 | 7 | 1. Pick an example below 8 | 2. Open the YAML file 9 | 3. Read the header comments for complete instructions 10 | 4. Run the commands step-by-step 11 | 12 | ## Prerequisites 13 | 14 | All examples require: 15 | - Docker Swarm initialized (`docker swarm init`) 16 | - Overlay network created (`docker network create --driver overlay --attachable easyhaproxy`) 17 | - EasyHAProxy deployed (`docker stack deploy -c easyhaproxy.yml easyhaproxy`) 18 | 19 | See header comments in each file for detailed setup instructions. 20 | 21 | ## Basic Examples 22 | 23 | | File | Description | 24 | |------|-------------| 25 | | [easyhaproxy.yml](easyhaproxy.yml) | EasyHAProxy service for Swarm with stats and certbot | 26 | | [services.yml](services.yml) | Basic services with SSL (embedded cert and file-based) | 27 | | [portainer.yml](portainer.yml) | Portainer management UI behind EasyHAProxy | 28 | 29 | ## Plugin Examples 30 | 31 | | File | Description | 32 | |------|-------------| 33 | | [jwt-validator.yml](jwt-validator.yml) | JWT token validation for API protection | 34 | | [ip-whitelist.yml](ip-whitelist.yml) | IP whitelist for admin panels or sensitive services | 35 | | [cloudflare.yml](cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN | 36 | | [plugins-combined.yml](plugins-combined.yml) | Multiple plugins combined for layered security | 37 | 38 | ## Documentation Structure 39 | 40 | Each YAML file contains: 41 | - **WHAT THIS DEMONSTRATES** - Key features and concepts 42 | - **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times) 43 | - **HOW TO START** - Command to deploy the stack 44 | - **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs 45 | - **CLEAN UP** - Commands to remove resources 46 | 47 | ## Important: Service Labels 48 | 49 | In Swarm mode, labels must be under `deploy.labels`, NOT top-level `labels`: 50 | 51 | ```yaml 52 | # ✅ CORRECT - Service labels 53 | deploy: 54 | labels: 55 | easyhaproxy.http.host: example.com 56 | 57 | # ❌ WRONG - Container labels (ignored in Swarm) 58 | labels: 59 | easyhaproxy.http.host: example.com 60 | ``` 61 | 62 | ## Additional Documentation 63 | 64 | - [Docker Swarm Guide](../../docs/swarm.md) 65 | - [Container Labels Reference](../../docs/container-labels.md) 66 | - [Using Plugins](../../docs/plugins/) 67 | - [ACME/Let's Encrypt](../../docs/acme.md) 68 | -------------------------------------------------------------------------------- /src/tests/expected/services-fcgi.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | 25 | 26 | 27 | frontend http_in_80 28 | bind *:80 29 | mode http 30 | 31 | acl is_rule_phpapp_local_80_1 hdr(host) -i phpapp.local 32 | acl is_rule_phpapp_local_80_2 hdr(host) -i phpapp.local:80 33 | use_backend srv_phpapp_local_80 if is_rule_phpapp_local_80_1 OR is_rule_phpapp_local_80_2 34 | 35 | acl is_rule_phpapp-tcp_local_80_1 hdr(host) -i phpapp-tcp.local 36 | acl is_rule_phpapp-tcp_local_80_2 hdr(host) -i phpapp-tcp.local:80 37 | use_backend srv_phpapp-tcp_local_80 if is_rule_phpapp-tcp_local_80_1 OR is_rule_phpapp-tcp_local_80_2 38 | 39 | backend srv_phpapp_local_80 40 | balance roundrobin 41 | mode http 42 | option forwardfor 43 | http-request set-header X-Forwarded-Port %[dst_port] 44 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 45 | server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi 46 | backend srv_phpapp-tcp_local_80 47 | balance roundrobin 48 | mode http 49 | option forwardfor 50 | http-request set-header X-Forwarded-Port %[dst_port] 51 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 52 | server srv-0 172.17.0.3:9000 check weight 1 proto fcgi 53 | 54 | backend certbot_backend 55 | mode http 56 | server certbot 127.0.0.1:2080 57 | -------------------------------------------------------------------------------- /examples/static/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Static Configuration Mode 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - EasyHAProxy using static YAML configuration (no service discovery) 7 | # - Useful for non-containerized backends, VMs, or bare metal servers 8 | # - Configuration via /etc/haproxy/static/config.yml 9 | # 10 | # REQUIREMENTS (run these first): 11 | # ```bash 12 | # # Generate SSL certificates 13 | # cd ../.. && ./examples/generate-keys.sh && cd examples/static 14 | # 15 | # # Add to /etc/hosts (idempotent) 16 | # grep -q "host1.local" /etc/hosts || echo "127.0.0.1 host1.local" | sudo tee -a /etc/hosts 17 | # 18 | # # Copy a configuration file (choose one): 19 | # cp conf/config-basic.yml conf/config.yml # Basic HTTP→HTTPS redirect 20 | # # OR 21 | # cp conf/config-certbot.yml conf/config.yml # Let's Encrypt (requires public domain) 22 | # # OR 23 | # cp conf/config-deny-pages.yml conf/config.yml # Block specific paths 24 | # # OR 25 | # cp conf/config-jwt-validator.yml conf/config.yml # JWT authentication 26 | # ``` 27 | # 28 | # HOW TO START: 29 | # ```bash 30 | # # Start backend container 31 | # docker run -d --name container -p 8080:8080 byjg/static-httpserver 32 | # 33 | # # Start EasyHAProxy 34 | # docker compose up -d 35 | # ``` 36 | # 37 | # HOW TO VERIFY IT'S WORKING: 38 | # ```bash 39 | # # Test HTTPS 40 | # curl -k -H "Host: host1.local" https://127.0.0.1/ 41 | # # Expected: 200 OK with "Hello from Static HTTP Server!" 42 | # 43 | # # Test HTTP redirect (if using basic config) 44 | # curl -I -H "Host: host1.local" http://127.0.0.1 45 | # # Expected: HTTP/1.1 301 Moved Permanently 46 | # 47 | # # View HAProxy stats 48 | # # URL: http://localhost:1936 49 | # # Username: admin 50 | # # Password: password 51 | # ``` 52 | # 53 | # CLEAN UP: 54 | # ```bash 55 | # docker compose down 56 | # docker stop container && docker rm container 57 | # ``` 58 | # 59 | # ============================================================================== 60 | 61 | services: 62 | haproxy: 63 | image: byjg/easy-haproxy:5.0.0 64 | volumes: 65 | - ./conf/:/etc/haproxy/static/ 66 | - ./host1.local.pem:/certs/haproxy/host1.local.pem 67 | - /var/run/docker.sock:/var/run/docker.sock 68 | environment: 69 | EASYHAPROXY_DISCOVER: static 70 | ports: 71 | - "80:80/tcp" 72 | - "443:443/tcp" 73 | - "1936:1936/tcp" 74 | 75 | container: 76 | image: byjg/static-httpserver 77 | -------------------------------------------------------------------------------- /docs/ssl.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 9 3 | --- 4 | 5 | # Setup custom certificates 6 | 7 | You can use your own certificates with EasyHAProxy. You just need to let EasyHAProxy know that certificate. 8 | 9 | There are two ways to do that. 10 | 11 | - [Setup certificate as a label definition in docker container](#setup-certificate-as-a-label-definition-in-docker-container) 12 | - [Map the certificate as a docker volume](#map-the-certificate-as-a-docker-volume) 13 | 14 | ## Setup certificate as a label definition in docker container 15 | 16 | 1. Create a single PEM from the certificate and key. 17 | 18 | ```bash title="Combine certificate and key" 19 | cat example.com.crt example.com.key > single.pem 20 | 21 | cat single.pem 22 | 23 | -----BEGIN CERTIFICATE----- 24 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+ 25 | U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI= 26 | -----END CERTIFICATE----- 27 | 28 | -----BEGIN PRIVATE KEY----- 29 | MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL 30 | 3j4wz8/I5fdsk090j4s5KA== 31 | -----END PRIVATE KEY----- 32 | ``` 33 | 34 | 2. Convert the `single.pem` to BASE64 in a single line: 35 | 36 | ```bash title="Convert to BASE64" 37 | cat single.pem | base64 -w0 38 | ``` 39 | 40 | 3. Define a label in yout container 41 | 42 | Add the Base64 string you generated before to the label `easyhaproxy.[definition].sslcert` 43 | 44 | ## Map the certificate as a docker volume 45 | 46 | EasyHAProxy stores the certificates inside the container folder `/certs/haproxy`. 47 | 48 | 1. Run EasyHAProxy with the volume for the certificates: 49 | 50 | ```bash title="Create and mount certificate volume" 51 | docker volume create certs_haproxy 52 | 53 | docker run \ 54 | /* other parameters */ 55 | -v certs_haproxy:/certs/haproxy \ 56 | -d byjg/easy-haproxy 57 | ``` 58 | 59 | 2. Create a single PEM from the certificate and the key. 60 | 61 | ```bash title="Combine certificate and key" 62 | cat example.com.crt example.com.key > single.pem 63 | 64 | cat single.pem 65 | 66 | -----BEGIN CERTIFICATE----- 67 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZheHqmBnEJP+ 68 | U9r1gxYWKLzdqrMrcxtQN6M1hIH9n0peuJeIrybdcV7sMbStMXI= 69 | -----END CERTIFICATE----- 70 | 71 | -----BEGIN PRIVATE KEY----- 72 | MIIEojCCA4qgAwIBAgIUegW2BimwuL4RzRZ2WYkHA6U5nkAwDQYJKoZIhvcNAQEL 73 | 3j4wz8/I5fdsk090j4s5KA== 74 | -----END PRIVATE KEY----- 75 | ``` 76 | 77 | 3. Copy this certificate to EasyHAProxy volume: 78 | 79 | ```bash title="Copy certificate to container" 80 | docker cp single.pem easyhaproxy:/certs/haproxy 81 | ``` 82 | 83 | ---- 84 | [Open source ByJG](http://opensource.byjg.com) 85 | -------------------------------------------------------------------------------- /examples/static/conf/config-certbot.yml: -------------------------------------------------------------------------------- 1 | # Certbot/Let's Encrypt Configuration Example 2 | # 3 | # Demonstrates: 4 | # - Automatic SSL certificate generation with Let's Encrypt 5 | # - HTTP to HTTPS redirect 6 | # - Certificate renewal 7 | # 8 | # Prerequisites: 9 | # 1. Public IP address with ports 80 and 443 accessible 10 | # 2. DNS records pointing to your server: 11 | # example.com -> your-server-ip 12 | # www.example.com -> your-server-ip 13 | # 14 | # 3. Set environment variable: 15 | # EASYHAPROXY_CERTBOT_EMAIL=your-email@example.com 16 | # 17 | # 4. Mount this config: 18 | # -v ./conf/config-certbot.yml:/etc/haproxy/static/config.yml 19 | # 20 | # 5. Persist certificates: 21 | # -v ./certs/certbot:/certs/certbot 22 | # 23 | # How it works: 24 | # - EasyHAProxy requests certificates from Let's Encrypt via HTTP-01 challenge 25 | # - Certificates are stored in /certs/certbot/ 26 | # - Certificates auto-renew when needed 27 | # 28 | # Note: Let's Encrypt has rate limits. Use staging environment for testing: 29 | # EASYHAPROXY_CERTBOT_AUTOCONFIG=staging 30 | 31 | stats: 32 | username: admin 33 | password: password 34 | port: 1936 35 | 36 | customerrors: true 37 | 38 | easymapping: 39 | # HTTP Port 80 40 | # Required for ACME HTTP-01 challenge and redirect 41 | - port: 80 42 | hosts: 43 | # Domain with certbot enabled 44 | example.com: 45 | containers: 46 | - webapp:8080 47 | # Enable certbot for this domain 48 | certbot: true 49 | # Redirect HTTP to HTTPS after cert is issued 50 | redirect_ssl: true 51 | 52 | # Additional domain with certbot 53 | app.example.com: 54 | containers: 55 | - app:3000 56 | certbot: true 57 | redirect_ssl: true 58 | 59 | # Domain without certbot (uses custom certificate) 60 | custom.example.com: 61 | containers: 62 | - custom-app:8080 63 | # No certbot - expects certificate at /certs/haproxy/custom.example.com.pem 64 | 65 | # HTTPS Port 443 66 | # Serves HTTPS traffic with auto-generated certificates 67 | - port: 443 68 | ssl: true 69 | hosts: 70 | example.com: 71 | containers: 72 | - webapp:8080 73 | # Certificate path (auto-generated by certbot) 74 | # /certs/certbot/example.com/fullchain.pem 75 | 76 | app.example.com: 77 | containers: 78 | - app:3000 79 | 80 | # Custom certificate example 81 | custom.example.com: 82 | containers: 83 | - custom-app:8080 84 | # Place your certificate at: 85 | # /certs/haproxy/custom.example.com.pem 86 | 87 | # Multiple domains with different backends 88 | # Certbot will request separate certificates for each domain 89 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Docker 6 | 7 | ## Setup Docker EasyHAProxy 8 | 9 | This method involves using a standalone Docker installation to discover containers 10 | and configure HAProxy. 11 | 12 | EasyHAProxy inspects Docker containers and retrieves labels to configure HAProxy. 13 | Once it identifies a container with at least the label 'easyhaproxy.http.host,' 14 | it configures HAProxy to redirect traffic to that container. 15 | To accomplish this, EasyHAProxy may need to attach the same network to its container. 16 | 17 | It's recommended to create a network external to EasyHAProxy, although it's not mandatory. 18 | 19 | :::warning Limitations 20 | - You cannot mix Docker containers with Swarm containers. 21 | - This method does not work with containers that use the `--network=host` option. See [limitations](limitations.md) for details. 22 | ::: 23 | 24 | For example: 25 | 26 | ```bash title="Create EasyHAProxy network" 27 | docker network create easyhaproxy 28 | ``` 29 | 30 | And then run the EasyHAProxy: 31 | 32 | ```bash title="Run EasyHAProxy container" 33 | docker run -d \ 34 | --name easy-haproxy-container \ 35 | -v /var/run/docker.sock:/var/run/docker.sock \ 36 | -e EASYHAPROXY_DISCOVER="docker" \ 37 | # + Environment Variables \ 38 | -p 80:80 \ 39 | -p 443:443 \ 40 | -p 1936:1936 \ 41 | --network easyhaproxy 42 | byjg/easy-haproxy 43 | ``` 44 | 45 | Mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; 46 | 47 | ## Running containers 48 | 49 | To make your containers "discoverable" by EasyHAProxy, this is the minimum configuration you need: 50 | 51 | ```bash title="Run container with EasyHAProxy labels" 52 | docker run -d \ 53 | --label easyhaproxy.http.host=example.org \ 54 | --label easyhaproxy.http.port=80 \ 55 | --label easyhaproxy.http.localport=8080 \ 56 | --network easyhaproxy 57 | my/image:tag 58 | ``` 59 | 60 | Once the container is running, EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. 61 | 62 | You don't need to expose any port in your container. 63 | 64 | Please follow the [docker label configuration](container-labels.md) to see other configurations available. 65 | 66 | ## Setup the EasyHAProxy container 67 | 68 | You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [environment variable guide](environment-variable.md) 69 | 70 | ## Setup certificates with ACME (e.g. Letsencrypt) 71 | 72 | Follow [this link](acme.md) 73 | 74 | ## Setup your own certificates 75 | 76 | Follow [this link](ssl.md) 77 | 78 | ---- 79 | [Open source ByJG](http://opensource.byjg.com) 80 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-php-fpm.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: FastCGI Plugin with PHP-FPM 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - PHP-FPM configuration with FastCGI protocol (proto: fcgi) 7 | # - FastCGI plugin for PHP environment variable configuration 8 | # - TCP connection to PHP-FPM on port 9000 9 | # - PATH_INFO support for RESTful routing 10 | # - Custom document root and index file configuration 11 | # 12 | # REQUIREMENTS (run these first): 13 | # ```bash 14 | # # Add to /etc/hosts (idempotent) 15 | # grep -q "phpapp.local" /etc/hosts || echo "127.0.0.1 phpapp.local" | sudo tee -a /etc/hosts 16 | # ``` 17 | # 18 | # HOW TO START: 19 | # ```bash 20 | # docker compose -f docker-compose-php-fpm.yml up -d 21 | # ``` 22 | # 23 | # HOW TO VERIFY IT'S WORKING: 24 | # ```bash 25 | # # Test main page 26 | # curl http://phpapp.local/ 27 | # # Expected: 200 OK with PHP environment info 28 | # 29 | # # Test PHP info page 30 | # curl http://phpapp.local/info.php 31 | # # Expected: phpinfo() output 32 | # 33 | # # Test PATH_INFO routing 34 | # curl http://phpapp.local/test-path-info.php/users/123 35 | # # Expected: PATH_INFO=/users/123 36 | # 37 | # # View HAProxy stats 38 | # # URL: http://localhost:1936 39 | # # Username: admin 40 | # # Password: password 41 | # ``` 42 | # 43 | # CLEAN UP: 44 | # ```bash 45 | # docker compose -f docker-compose-php-fpm.yml down 46 | # ``` 47 | # 48 | # ============================================================================== 49 | 50 | services: 51 | haproxy: 52 | image: byjg/easy-haproxy:5.0.0 53 | volumes: 54 | - /var/run/docker.sock:/var/run/docker.sock 55 | environment: 56 | EASYHAPROXY_DISCOVER: docker 57 | HAPROXY_CUSTOMERRORS: "true" 58 | HAPROXY_USERNAME: admin 59 | HAPROXY_PASSWORD: password 60 | HAPROXY_STATS_PORT: 1936 61 | ports: 62 | - "80:80/tcp" 63 | - "1936:1936/tcp" 64 | 65 | # PHP-FPM service using byjg/php image 66 | php-fpm: 67 | image: byjg/php:8.5-fpm 68 | volumes: 69 | # Mount PHP application files 70 | - ./php-app:/var/www/html:ro 71 | labels: 72 | easyhaproxy.http.host: phpapp.local 73 | easyhaproxy.http.port: 80 74 | # PHP-FPM listens on port 9000 75 | easyhaproxy.http.localport: 9000 76 | easyhaproxy.http.proto: fcgi 77 | 78 | # Enable FastCGI plugin for PHP environment configuration 79 | easyhaproxy.http.plugins: fastcgi 80 | 81 | # FastCGI plugin configuration 82 | easyhaproxy.http.plugin.fastcgi.document_root: /var/www/html 83 | easyhaproxy.http.plugin.fastcgi.index_file: index.php 84 | easyhaproxy.http.plugin.fastcgi.path_info: "true" 85 | -------------------------------------------------------------------------------- /examples/static/conf/config-jwt-validator.yml: -------------------------------------------------------------------------------- 1 | # JWT Validator Plugin Configuration Example 2 | # 3 | # Demonstrates: 4 | # - JWT token validation for API protection 5 | # - Different JWT configurations per domain 6 | # - Optional issuer/audience validation 7 | # 8 | # Prerequisites: 9 | # 1. Generate RSA key pair: 10 | # openssl genrsa -out jwt_private.pem 2048 11 | # openssl rsa -in jwt_private.pem -pubout -out jwt_pubkey.pem 12 | # 13 | # 2. Mount public keys: 14 | # -v ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro 15 | # -v ./jwt_pubkey2.pem:/etc/haproxy/jwt_keys/admin_pubkey.pem:ro 16 | # 17 | # 3. Mount this config: 18 | # -v ./conf/config-jwt-validator.yml:/etc/haproxy/static/config.yml 19 | # 20 | # 4. Test: 21 | # # Without token - should fail 22 | # curl http://api.local/users 23 | # # Response: Missing Authorization HTTP header 24 | # 25 | # # With valid token - should succeed 26 | # curl -H "Authorization: Bearer eyJhbGc..." http://api.local/users 27 | 28 | stats: 29 | username: admin 30 | password: password 31 | port: 1936 32 | 33 | customerrors: true 34 | 35 | easymapping: 36 | - port: 80 37 | hosts: 38 | # Public API with full JWT validation 39 | api.local: 40 | containers: 41 | - api-server:8080 42 | plugins: 43 | - jwt_validator 44 | plugin_config: 45 | jwt_validator: 46 | algorithm: RS256 47 | issuer: https://auth.example.com/ 48 | audience: https://api.example.com 49 | pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem 50 | 51 | # Internal API - validate signature only (no issuer/audience check) 52 | internal-api.local: 53 | containers: 54 | - internal-api:3000 55 | plugins: 56 | - jwt_validator 57 | plugin_config: 58 | jwt_validator: 59 | algorithm: RS256 60 | # No issuer/audience = skip those validations 61 | pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem 62 | 63 | # Admin API - different issuer and key 64 | admin-api.local: 65 | containers: 66 | - admin-api:4000 67 | plugins: 68 | - jwt_validator 69 | - deny_pages # Also block internal paths 70 | plugin_config: 71 | jwt_validator: 72 | algorithm: RS256 73 | issuer: https://admin-auth.example.com/ 74 | audience: https://admin.example.com 75 | pubkey_path: /etc/haproxy/jwt_keys/admin_pubkey.pem 76 | deny_pages: 77 | paths: 78 | - /internal 79 | - /debug 80 | status_code: 403 81 | 82 | # Public website - no JWT required 83 | website.local: 84 | containers: 85 | - website:8080 86 | # No plugins = public access 87 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-ip-whitelist.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: IP Whitelist Plugin 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restricting access to specific IP addresses or CIDR ranges 7 | # - Single IP, CIDR notation, and multiple IP support 8 | # - Custom HTTP status code for blocked requests 9 | # - Admin panel or sensitive application protection 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Add to /etc/hosts (idempotent) 14 | # grep -q "admin.local" /etc/hosts || echo "127.0.0.1 admin.local" | sudo tee -a /etc/hosts 15 | # 16 | # # IMPORTANT: Update the allowed_ips in this file (line 52) with your actual IPs! 17 | # # Default allows localhost and private networks for testing 18 | # ``` 19 | # 20 | # HOW TO START: 21 | # ```bash 22 | # docker compose -f docker-compose-ip-whitelist.yml up -d 23 | # ``` 24 | # 25 | # HOW TO VERIFY IT'S WORKING: 26 | # ```bash 27 | # # Test from localhost (127.0.0.1 is whitelisted) 28 | # curl http://admin.local/ 29 | # # Expected: 200 OK - Access granted 30 | # 31 | # # Test from non-whitelisted IP 32 | # # You'll need to test from another machine or temporarily remove your IP 33 | # # from the allowed_ips list to see the 403 Forbidden response 34 | # 35 | # # View HAProxy stats to see blocked requests 36 | # # URL: http://localhost:1936 37 | # # Username: admin 38 | # # Password: password 39 | # ``` 40 | # 41 | # CLEAN UP: 42 | # ```bash 43 | # docker compose -f docker-compose-ip-whitelist.yml down 44 | # ``` 45 | # 46 | # ============================================================================== 47 | 48 | services: 49 | haproxy: 50 | image: byjg/easy-haproxy:5.0.0 51 | volumes: 52 | - /var/run/docker.sock:/var/run/docker.sock 53 | environment: 54 | EASYHAPROXY_DISCOVER: docker 55 | HAPROXY_CUSTOMERRORS: "true" 56 | HAPROXY_USERNAME: admin 57 | HAPROXY_PASSWORD: password 58 | HAPROXY_STATS_PORT: 1936 59 | ports: 60 | - "80:80/tcp" 61 | - "1936:1936/tcp" 62 | 63 | # Admin panel with IP whitelist 64 | admin: 65 | image: byjg/static-httpserver 66 | environment: 67 | TITLE: "Admin Panel - IP Restricted" 68 | labels: 69 | easyhaproxy.http.host: admin.local 70 | easyhaproxy.http.port: 80 71 | easyhaproxy.http.localport: 8080 72 | 73 | # Enable IP whitelist plugin 74 | easyhaproxy.http.plugins: ip_whitelist 75 | 76 | # Allow localhost and private networks 77 | # UPDATE THIS with your actual IPs/networks! 78 | 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 79 | 80 | # Status code to return for blocked IPs 81 | easyhaproxy.http.plugin.ip_whitelist.status_code: 403 82 | -------------------------------------------------------------------------------- /src/tests/test_daemonize.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | 5 | from functions import DaemonizeHAProxy, Functions 6 | 7 | 8 | def test_daemonize_haproxy(): 9 | daemon = DaemonizeHAProxy() 10 | assert daemon is not None 11 | 12 | def test_daemonize_haproxy_check_config(): 13 | daemon = DaemonizeHAProxy() 14 | filed = daemon.get_custom_config_files() 15 | assert filed == {} 16 | 17 | def test_daemonize_haproxy_get_haproxy_command_start(): 18 | daemon = DaemonizeHAProxy() 19 | command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) 20 | assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock" 21 | 22 | def test_daemonize_haproxy_get_haproxy_command_reload_nopid(): 23 | daemon = DaemonizeHAProxy() 24 | command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD) 25 | assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -S /var/run/haproxy.sock" 26 | 27 | def test_daemonize_haproxy_get_haproxy_command_reload_pidinvalid(): 28 | daemon = DaemonizeHAProxy() 29 | try: 30 | with open("/tmp/temp.pid", 'w') as file: 31 | file.write("-1001") 32 | command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") 33 | assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /tmp/temp.pid -S /var/run/haproxy.sock" 34 | finally: 35 | assert not os.path.exists("/tmp/temp.pid") 36 | 37 | def test_daemonize_haproxy_get_haproxy_command_reload_existing_pin(): 38 | daemon = DaemonizeHAProxy() 39 | try: 40 | with open("/tmp/temp.pid", 'w') as file: 41 | file.write("1") 42 | command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_RELOAD, "/tmp/temp.pid") 43 | assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /tmp/temp.pid -x /var/run/haproxy.sock -sf 1" 44 | finally: 45 | assert os.path.exists("/tmp/temp.pid") 46 | os.unlink("/tmp/temp.pid") 47 | 48 | def test_daemonize_haproxy2_check_config(): 49 | daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures') 50 | filed = daemon.get_custom_config_files() 51 | assert filed == { 52 | os.path.dirname(__file__) + "/fixtures/00_haproxy.cfg": os.path.getmtime(os.path.dirname(__file__) + "/fixtures/00_haproxy.cfg"), 53 | os.path.dirname(__file__) + "/fixtures/10_haproxy.cfg": os.path.getmtime(os.path.dirname(__file__) + "/fixtures/10_haproxy.cfg") 54 | } 55 | 56 | def test_daemonize_haproxy2_get_haproxy_command_start(): 57 | daemon = DaemonizeHAProxy(os.path.abspath(os.path.dirname(__file__)) + '/fixtures') 58 | command = daemon.get_haproxy_command(DaemonizeHAProxy.HAPROXY_START) 59 | assert command == "/usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -f %s -p /run/haproxy.pid -S /var/run/haproxy.sock" % (os.path.dirname(__file__) + "/fixtures") 60 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-cloudflare.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Cloudflare IP Restoration Plugin 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restoring original visitor IPs when behind Cloudflare CDN 7 | # - Detecting requests from Cloudflare IP ranges 8 | # - Using CF-Connecting-IP header for real client IP 9 | # - Accurate IP logging for applications behind Cloudflare 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Download Cloudflare IP ranges (idempotent - overwrites if exists) 14 | # curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst 15 | # echo "" >> cloudflare_ips.lst 16 | # curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst 17 | # 18 | # # Add to /etc/hosts (idempotent) 19 | # grep -q "myapp.local" /etc/hosts || echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts 20 | # ``` 21 | # 22 | # HOW TO START: 23 | # ```bash 24 | # docker compose -f docker-compose-cloudflare.yml up -d 25 | # ``` 26 | # 27 | # HOW TO VERIFY IT'S WORKING: 28 | # ```bash 29 | # # Test normal request 30 | # curl -H "Host: myapp.local" http://127.0.0.1/ 31 | # # Expected: 200 OK 32 | # 33 | # # Test with CF-Connecting-IP header (simulating Cloudflare) 34 | # curl -H "Host: myapp.local" -H "CF-Connecting-IP: 203.0.113.50" http://127.0.0.1/ 35 | # # Expected: 200 OK (backend sees 203.0.113.50 as client IP) 36 | # 37 | # # Note: This plugin is most useful when your site is actually behind Cloudflare 38 | # # In production, requests come from Cloudflare IPs and the plugin restores real client IPs 39 | # ``` 40 | # 41 | # CLEAN UP: 42 | # ```bash 43 | # docker compose -f docker-compose-cloudflare.yml down 44 | # ``` 45 | # 46 | # ============================================================================== 47 | 48 | services: 49 | haproxy: 50 | image: byjg/easy-haproxy:5.0.0 51 | volumes: 52 | - /var/run/docker.sock:/var/run/docker.sock 53 | # Mount Cloudflare IP list 54 | - ./cloudflare_ips.lst:/etc/haproxy/cloudflare_ips.lst:ro 55 | environment: 56 | EASYHAPROXY_DISCOVER: docker 57 | HAPROXY_CUSTOMERRORS: "true" 58 | HAPROXY_USERNAME: admin 59 | HAPROXY_PASSWORD: password 60 | HAPROXY_STATS_PORT: 1936 61 | ports: 62 | - "80:80/tcp" 63 | - "1936:1936/tcp" 64 | 65 | # Web application behind Cloudflare 66 | webapp: 67 | image: byjg/static-httpserver 68 | environment: 69 | TITLE: "App Behind Cloudflare" 70 | labels: 71 | easyhaproxy.http.host: myapp.local 72 | easyhaproxy.http.port: 80 73 | easyhaproxy.http.localport: 8080 74 | 75 | # Enable Cloudflare plugin 76 | easyhaproxy.http.plugins: cloudflare 77 | 78 | # Use custom IP list (disable built-in IPs) 79 | easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false 80 | easyhaproxy.http.plugin.cloudflare.ip_list_path: /etc/haproxy/cloudflare_ips.lst 81 | -------------------------------------------------------------------------------- /docs/Plugins/cleanup.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 21 3 | --- 4 | 5 | # Cleanup Plugin 6 | 7 | **Type:** Global Plugin 8 | **Runs:** Once per discovery cycle 9 | 10 | ## Overview 11 | 12 | The Cleanup plugin performs cleanup tasks during each discovery cycle, such as removing old temporary files. 13 | 14 | ## Why Use It 15 | 16 | Prevents disk space issues by automatically cleaning up temporary files created by EasyHAProxy. 17 | 18 | ## Configuration Options 19 | 20 | | Option | Description | Default | 21 | |----------------------|----------------------------------------------|---------| 22 | | `enabled` | Enable/disable plugin | `true` | 23 | | `max_idle_time` | Maximum age in seconds before deleting files | `300` | 24 | | `cleanup_temp_files` | Enable temp file cleanup | `true` | 25 | 26 | ## Configuration Examples 27 | 28 | ### Static YAML Configuration 29 | 30 | ```yaml 31 | # /etc/haproxy/static/config.yaml 32 | plugins: 33 | enabled: [cleanup] 34 | config: 35 | cleanup: 36 | max_idle_time: 600 37 | cleanup_temp_files: true 38 | ``` 39 | 40 | ### Environment Variables 41 | 42 | Configure the Cleanup plugin globally: 43 | 44 | | Environment Variable | Config Key | Type | Default | Description | 45 | |-------------------------------------------------|----------------------|----------|---------|----------------------------------------------| 46 | | `EASYHAPROXY_PLUGINS_ENABLED` | - | string | - | Enable cleanup plugin (value: `cleanup`) | 47 | | `EASYHAPROXY_PLUGIN_CLEANUP_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin | 48 | | `EASYHAPROXY_PLUGIN_CLEANUP_MAX_IDLE_TIME` | `max_idle_time` | integer | `300` | Maximum age in seconds before deleting files | 49 | | `EASYHAPROXY_PLUGIN_CLEANUP_CLEANUP_TEMP_FILES` | `cleanup_temp_files` | boolean | `true` | Enable temp file cleanup | 50 | 51 | **Note:** This is a global plugin - configuration applies to the entire system. 52 | 53 | ### Custom Idle Time (1 hour) 54 | 55 | ```yaml 56 | # /etc/haproxy/static/config.yaml 57 | plugins: 58 | enabled: [cleanup] 59 | config: 60 | cleanup: 61 | enabled: true 62 | max_idle_time: 3600 # 1 hour 63 | ``` 64 | 65 | ## How It Works 66 | 67 | The cleanup plugin: 68 | - Runs once during each discovery cycle 69 | - Scans temporary directories for old files 70 | - Removes files older than `max_idle_time` seconds 71 | - Helps maintain disk space efficiency 72 | 73 | ## Important Notes 74 | 75 | - This is a **global plugin** - it runs once per discovery cycle, not per domain 76 | - Does not generate HAProxy configuration 77 | - Performs maintenance operations in the background 78 | - Safe to enable in production environments 79 | 80 | ## Related Documentation 81 | 82 | - [Plugin System Overview](../plugins.md) 83 | - [Environment Variables Reference](../environment-variable.md) 84 | - [Static Configuration Reference](../static.md) 85 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-jwt-validator.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: JWT Validator Plugin 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - JWT token validation for API protection 7 | # - RS256 algorithm signature verification 8 | # - Issuer and audience validation 9 | # - Public key-based JWT verification 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Generate SSL certificates and JWT keys (from project root) 14 | # cd ../.. && ./examples/generate-keys.sh && cd examples/docker 15 | # 16 | # # Add to /etc/hosts (idempotent) 17 | # grep -q "api.local" /etc/hosts || echo "127.0.0.1 api.local" | sudo tee -a /etc/hosts 18 | # ``` 19 | # 20 | # HOW TO START: 21 | # ```bash 22 | # docker compose -f docker-compose-jwt-validator.yml up -d 23 | # ``` 24 | # 25 | # HOW TO VERIFY IT'S WORKING: 26 | # ```bash 27 | # # Test without token (should fail) 28 | # curl http://api.local/ 29 | # # Expected: HTTP 403 - Missing Authorization HTTP header 30 | # 31 | # # Generate test JWT at https://jwt.io with: 32 | # # - Algorithm: RS256 33 | # # - Payload: {"iss":"https://auth.example.com/","aud":"https://api.example.com","exp":9999999999} 34 | # # - Paste contents of jwt_private.pem in private key field 35 | # 36 | # # Test with valid token 37 | # TOKEN="eyJhbGc..." # Replace with your generated token 38 | # curl -H "Authorization: Bearer $TOKEN" http://api.local/ 39 | # # Expected: 200 OK with API response 40 | # 41 | # # View HAProxy stats 42 | # # URL: http://localhost:1936 43 | # # Username: admin 44 | # # Password: password 45 | # ``` 46 | # 47 | # CLEAN UP: 48 | # ```bash 49 | # docker compose -f docker-compose-jwt-validator.yml down 50 | # ``` 51 | # 52 | # ============================================================================== 53 | 54 | services: 55 | haproxy: 56 | image: byjg/easy-haproxy:5.0.0 57 | volumes: 58 | - /var/run/docker.sock:/var/run/docker.sock 59 | # Mount the public key for JWT verification 60 | - ./jwt_pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro 61 | environment: 62 | EASYHAPROXY_DISCOVER: docker 63 | HAPROXY_CUSTOMERRORS: "true" 64 | HAPROXY_USERNAME: admin 65 | HAPROXY_PASSWORD: password 66 | HAPROXY_STATS_PORT: 1936 67 | ports: 68 | - "80:80/tcp" 69 | - "1936:1936/tcp" 70 | 71 | # API service protected by JWT 72 | api: 73 | image: byjg/static-httpserver 74 | environment: 75 | TITLE: "Protected API - JWT Required" 76 | labels: 77 | easyhaproxy.http.host: api.local 78 | easyhaproxy.http.port: 80 79 | easyhaproxy.http.localport: 8080 80 | 81 | # Enable JWT validator plugin 82 | easyhaproxy.http.plugins: jwt_validator 83 | 84 | # JWT validator configuration 85 | easyhaproxy.http.plugin.jwt_validator.algorithm: RS256 86 | easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/ 87 | easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com 88 | easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem 89 | -------------------------------------------------------------------------------- /src/tests/fixtures/services-letsencrypt: -------------------------------------------------------------------------------- 1 | {"f5c645a0dfc6": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.certbot":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, 2 | "bbd4d1854155": {"com.docker.compose.config-hash":"3dc790bf2bea944359c75a40c45655bd868f1d85beb599d1ca797e8ea2c95ee4","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:0fd95b1512c207048ab3fcc74032354f38143fbb8235ac2a47da903c98a58205","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"haproxy","com.docker.compose.version":"2.8.0"}, 3 | "b63438410b6a": {"com.docker.compose.config-hash":"b95ebc27d0e61caa418cdfa632e05a656da9bbc3ea0d4603651971015f10a1f0","com.docker.compose.container-number":"2","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:bea3509d6fdc8d7f9ec95563a5a226dc977ee74fb3e980e0de70e892c2d38dde","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"nginx","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test.example.org","easyhaproxy.http.certbot":"true","easyhaproxy.http.localport":"80","easyhaproxy.http.port":"80","easyhaproxy.http.redirect":"{\"google.helloworld.com\": \"www.google.com\"}","easyhaproxy.http.redirect_ssl":"true"}, 4 | "83d57d592e26": {"com.docker.compose.config-hash":"8c5871144f1e8a3aeca037207c02f011ab2c6e6c311a3773602b63541762dab5","com.docker.compose.container-number":"1","com.docker.compose.depends_on":"","com.docker.compose.image":"sha256:c4232396c715f3d568816c666e6d9b4a68ef6c36f6243b4007c4ee1d8335fd65","com.docker.compose.oneoff":"False","com.docker.compose.project":"docker","com.docker.compose.project.config_files":"/workspace/docker-easy-haproxy/examples/docker/docker-compose-test.yml","com.docker.compose.project.working_dir":"/workspace/docker-easy-haproxy/examples/docker","com.docker.compose.service":"static","com.docker.compose.version":"2.8.0","easyhaproxy.http.host":"test2.example.org","easyhaproxy.http.localport":"8080","easyhaproxy.http.port":"80","io.buildah.version":"1.21.0"}} -------------------------------------------------------------------------------- /examples/docker/docker-compose-acme.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Let's Encrypt SSL with ACME/Certbot 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Automatic SSL certificate generation using Let's Encrypt 7 | # - HTTP-01 ACME challenge protocol 8 | # - Certificate persistence across container restarts 9 | # - Auto-renewal of certificates 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # You MUST have: 14 | # # - A public IP address pointing to your machine 15 | # # - Ports 80 and 443 open in your firewall 16 | # # - A valid domain name with DNS configured 17 | # 18 | # # Edit this file and change: 19 | # # - Line 21: EASYHAPROXY_CERTBOT_EMAIL to your email 20 | # # - Line 36: easyhaproxy.http.host to your real domain 21 | # 22 | # # Create certs directory 23 | # mkdir -p ./certs/certbot 24 | # ``` 25 | # 26 | # HOW TO START: 27 | # ```bash 28 | # docker compose -f docker-compose-acme.yml up -d 29 | # ``` 30 | # 31 | # HOW TO VERIFY IT'S WORKING: 32 | # ```bash 33 | # # Check logs for certificate issuance 34 | # docker compose -f docker-compose-acme.yml logs -f haproxy 35 | # # Look for: "Successfully received certificate" 36 | # 37 | # # Test HTTPS with real domain (replace test.xpto.us with your domain) 38 | # curl https://test.xpto.us/ 39 | # # Expected: 200 OK with valid SSL certificate 40 | # 41 | # # Verify certificate 42 | # openssl s_client -showcerts -connect test.xpto.us:443 < /dev/null | grep "Issuer:" 43 | # # Expected: Issuer: C = US, O = Let's Encrypt 44 | # 45 | # # Check certificate files 46 | # ls -la ./certs/certbot/ 47 | # # Expected: Your domain certificate files 48 | # ``` 49 | # 50 | # CLEAN UP: 51 | # ```bash 52 | # docker compose -f docker-compose-acme.yml down 53 | # # Keep certificates: 54 | # # docker compose -f docker-compose-acme.yml down 55 | # # Remove certificates too: 56 | # # docker compose -f docker-compose-acme.yml down && rm -rf ./certs/certbot 57 | # ``` 58 | # 59 | # ============================================================================== 60 | 61 | services: 62 | haproxy: 63 | image: byjg/easy-haproxy:5.0.0 64 | volumes: 65 | - /var/run/docker.sock:/var/run/docker.sock 66 | # Persist the CERTBOT to avoid re-challenge when the server restarts 67 | - ./certs/certbot:/certs/certbot 68 | environment: 69 | EASYHAPROXY_DISCOVER: docker 70 | HAPROXY_CUSTOMERRORS: "true" 71 | HAPROXY_USERNAME: admin 72 | HAPROXY_PASSWORD: password 73 | HAPROXY_STATS_PORT: 1936 74 | # SETUP THE EMAIL for CertBot 75 | EASYHAPROXY_CERTBOT_EMAIL: user@example.com 76 | # Let's encrypt don´t need AUTOCONFIG, just email. 77 | # If you want other, please refer to the documentation 78 | # EASYHAPROXY_CERTBOT_AUTOCONFIG: zerossl 79 | 80 | ports: 81 | - "80:80/tcp" 82 | - "443:443/tcp" 83 | - "1936:1936/tcp" 84 | 85 | container: 86 | image: byjg/static-httpserver 87 | labels: 88 | # Setup here the domain will have the SSL issued 89 | easyhaproxy.http.redirect_ssl: true 90 | easyhaproxy.http.host: test.xpto.us 91 | easyhaproxy.http.localport: 8080 92 | easyhaproxy.http.certbot: true 93 | 94 | -------------------------------------------------------------------------------- /examples/static/README.md: -------------------------------------------------------------------------------- 1 | # Static Configuration Example 2 | 3 | Self-contained example for EasyHAProxy using static YAML configuration. **All documentation is in the docker-compose file as header comments.** 4 | 5 | ## Quick Start 6 | 7 | 1. Open [docker-compose.yml](docker-compose.yml) 8 | 2. Read the header comments for complete instructions 9 | 3. Choose a configuration scenario from `conf/` directory 10 | 4. Run the commands step-by-step 11 | 12 | ## What is Static Mode? 13 | 14 | Static mode uses explicit YAML configuration files instead of dynamic service discovery. This is useful for: 15 | - **Non-containerized backends** - VMs, bare metal servers, external APIs 16 | - **Fixed infrastructure** - When your backend IPs/ports don't change 17 | - **Explicit routing control** - Precise control over HAProxy configuration 18 | 19 | ## Configuration Files 20 | 21 | All scenarios use `/etc/haproxy/static/config.yml` mounted from `./conf/config.yml`. 22 | 23 | Choose one of these pre-made configurations: 24 | 25 | | Configuration File | Description | 26 | |----------------------------|-------------------------------------------------| 27 | | `config-basic.yml` | Simple HTTP→HTTPS redirect with SSL termination | 28 | | `config-certbot.yml` | Let's Encrypt SSL (requires public domain) | 29 | | `config-deny-pages.yml` | Block specific paths (e.g., `/admin`, `/.env`) | 30 | | `config-jwt-validator.yml` | JWT token validation for API authentication | 31 | 32 | ## Prerequisites 33 | 34 | - SSL certificates generated (`./examples/generate-keys.sh`) 35 | - `/etc/hosts` entry for `host1.local` 36 | - Backend container running on port 8080 37 | 38 | See header comments in [docker-compose.yml](docker-compose.yml) for detailed setup. 39 | 40 | ## Documentation Structure 41 | 42 | The docker-compose.yml file contains: 43 | - **WHAT THIS DEMONSTRATES** - Key features and concepts 44 | - **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times) 45 | - **HOW TO START** - Commands to start backend and EasyHAProxy 46 | - **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs 47 | - **CLEAN UP** - Commands to stop and remove resources 48 | 49 | ## Example Workflow 50 | 51 | ```bash 52 | # 1. Generate certificates 53 | cd ../.. && ./examples/generate-keys.sh && cd examples/static 54 | 55 | # 2. Choose a configuration 56 | cp conf/config-basic.yml conf/config.yml 57 | 58 | # 3. Start backend 59 | docker run -d --name container -p 8080:8080 byjg/static-httpserver 60 | 61 | # 4. Start EasyHAProxy 62 | docker compose up -d 63 | 64 | # 5. Test 65 | curl -k https://host1.local/ 66 | 67 | # 6. Clean up 68 | docker compose down 69 | docker stop container && docker rm container 70 | ``` 71 | 72 | ## Configuration File Reference 73 | 74 | Basic structure of `config.yml`: 75 | 76 | ```yaml 77 | stats: 78 | username: admin 79 | password: password 80 | port: 1936 81 | 82 | easymapping: 83 | - port: 443 84 | ssl: true 85 | hosts: 86 | host1.local: 87 | containers: 88 | - container:8080 # Can also be IP:PORT for external backends 89 | ``` 90 | 91 | See `conf/` directory for complete examples. 92 | 93 | ## Additional Documentation 94 | 95 | - [Static Configuration Guide](../../docs/static.md) 96 | - [Using Plugins](../../docs/plugins/) 97 | - [Environment Variables](../../docs/environment-variable.md) 98 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from deepdiff import DeepDiff 4 | 5 | from functions import Functions, DaemonizeHAProxy, Certbot, Consts, loggerInit, loggerEasyHaproxy, loggerHaproxy, \ 6 | loggerCertbot 7 | from processor import ProcessorInterface 8 | 9 | 10 | def start(): 11 | processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER")) 12 | if processor_obj is None: 13 | exit(1) 14 | 15 | os.makedirs(Consts.certs_certbot, exist_ok=True) 16 | os.makedirs(Consts.certs_haproxy, exist_ok=True) 17 | 18 | processor_obj.save_config(Consts.haproxy_config) 19 | processor_obj.save_certs(Consts.certs_haproxy) 20 | certbot_certs_found = processor_obj.get_certbot_hosts() 21 | loggerEasyHaproxy.info('Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to run after save_config 22 | loggerEasyHaproxy.debug('Object Found: %s' % (processor_obj.get_parsed_object())) 23 | 24 | old_haproxy = None 25 | haproxy = DaemonizeHAProxy() 26 | current_custom_config_files = haproxy.get_custom_config_files() 27 | haproxy.haproxy(DaemonizeHAProxy.HAPROXY_START) 28 | haproxy.sleep() 29 | 30 | certbot = Certbot(Consts.certs_certbot) 31 | 32 | while True: 33 | if old_haproxy is not None: 34 | old_haproxy.kill() 35 | old_haproxy = None 36 | try: 37 | old_parsed = processor_obj.get_parsed_object() 38 | processor_obj.refresh() 39 | if certbot.check_certificates(certbot_certs_found) or DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive() or DeepDiff(current_custom_config_files, haproxy.get_custom_config_files()) != {}: 40 | loggerEasyHaproxy.info('New configuration found. Reloading...') 41 | loggerEasyHaproxy.debug('Object Found: %s' % (processor_obj.get_parsed_object())) 42 | processor_obj.save_config(Consts.haproxy_config) 43 | processor_obj.save_certs(Consts.certs_haproxy) 44 | certbot_certs_found = processor_obj.get_certbot_hosts() 45 | loggerEasyHaproxy.info('Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config 46 | old_haproxy = haproxy 47 | haproxy = DaemonizeHAProxy() 48 | current_custom_config_files = haproxy.get_custom_config_files() 49 | haproxy.haproxy(DaemonizeHAProxy.HAPROXY_RELOAD) 50 | old_haproxy.terminate() 51 | 52 | except Exception as e: 53 | loggerEasyHaproxy.fatal("Err: %s" % e) 54 | 55 | loggerEasyHaproxy.info('Heartbeat') 56 | haproxy.sleep() 57 | 58 | 59 | def main(): 60 | Functions.run_bash(loggerInit, '/usr/sbin/haproxy -v') 61 | 62 | loggerInit.info(" _ ") 63 | loggerInit.info(" ___ __ _ ____ _ ___| |_ __ _ _ __ _ _ _____ ___ _ ") 64 | loggerInit.info("/ -_) _` (_-< || |___| ' \\/ _` | '_ \\ '_/ _ \\ \\ / || |") 65 | loggerInit.info("\\___\\__,_/__/\\_, | |_||_\\__,_| .__/_| \\___/_\\_\\_, |") 66 | loggerInit.info(" |__/ |_| |__/ ") 67 | 68 | loggerInit.info("Release: %s" % (os.getenv("RELEASE_VERSION"))) 69 | loggerInit.debug('Environment:') 70 | for name, value in os.environ.items(): 71 | if "HAPROXY" in name: 72 | loggerInit.debug("- {0}: {1}".format(name, value)) 73 | 74 | start() 75 | 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /examples/docker/README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose Examples 2 | 3 | Self-contained examples for EasyHAProxy. **All documentation is in the docker-compose files as header comments.** 4 | 5 | ## Quick Start 6 | 7 | 1. Pick an example below 8 | 2. Open the docker-compose file 9 | 3. Read the header comments for complete instructions 10 | 4. Run the commands step-by-step 11 | 12 | ## Basic Examples 13 | 14 | | File | Description | 15 | |----------------------------------------------------------------------------|----------------------------------------------------------------| 16 | | [docker-compose.yml](docker-compose.yml) | Basic SSL setup with two virtual hosts and stats interface | 17 | | [docker-compose-acme.yml](docker-compose-acme.yml) | Let's Encrypt SSL with automatic certificate generation | 18 | | [docker-compose-multi-containers.yml](docker-compose-multi-containers.yml) | Load balancing across multiple container replicas | 19 | | [docker-compose-changed-label.yml](docker-compose-changed-label.yml) | Using custom label prefix (for multiple EasyHAProxy instances) | 20 | 21 | ## Real-World Application Examples 22 | 23 | | File | Description | 24 | |--------------------------------------------------------------------------------------|-----------------------------------------------------| 25 | | [docker-compose-portainer.yml](docker-compose-portainer.yml) | Portainer behind EasyHAProxy with Let's Encrypt | 26 | | [docker-compose-portainer-app-example.yml](docker-compose-portainer-app-example.yml) | Additional app alongside Portainer (shared network) | 27 | 28 | ## Plugin Examples 29 | 30 | | File | Description | 31 | |----------------------------------------------------------------------------|-----------------------------------------------------| 32 | | [docker-compose-php-fpm.yml](docker-compose-php-fpm.yml) | FastCGI plugin with PHP-FPM and PATH_INFO routing | 33 | | [docker-compose-jwt-validator.yml](docker-compose-jwt-validator.yml) | JWT token validation for API protection | 34 | | [docker-compose-ip-whitelist.yml](docker-compose-ip-whitelist.yml) | IP whitelist for admin panels or sensitive services | 35 | | [docker-compose-cloudflare.yml](docker-compose-cloudflare.yml) | Restore real client IPs when behind Cloudflare CDN | 36 | | [docker-compose-plugins-combined.yml](docker-compose-plugins-combined.yml) | Multiple plugins combined for layered security | 37 | 38 | ## Documentation Structure 39 | 40 | Each docker-compose file contains: 41 | - **WHAT THIS DEMONSTRATES** - Key features and concepts 42 | - **REQUIREMENTS** - Idempotent setup commands (safe to run multiple times) 43 | - **HOW TO START** - Command to launch the stack 44 | - **HOW TO VERIFY IT'S WORKING** - Test commands with expected outputs 45 | - **CLEAN UP** - Commands to stop and remove resources 46 | 47 | ## Additional Documentation 48 | 49 | - [Container Labels Reference](../../docs/container-labels.md) 50 | - [Docker Configuration Guide](../../docs/docker.md) 51 | - [Environment Variables](../../docs/environment-variable.md) 52 | - [Plugin Documentation](../../docs/plugins/) 53 | -------------------------------------------------------------------------------- /examples/docker/docker-compose-portainer.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Portainer Behind EasyHAProxy 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Running Portainer (Docker management UI) behind EasyHAProxy 7 | # - Using external volumes and networks for shared infrastructure 8 | # - Real-world application example with Let's Encrypt 9 | # - HTTP to HTTPS redirect with Certbot 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Create required volumes (idempotent) 14 | # docker volume create certs_certbot 15 | # docker volume create certs_haproxy 16 | # docker volume create portainer_data 17 | # 18 | # # Create shared network (idempotent) 19 | # docker network create easyhaproxy 20 | # 21 | # # Edit this file and change: 22 | # # - Line 18: EASYHAPROXY_CERTBOT_EMAIL to your email 23 | # # - Line 38: easyhaproxy.http.host to your real domain 24 | # ``` 25 | # 26 | # HOW TO START: 27 | # ```bash 28 | # docker compose -f docker-compose-portainer.yml up -d 29 | # ``` 30 | # 31 | # HOW TO VERIFY IT'S WORKING: 32 | # ```bash 33 | # # Check containers are running 34 | # docker compose -f docker-compose-portainer.yml ps 35 | # # Expected: Both easyhaproxy and portainer containers running 36 | # 37 | # # Access Portainer (replace with your domain or use /etc/hosts) 38 | # # First time: Create admin user 39 | # curl http://portainer.xpto.us 40 | # # OR with /etc/hosts: echo "127.0.0.1 portainer.xpto.us" | sudo tee -a /etc/hosts 41 | # 42 | # # View HAProxy stats 43 | # # URL: http://localhost:1936 44 | # # Username: admin 45 | # # Password: password 46 | # ``` 47 | # 48 | # CLEAN UP: 49 | # ```bash 50 | # docker compose -f docker-compose-portainer.yml down 51 | # # To also remove volumes: 52 | # # docker compose -f docker-compose-portainer.yml down -v 53 | # # docker volume rm certs_certbot certs_haproxy portainer_data 54 | # # docker network rm easyhaproxy 55 | # ``` 56 | # 57 | # ============================================================================== 58 | 59 | 60 | services: 61 | easyhaproxy: 62 | image: byjg/easy-haproxy:5.0.0 63 | volumes: 64 | - /var/run/docker.sock:/var/run/docker.sock 65 | - certs_certbot:/certs/certbot 66 | # - certs_haproxy:/certs/haproxy 67 | 68 | environment: 69 | EASYHAPROXY_DISCOVER: docker 70 | EASYHAPROXY_LABEL_PREFIX: easyhaproxy 71 | EASYHAPROXY_CERTBOT_EMAIL: changeme@example.org 72 | EASYHAPROXY_SSL_MODE: "default" 73 | HAPROXY_CUSTOMERRORS: "true" 74 | HAPROXY_USERNAME: admin 75 | HAPROXY_PASSWORD: password 76 | HAPROXY_STATS_PORT: 1936 77 | 78 | ports: 79 | - "80:80/tcp" 80 | - "443:443/tcp" 81 | - "1936:1936/tcp" 82 | 83 | portainer: 84 | image: portainer/portainer-ce:latest 85 | volumes: 86 | - portainer_data:/data 87 | - /var/run/docker.sock:/var/run/docker.sock 88 | labels: 89 | easyhaproxy.http.redirect_ssl: true 90 | easyhaproxy.http.certbot: true 91 | easyhaproxy.http.host: portainer.xpto.us 92 | easyhaproxy.http.port: 80 93 | easyhaproxy.http.localport: 9000 94 | 95 | 96 | volumes: 97 | certs_certbot: 98 | external: true 99 | certs_haproxy: 100 | external: true 101 | portainer_data: 102 | external: true 103 | 104 | 105 | networks: 106 | default: 107 | name: easyhaproxy 108 | external: true 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/tests/expected/services-multiple-hosts.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | errorfile 400 /etc/haproxy/errors-custom/400.http 25 | errorfile 403 /etc/haproxy/errors-custom/403.http 26 | errorfile 408 /etc/haproxy/errors-custom/408.http 27 | errorfile 500 /etc/haproxy/errors-custom/500.http 28 | errorfile 502 /etc/haproxy/errors-custom/502.http 29 | errorfile 503 /etc/haproxy/errors-custom/503.http 30 | errorfile 504 /etc/haproxy/errors-custom/504.http 31 | 32 | 33 | frontend stats 34 | bind *:1937 35 | mode http 36 | http-request use-service prometheus-exporter if { path /metrics } 37 | stats enable 38 | stats hide-version 39 | stats realm Haproxy\ Statistics 40 | stats uri / 41 | stats auth joe:s3cr3t 42 | default_backend srv_stats 43 | 44 | backend srv_stats 45 | mode http 46 | server Local 127.0.0.1:1937 47 | 48 | frontend http_in_19901 49 | bind *:19901 50 | mode http 51 | redirect prefix www.google.com code 301 if { hdr(host) -i google.helloworld.com } 52 | 53 | acl is_rule_www_helloworld_com_19901_1 hdr(host) -i www.helloworld.com 54 | acl is_rule_www_helloworld_com_19901_2 hdr(host) -i www.helloworld.com:19901 55 | use_backend srv_www_helloworld_com_19901 if is_rule_www_helloworld_com_19901_1 OR is_rule_www_helloworld_com_19901_2 56 | 57 | acl is_rule_hello_com_19901_1 hdr(host) -i hello.com 58 | acl is_rule_hello_com_19901_2 hdr(host) -i hello.com:19901 59 | use_backend srv_hello_com_19901 if is_rule_hello_com_19901_1 OR is_rule_hello_com_19901_2 60 | 61 | backend srv_www_helloworld_com_19901 62 | balance roundrobin 63 | mode http 64 | option forwardfor 65 | http-request set-header X-Forwarded-Port %[dst_port] 66 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 67 | server srv-0 3e63154954b0:80 check weight 1 68 | server srv-1 eb294c110eb1:80 check weight 1 69 | backend srv_hello_com_19901 70 | balance roundrobin 71 | mode http 72 | option forwardfor 73 | http-request set-header X-Forwarded-Port %[dst_port] 74 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 75 | server srv-0 3e63154954b0:80 check weight 1 76 | server srv-1 eb294c110eb1:80 check weight 1 77 | 78 | backend certbot_backend 79 | mode http 80 | server certbot 127.0.0.1:2080 81 | -------------------------------------------------------------------------------- /helm/easyhaproxy/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: {{ ternary "Deployment" "DaemonSet" .Values.service.create }} 4 | metadata: 5 | name: {{ include "easyhaproxy.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "easyhaproxy.labels" . | nindent 4 }} 9 | spec: 10 | {{- if .Values.service.create }} 11 | replicas: {{ .Values.replicaCount | default 1 }} 12 | {{- end }} 13 | selector: 14 | matchLabels: 15 | {{- include "easyhaproxy.selectorLabels" . | nindent 6 }} 16 | template: 17 | metadata: 18 | labels: 19 | {{- include "easyhaproxy.selectorLabels" . | nindent 8 }} 20 | spec: 21 | {{- if not .Values.service.create }} 22 | affinity: 23 | nodeAffinity: 24 | requiredDuringSchedulingIgnoredDuringExecution: 25 | nodeSelectorTerms: 26 | - matchExpressions: 27 | - key: {{ .Values.masterNode.label }} 28 | operator: In 29 | values: 30 | {{- toYaml .Values.masterNode.values | nindent 18 }} 31 | {{- end }} 32 | serviceAccountName: {{ include "easyhaproxy.serviceAccountName" . }} 33 | securityContext: 34 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 35 | containers: 36 | - name: {{ .Chart.Name }} 37 | securityContext: 38 | {{- toYaml .Values.securityContext | nindent 12 }} 39 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 40 | imagePullPolicy: {{ .Values.image.pullPolicy }} 41 | ports: 42 | - name: http 43 | containerPort: 80 44 | {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.http }}{{ end }} 45 | - name: https 46 | containerPort: 443 47 | {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.https }}{{ end }} 48 | - name: stats 49 | containerPort: 1936 50 | {{ if not .Values.service.create }}hostPort: {{ .Values.binding.ports.stats }}{{ end }} 51 | {{- range $port := .Values.binding.additionalPorts }} 52 | - name: extra{{ $port }} 53 | containerPort: {{ $port }} 54 | {{ if not $.Values.service.create }}hostPort: {{ $port }}{{ end }} 55 | {{- end }} 56 | resources: 57 | {{- toYaml .Values.resources | nindent 12 }} 58 | env: 59 | - name: EASYHAPROXY_DISCOVER 60 | value: kubernetes 61 | - name: HAPROXY_USERNAME 62 | value: {{ .Values.easyhaproxy.stats.username }} 63 | - name: HAPROXY_PASSWORD 64 | value: {{ .Values.easyhaproxy.stats.password }} 65 | - name: EASYHAPROXY_REFRESH_CONF 66 | value: {{ .Values.easyhaproxy.refresh | quote }} 67 | - name: HAPROXY_CUSTOMERRORS 68 | value: {{ .Values.easyhaproxy.customErrors | quote}} 69 | - name: EASYHAPROXY_SSL_MODE 70 | value: {{ .Values.easyhaproxy.sslMode }} 71 | - name: EASYHAPROXY_LOG_LEVEL 72 | value: {{ .Values.easyhaproxy.logLevel.easyhaproxy }} 73 | - name: HAPROXY_LOG_LEVEL 74 | value: {{ .Values.easyhaproxy.logLevel.haproxy }} 75 | - name: CERTBOT_LOG_LEVEL 76 | value: {{ .Values.easyhaproxy.logLevel.certbot }} 77 | {{- if .Values.easyhaproxy.certbot.email }} 78 | - name: EASYHAPROXY_CERTBOT_EMAIL 79 | value: {{ .Values.easyhaproxy.certbot.email }} 80 | {{ end }} 81 | -------------------------------------------------------------------------------- /examples/kubernetes/ip-whitelist.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: IP Whitelist Plugin for Kubernetes 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restricting access to specific IP addresses or CIDR ranges 7 | # - Using annotations for IP-based access control 8 | # - Protecting admin panels or sensitive services in Kubernetes 9 | # - Custom HTTP status code for blocked requests 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # 1. Ensure EasyHAProxy is installed in your cluster 14 | # kubectl create namespace easyhaproxy 15 | # kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml 16 | # 17 | # # 2. IMPORTANT: Edit this file (line 80) and update allowed_ips 18 | # # with your actual office/VPN IP addresses or networks 19 | # ``` 20 | # 21 | # HOW TO START: 22 | # ```bash 23 | # kubectl apply -f ip-whitelist.yml 24 | # ``` 25 | # 26 | # HOW TO VERIFY IT'S WORKING: 27 | # ```bash 28 | # # Check resources are created 29 | # kubectl get deployment,service,ingress -l app=admin 30 | # 31 | # # Test from allowed IP 32 | # kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80 33 | # curl -H "Host: admin.example.local" http://localhost:8080 34 | # # Expected: 200 OK with "Admin Panel - IP Restricted" (if your IP is in allowed_ips) 35 | # 36 | # # Test from non-allowed IP 37 | # # Expected: HTTP 403 Forbidden 38 | # ``` 39 | # 40 | # CLEAN UP: 41 | # ```bash 42 | # kubectl delete -f ip-whitelist.yml 43 | # ``` 44 | # 45 | # ============================================================================== 46 | 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: admin-service 52 | namespace: default 53 | spec: 54 | ports: 55 | - port: 8080 56 | targetPort: 8080 57 | selector: 58 | app: admin 59 | type: ClusterIP 60 | 61 | --- 62 | apiVersion: apps/v1 63 | kind: Deployment 64 | metadata: 65 | name: admin 66 | namespace: default 67 | spec: 68 | replicas: 2 69 | selector: 70 | matchLabels: 71 | app: admin 72 | template: 73 | metadata: 74 | labels: 75 | app: admin 76 | spec: 77 | containers: 78 | - name: admin 79 | image: byjg/static-httpserver 80 | ports: 81 | - containerPort: 8080 82 | env: 83 | - name: TITLE 84 | value: "Admin Panel - IP Restricted" 85 | resources: 86 | limits: 87 | cpu: '0.1' 88 | memory: '64Mi' 89 | requests: 90 | cpu: '0.05' 91 | memory: '32Mi' 92 | 93 | --- 94 | apiVersion: networking.k8s.io/v1 95 | kind: Ingress 96 | metadata: 97 | annotations: 98 | kubernetes.io/ingress.class: easyhaproxy-ingress 99 | 100 | # Enable IP whitelist plugin 101 | easyhaproxy.plugins: "ip_whitelist" 102 | 103 | # Allow specific IPs and networks 104 | # UPDATE THIS with your actual office/VPN IPs! 105 | easyhaproxy.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,198.51.100.42,10.0.0.0/8" 106 | 107 | # Status code to return for blocked IPs 108 | easyhaproxy.plugin.ip_whitelist.status_code: "403" 109 | name: admin-ingress-whitelist 110 | namespace: default 111 | spec: 112 | rules: 113 | - host: admin.example.local 114 | http: 115 | paths: 116 | - backend: 117 | service: 118 | name: admin-service 119 | port: 120 | number: 8080 121 | pathType: ImplementationSpecific 122 | -------------------------------------------------------------------------------- /src/plugins/builtin/deny_pages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Deny Pages Plugin for EasyHAProxy 3 | 4 | This plugin blocks access to specific paths for a domain. 5 | It runs as a DOMAIN plugin (once per domain). 6 | 7 | Configuration: 8 | - enabled: Enable/disable the plugin (default: true) 9 | - paths: Comma-separated list of paths to deny (e.g., "/admin,/private") 10 | - status_code: HTTP status code to return (default: 403) 11 | 12 | Example YAML config: 13 | plugins: 14 | deny_pages: 15 | enabled: true 16 | paths: "/admin,/private,/internal" 17 | status_code: 403 18 | 19 | Example Container Label: 20 | easyhaproxy.http.plugins: "deny_pages" 21 | easyhaproxy.http.plugin.deny_pages.paths: "/admin,/private" 22 | 23 | HAProxy Config Generated: 24 | # Deny Pages - Block specific paths 25 | acl denied_path path_beg /admin /private 26 | http-request deny deny_status 403 if denied_path 27 | """ 28 | 29 | import os 30 | import sys 31 | 32 | # Add parent directory to path for imports 33 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 34 | 35 | from plugins import PluginInterface, PluginType, PluginContext, PluginResult 36 | 37 | 38 | class DenyPagesPlugin(PluginInterface): 39 | """Plugin to deny access to specific paths""" 40 | 41 | def __init__(self): 42 | self.enabled = True 43 | self.paths = [] 44 | self.status_code = 403 45 | 46 | @property 47 | def name(self) -> str: 48 | return "deny_pages" 49 | 50 | @property 51 | def plugin_type(self) -> PluginType: 52 | return PluginType.DOMAIN 53 | 54 | def configure(self, config: dict) -> None: 55 | """ 56 | Configure the plugin 57 | 58 | Args: 59 | config: Dictionary with configuration options 60 | - enabled: Whether plugin is enabled 61 | - paths: Comma-separated list of paths to deny 62 | - status_code: HTTP status code to return 63 | """ 64 | if "enabled" in config: 65 | self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"] 66 | 67 | if "paths" in config: 68 | paths_str = str(config["paths"]) 69 | self.paths = [p.strip() for p in paths_str.split(",") if p.strip()] 70 | 71 | if "status_code" in config: 72 | try: 73 | self.status_code = int(config["status_code"]) 74 | except ValueError: 75 | self.status_code = 403 76 | 77 | def process(self, context: PluginContext) -> PluginResult: 78 | """ 79 | Generate HAProxy config to deny specific paths 80 | 81 | Args: 82 | context: Plugin execution context with domain information 83 | 84 | Returns: 85 | PluginResult with HAProxy configuration snippet 86 | """ 87 | if not self.enabled or not self.paths: 88 | return PluginResult() 89 | 90 | # Create path list for ACL 91 | paths_str = " ".join(self.paths) 92 | 93 | # Generate HAProxy config snippet 94 | haproxy_config = f"""# Deny Pages - Block specific paths 95 | acl denied_path path_beg {paths_str} 96 | http-request deny deny_status {self.status_code} if denied_path""" 97 | 98 | return PluginResult( 99 | haproxy_config=haproxy_config, 100 | modified_easymapping=None, 101 | metadata={ 102 | "domain": context.domain, 103 | "blocked_paths": self.paths, 104 | "status_code": self.status_code 105 | } 106 | ) 107 | -------------------------------------------------------------------------------- /examples/docker/php-app/test-path-info.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PATH_INFO Test 7 | 62 | 63 | 64 |
65 |

PATH_INFO Test

66 | 67 | 68 |
69 | Success! PATH_INFO is working correctly. 70 |
71 | 72 |

PATH_INFO Value

73 |
74 | 75 |

Parsed Path Segments

76 |
80 | 81 | 82 |
83 | Note: PATH_INFO is not set. Try accessing this page with additional path segments. 84 |
85 | 86 | 87 |

Request Information

88 |
94 | 95 |

Example Usage

96 |

PATH_INFO enables RESTful URL routing. Try these URLs:

97 | 102 | 103 |

← Back to Home

104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /examples/swarm/ip-whitelist.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: IP Whitelist Plugin (Swarm) 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restricting access to specific IP addresses/networks 7 | # - IP-based access control for admin panels or sensitive services 8 | # - Returning custom status codes for blocked IPs 9 | # - Service discovery in Swarm mode with plugins 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Initialize Docker Swarm (if not already initialized) 14 | # docker swarm init 15 | # 16 | # # Create overlay network (idempotent) 17 | # docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy 18 | # 19 | # # Ensure EasyHAProxy is deployed 20 | # docker stack deploy -c easyhaproxy.yml easyhaproxy 21 | # 22 | # # Add to /etc/hosts for local testing (idempotent) 23 | # grep -q "admin.example.com" /etc/hosts || echo "127.0.0.1 admin.example.com" | sudo tee -a /etc/hosts 24 | # 25 | # # IMPORTANT: Edit this file (ip-whitelist.yml) line 64 to add your actual IP addresses! 26 | # # Get your current IP: curl ifconfig.me 27 | # ``` 28 | # 29 | # HOW TO START: 30 | # ```bash 31 | # docker stack deploy -c ip-whitelist.yml admin 32 | # ``` 33 | # 34 | # HOW TO VERIFY IT'S WORKING: 35 | # ```bash 36 | # # Check stack is deployed 37 | # docker stack ls | grep admin 38 | # # Expected: admin stack listed 39 | # 40 | # # Check service is running 41 | # docker service ls | grep admin_admin 42 | # # Expected: admin_admin with 3/3 replicas 43 | # 44 | # # Test from allowed IP (assumes 127.0.0.1 or your IP is in the whitelist) 45 | # curl -H "Host: admin.example.com" http://localhost/ 46 | # # Expected: 200 OK with "Admin Panel - IP Restricted" 47 | # 48 | # # Test from blocked IP (using a different IP via proxy or VPN) 49 | # # Expected: HTTP 403 Forbidden 50 | # 51 | # # View HAProxy stats to see IP whitelist rules 52 | # # URL: http://localhost:1936 53 | # # Username: admin 54 | # # Password: password 55 | # ``` 56 | # 57 | # CLEAN UP: 58 | # ```bash 59 | # docker stack rm admin 60 | # ``` 61 | # 62 | # ============================================================================== 63 | 64 | version: "3.7" 65 | 66 | services: 67 | haproxy: 68 | image: byjg/easy-haproxy:5.0.0 69 | volumes: 70 | - /var/run/docker.sock:/var/run/docker.sock 71 | deploy: 72 | replicas: 1 73 | placement: 74 | constraints: 75 | - node.role == manager 76 | environment: 77 | EASYHAPROXY_DISCOVER: swarm 78 | HAPROXY_USERNAME: admin 79 | HAPROXY_PASSWORD: password 80 | HAPROXY_STATS_PORT: 1936 81 | ports: 82 | - "80:80/tcp" 83 | - "1936:1936/tcp" 84 | networks: 85 | - easyhaproxy 86 | 87 | # Admin panel with IP restrictions 88 | admin: 89 | image: byjg/static-httpserver 90 | environment: 91 | TITLE: "Admin Panel - IP Restricted" 92 | deploy: 93 | replicas: 3 94 | labels: 95 | easyhaproxy.http.host: "admin.example.com" 96 | easyhaproxy.http.port: "80" 97 | easyhaproxy.http.localport: "8080" 98 | 99 | # Enable IP whitelist plugin 100 | easyhaproxy.http.plugins: "ip_whitelist" 101 | 102 | # Allow specific IPs and networks 103 | # UPDATE THIS with your actual office/VPN IPs! 104 | easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,198.51.100.0/24,10.0.0.0/8" 105 | 106 | # Status code to return for blocked IPs 107 | easyhaproxy.http.plugin.ip_whitelist.status_code: "403" 108 | networks: 109 | - easyhaproxy 110 | 111 | networks: 112 | easyhaproxy: 113 | external: true 114 | -------------------------------------------------------------------------------- /docs/environment-variable.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 12 3 | --- 4 | 5 | # Docker environment variables 6 | 7 | | Environment Variable | Description | Default | 8 | |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| 9 | | EASYHAPROXY_DISCOVER | How the services will be discovered to create `haproxy.cfg`: `static`, `docker`, `swarm` or `kubernetes` | **required** | 10 | | EASYHAPROXY_LABEL_PREFIX | (Optional) The key will search for matching resources. | `easyhaproxy` | 11 | | EASYHAPROXY_CERTBOT_* | (Optional) Enable Let's Encrypt or any other ACME certificate. See more: [acme](acme.md) | *empty* | 12 | | 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` | 13 | | EASYHAPROXY_REFRESH_CONF | (Optional) Check for new containers/services every N seconds. | 10 | 14 | | EASYHAPROXY_LOG_LEVEL | (Optional) The log level for EasyHAproxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | 15 | | CERTBOT_LOG_LEVEL | (Optional) The log level for Certbot messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | DEBUG | 16 | | HAPROXY_LOG_LEVEL | (Optional) The log level for HAProxy messages. Available: TRACE,DEBUG,INFO,WARN,ERROR,FATAL | INFO | 17 | | HAPROXY_USERNAME | (Optional) The HAProxy username for the statistics endpoint (used only when `HAPROXY_PASSWORD` is set). | `admin` | 18 | | HAPROXY_PASSWORD | (Optional) The HAProxy password to the statistics endpoint. Stats are **disabled** unless this is defined. | *empty* | 19 | | HAPROXY_STATS_PORT | (Optional) The HAProxy port to the statistics. If set to `false`, disable statistics. Only applies when `HAPROXY_PASSWORD` is defined. | `1936` | 20 | | HAPROXY_CUSTOMERRORS | (Optional) If HAProxy will use custom HTML errors. true/false. | `false` | 21 | 22 | :::tip HAProxy Stats 23 | Statistics are only configured when `HAPROXY_PASSWORD` is set. Without a password, the stats section is not generated. 24 | ::: 25 | 26 | :::note ACME/Certbot Environment Variables 27 | 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. 28 | ::: 29 | 30 | ---- 31 | [Open source ByJG](http://opensource.byjg.com) 32 | -------------------------------------------------------------------------------- /docs/swarm.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Swarm 6 | 7 | ## Setup Docker EasyHAProxy 8 | 9 | This method involves using a Docker Swarm installation to discover containers and configure HAProxy. 10 | 11 | EasyHAProxy inspects Docker containers within the Swarm and retrieves labels to configure HAProxy. Once it identifies a container with at least the label 'easyhaproxy.http.host,' it configures HAProxy to redirect traffic to that container. To accomplish this, EasyHAProxy may need to attach the same network to its container. 12 | 13 | :::tip Docker Swarm Advantages 14 | - **Container Discovery**: Docker Swarm facilitates the discovery of containers within the cluster, streamlining the process of identifying services for HAProxy configuration. 15 | - **Remote Node Management**: Docker Swarm allows for the management of containers across multiple nodes, providing flexibility and scalability in deploying services while ensuring seamless HAProxy configuration across the cluster. 16 | ::: 17 | 18 | It's recommended to create a network external to EasyHAProxy. 19 | 20 | :::warning Limitations 21 | - You cannot mix Docker containers with Swarm containers. 22 | - This method does not work with containers that use the `--network=host` option. See [limitations](limitations.md) for details. 23 | ::: 24 | 25 | For example: 26 | 27 | ```bash title="Create overlay network" 28 | docker network create -d overlay --attachable easyhaproxy 29 | ``` 30 | 31 | And then deploy the EasyHAProxy stack: 32 | 33 | ```yaml 34 | services: 35 | haproxy: 36 | image: byjg/easy-haproxy:5.0.0 37 | volumes: 38 | - /var/run/docker.sock:/var/run/docker.sock 39 | deploy: 40 | replicas: 1 41 | environment: 42 | EASYHAPROXY_DISCOVER: swarm 43 | EASYHAPROXY_SSL_MODE: "loose" 44 | HAPROXY_CUSTOMERRORS: "true" 45 | HAPROXY_USERNAME: admin 46 | HAPROXY_PASSWORD: password 47 | HAPROXY_STATS_PORT: 1936 48 | ports: 49 | - "80:80/tcp" 50 | - "443:443/tcp" 51 | - "1936:1936/tcp" 52 | networks: 53 | - easyhaproxy 54 | 55 | networks: 56 | easyhaproxy: 57 | external: true 58 | ``` 59 | 60 | Deploy the stack: 61 | 62 | ```bash title="Deploy EasyHAProxy stack" 63 | docker stack deploy --compose-file docker-compose.yml easyhaproxy 64 | ``` 65 | 66 | Mapping to `/var/run/docker.sock` is necessary to discover the docker containers and get the labels; 67 | 68 | :::danger Single Replica Only 69 | **Do not** add more than one replica for EasyHAProxy. To understand why, see the [limitations](limitations.md) page. 70 | ::: 71 | 72 | ## Running containers 73 | 74 | To make your containers "discoverable" by EasyHAProxy, that is the minimum configuration you need: 75 | 76 | ```yaml 77 | services: 78 | container: 79 | image: my/image:tag 80 | deploy: 81 | replicas: 1 82 | labels: 83 | easyhaproxy.http.host: host1.local 84 | easyhaproxy.http.port: 80 85 | easyhaproxy.http.localport: 8080 86 | networks: 87 | - easyhaproxy 88 | 89 | networks: 90 | easyhaproxy: 91 | external: true 92 | ``` 93 | 94 | Once the container is running, EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container. 95 | 96 | You don't need to expose any port in your container. 97 | 98 | Please follow the [docker label configuration](container-labels.md) to see other configurations available. 99 | 100 | ## Setup the EasyHAProxy container 101 | 102 | You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [environment variable guide](environment-variable.md) 103 | 104 | ## More information 105 | 106 | You can refer to the [Docker Documentation](docker.md) to get other detailed instructions. 107 | 108 | ---- 109 | [Open source ByJG](http://opensource.byjg.com) 110 | -------------------------------------------------------------------------------- /src/plugins/builtin/ip_whitelist.py: -------------------------------------------------------------------------------- 1 | """ 2 | IP Whitelist Plugin for EasyHAProxy 3 | 4 | This plugin restricts access to a domain to only specific IP addresses or CIDR ranges. 5 | It runs as a DOMAIN plugin (once per domain). 6 | 7 | Configuration: 8 | - enabled: Enable/disable the plugin (default: true) 9 | - allowed_ips: Comma-separated list of IPs/CIDR ranges to allow 10 | - status_code: HTTP status code to return for blocked IPs (default: 403) 11 | 12 | Example YAML config: 13 | plugins: 14 | ip_whitelist: 15 | enabled: true 16 | allowed_ips: "192.168.1.0/24,10.0.0.1,172.16.0.0/16" 17 | status_code: 403 18 | 19 | Example Container Label: 20 | easyhaproxy.http.plugins: "ip_whitelist" 21 | easyhaproxy.http.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.1" 22 | easyhaproxy.http.plugin.ip_whitelist.status_code: 403 23 | 24 | HAProxy Config Generated: 25 | # IP Whitelist - Only allow specific IPs 26 | acl whitelisted_ip src 192.168.1.0/24 10.0.0.1 27 | http-request deny deny_status 403 if !whitelisted_ip 28 | """ 29 | 30 | import os 31 | import sys 32 | 33 | # Add parent directory to path for imports 34 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 35 | 36 | from plugins import PluginInterface, PluginType, PluginContext, PluginResult 37 | 38 | 39 | class IpWhitelistPlugin(PluginInterface): 40 | """Plugin to restrict access to specific IP addresses""" 41 | 42 | def __init__(self): 43 | self.enabled = True 44 | self.allowed_ips = [] 45 | self.status_code = 403 46 | 47 | @property 48 | def name(self) -> str: 49 | return "ip_whitelist" 50 | 51 | @property 52 | def plugin_type(self) -> PluginType: 53 | return PluginType.DOMAIN 54 | 55 | def configure(self, config: dict) -> None: 56 | """ 57 | Configure the plugin 58 | 59 | Args: 60 | config: Dictionary with configuration options 61 | - enabled: Whether plugin is enabled 62 | - allowed_ips: Comma-separated list of IPs/CIDR ranges 63 | - status_code: HTTP status code to return for denied requests 64 | """ 65 | if "enabled" in config: 66 | self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"] 67 | 68 | if "allowed_ips" in config: 69 | ips_str = str(config["allowed_ips"]) 70 | self.allowed_ips = [ip.strip() for ip in ips_str.split(",") if ip.strip()] 71 | 72 | if "status_code" in config: 73 | try: 74 | self.status_code = int(config["status_code"]) 75 | except ValueError: 76 | self.status_code = 403 77 | 78 | def process(self, context: PluginContext) -> PluginResult: 79 | """ 80 | Generate HAProxy config to whitelist specific IPs 81 | 82 | Args: 83 | context: Plugin execution context with domain information 84 | 85 | Returns: 86 | PluginResult with HAProxy configuration snippet 87 | """ 88 | if not self.enabled or not self.allowed_ips: 89 | return PluginResult() 90 | 91 | # Create space-separated list of IPs for ACL 92 | ips_str = " ".join(self.allowed_ips) 93 | 94 | # Generate HAProxy config snippet 95 | haproxy_config = f"""# IP Whitelist - Only allow specific IPs 96 | acl whitelisted_ip src {ips_str} 97 | http-request deny deny_status {self.status_code} if !whitelisted_ip""" 98 | 99 | return PluginResult( 100 | haproxy_config=haproxy_config, 101 | modified_easymapping=None, 102 | metadata={ 103 | "domain": context.domain, 104 | "allowed_ips": self.allowed_ips, 105 | "status_code": self.status_code 106 | } 107 | ) 108 | -------------------------------------------------------------------------------- /examples/kubernetes/service.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Basic Kubernetes Ingress 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Basic ingress configuration with EasyHAProxy 7 | # - Multiple domains pointing to the same service 8 | # - Complete deployment + service + ingress setup 9 | # - HTTP ingress without TLS 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # 1. Ensure EasyHAProxy is installed in your cluster 14 | # kubectl create namespace easyhaproxy 15 | # kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml 16 | # 17 | # # 2. Label the node where EasyHAProxy will run 18 | # kubectl label nodes "easyhaproxy/node=master" 19 | # 20 | # # 3. Add to /etc/hosts for local testing (idempotent) 21 | # grep -q "example.org" /etc/hosts || echo " example.org www.example.org" | sudo tee -a /etc/hosts 22 | # ``` 23 | # 24 | # HOW TO START: 25 | # ```bash 26 | # kubectl apply -f service.yml 27 | # ``` 28 | # 29 | # HOW TO VERIFY IT'S WORKING: 30 | # ```bash 31 | # # Check resources are created 32 | # kubectl get deployment,service,ingress container-example 33 | # 34 | # # Test via node IP 35 | # curl -H "Host: example.org" http://:31080 36 | # # Expected: 200 OK with "My Host Example" 37 | # 38 | # # Or use port-forward for testing 39 | # kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80 40 | # curl -H "Host: example.org" http://localhost:8080 41 | # # Expected: 200 OK with "My Host Example" 42 | # 43 | # # Test second domain 44 | # curl -H "Host: www.example.org" http://:31080 45 | # # Expected: Same response 46 | # ``` 47 | # 48 | # CLEAN UP: 49 | # ```bash 50 | # kubectl delete -f service.yml 51 | # ``` 52 | # 53 | # ============================================================================== 54 | 55 | --- 56 | apiVersion: networking.k8s.io/v1 57 | kind: Ingress 58 | metadata: 59 | annotations: 60 | kubernetes.io/ingress.class: easyhaproxy-ingress 61 | name: container-example 62 | namespace: default 63 | spec: 64 | rules: 65 | - host: example.org 66 | http: 67 | paths: 68 | - backend: 69 | service: 70 | name: container-example 71 | port: 72 | number: 8080 73 | pathType: ImplementationSpecific 74 | - host: www.example.org 75 | http: 76 | paths: 77 | - backend: 78 | service: 79 | name: container-example 80 | port: 81 | number: 8080 82 | pathType: ImplementationSpecific 83 | 84 | --- 85 | apiVersion: v1 86 | kind: Service 87 | metadata: 88 | name: container-example 89 | namespace: default 90 | spec: 91 | ports: 92 | - name: http 93 | port: 8080 94 | selector: 95 | app: container-example 96 | type: ClusterIP 97 | 98 | --- 99 | apiVersion: apps/v1 100 | kind: Deployment 101 | metadata: 102 | name: container-example 103 | namespace: default 104 | spec: 105 | replicas: 1 106 | revisionHistoryLimit: 10 107 | strategy: 108 | rollingUpdate: 109 | maxSurge: 1 110 | maxUnavailable: 0 111 | type: RollingUpdate 112 | selector: 113 | matchLabels: 114 | app: container-example 115 | template: 116 | metadata: 117 | labels: 118 | app: container-example 119 | spec: 120 | containers: 121 | - name: container-example 122 | image: byjg/static-httpserver 123 | ports: 124 | - containerPort: 8080 125 | resources: 126 | limits: 127 | cpu: '0.05' 128 | memory: '20Mi' 129 | requests: 130 | cpu: '0.05' 131 | memory: '20Mi' 132 | env: 133 | - name: TITLE 134 | value: "My Host Example" 135 | -------------------------------------------------------------------------------- /src/templates/haproxy.cfg.j2: -------------------------------------------------------------------------------- 1 | {% set log_definition = data["logLevel"] | default({}) %} 2 | {% set log_level = log_definition["haproxy"] | default("INFO") | upper %} 3 | {% if log_level == "TRACE" or log_level == "DEBUG" %} 4 | {% set haproxy_log_level = "debug" %} 5 | {% elif log_level == "INFO" %} 6 | {% set haproxy_log_level = "info" %} 7 | {% elif log_level == "WARN" %} 8 | {% set haproxy_log_level = "warning" %} 9 | {% elif log_level == "ERROR" %} 10 | {% set haproxy_log_level = "err" %} 11 | {% elif log_level == "FATAL" %} 12 | {% set haproxy_log_level = "crit" %} 13 | {% endif %} 14 | global 15 | log stdout format raw local0 {{ haproxy_log_level }} 16 | maxconn 2000 17 | {% if data["ssl_mode"] == "strict" %} 18 | {% include "ssl_strict.j2" %} 19 | {% elif data["ssl_mode"] == "loose" %} 20 | {% include "ssl_loose.j2" %} 21 | {% else %} 22 | {% include "ssl_default.j2" %} 23 | {% endif %} 24 | 25 | 26 | defaults 27 | log global 28 | option httplog 29 | 30 | timeout connect 3s 31 | timeout client 10s 32 | timeout server 10m 33 | {% if data["customerrors"] %} 34 | errorfile 400 /etc/haproxy/errors-custom/400.http 35 | errorfile 403 /etc/haproxy/errors-custom/403.http 36 | errorfile 408 /etc/haproxy/errors-custom/408.http 37 | errorfile 500 /etc/haproxy/errors-custom/500.http 38 | errorfile 502 /etc/haproxy/errors-custom/502.http 39 | errorfile 503 /etc/haproxy/errors-custom/503.http 40 | errorfile 504 /etc/haproxy/errors-custom/504.http 41 | {% endif %} 42 | 43 | {% if global_plugin_configs %} 44 | # Global Plugin Configurations 45 | {% for config in global_plugin_configs %} 46 | {{ config }} 47 | {% endfor %} 48 | {% endif %} 49 | 50 | {% set data_stats = data["stats"] | default({}) %} 51 | {% if data_stats["port"] | default(1936) | int > 0 %} 52 | frontend stats 53 | bind *:{{ data_stats["port"] | default(1936) }} 54 | mode http 55 | http-request use-service prometheus-exporter if { path /metrics } 56 | stats enable 57 | stats hide-version 58 | stats realm Haproxy\ Statistics 59 | stats uri / 60 | {% if data_stats["password"] | default("") != "" %} 61 | stats auth {{ data_stats["username"] | default("admin") }}:{{ data_stats["password"] }} 62 | {% endif %} 63 | default_backend srv_stats 64 | 65 | backend srv_stats 66 | mode http 67 | server Local 127.0.0.1:{{ data_stats["port"] | default(1936) }} 68 | {% endif %} 69 | {% for o in data["easymapping"] -%} 70 | {% set mode = o["mode"] or "http" %} 71 | 72 | frontend {{ mode }}_in_{{ o["port"] }} 73 | {% include "bind.j2" %} 74 | {% if mode == "http" %} 75 | {% include "frontend-mode-http.j2" %} 76 | {% else %} 77 | {% include "frontend-mode-tcp.j2" %} 78 | {% endif %} 79 | 80 | {% for k in o["hosts"] -%} 81 | {% set host = k.replace(".", "_") + "_{0}".format(o["port"]) %} 82 | backend srv_{{ host }} 83 | balance {{ o["balance"] | default("roundrobin") }} 84 | mode {{ mode }} 85 | {% if o["hosts"][k]["plugin_configs"] is defined and o["hosts"][k]["plugin_configs"] | length > 0 %} 86 | # Domain Plugin Configurations for {{ k }} 87 | {% for config in o["hosts"][k]["plugin_configs"] %} 88 | {{ config | indent(4, first=True) }} 89 | {% endfor %} 90 | {% endif %} 91 | {% if mode == "http" %} 92 | option forwardfor 93 | http-request set-header X-Forwarded-Port %[dst_port] 94 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 95 | {% elif mode == "tcp" %} 96 | option tcp-check 97 | tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }} 98 | {% endif %} 99 | {% for c in o["hosts"][k]["containers"] %} 100 | 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") }} 101 | {% endfor %} 102 | {% endfor %} 103 | {% endfor %} 104 | 105 | backend certbot_backend 106 | mode http 107 | server certbot 127.0.0.1:2080 108 | 109 | -------------------------------------------------------------------------------- /examples/kubernetes/cloudflare.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Cloudflare IP Restoration Plugin for Kubernetes 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restoring original visitor IPs when behind Cloudflare CDN 7 | # - Using ConfigMaps to mount Cloudflare IP ranges 8 | # - Detecting requests from Cloudflare IP ranges 9 | # - Accurate client IP logging for applications behind Cloudflare 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # 1. Ensure EasyHAProxy is installed in your cluster 14 | # kubectl create namespace easyhaproxy 15 | # kubectl apply -f https://raw.githubusercontent.com/byjg/docker-easy-haproxy/5.0.0/deploy/kubernetes/easyhaproxy-daemonset.yml 16 | # 17 | # # 2. Download Cloudflare IP ranges 18 | # curl -s https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst 19 | # curl -s https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst 20 | # 21 | # # 3. Create ConfigMap with Cloudflare IPs 22 | # kubectl create configmap cloudflare-ips \ 23 | # --from-file=cloudflare_ips.lst=cloudflare_ips.lst \ 24 | # -n easyhaproxy 25 | # 26 | # # 4. Mount the ConfigMap in EasyHAProxy deployment: 27 | # # Edit your EasyHAProxy deployment and add: 28 | # # volumeMounts: 29 | # # - name: cloudflare-ips 30 | # # mountPath: /etc/haproxy/cloudflare_ips.lst 31 | # # subPath: cloudflare_ips.lst 32 | # # volumes: 33 | # # - name: cloudflare-ips 34 | # # configMap: 35 | # # name: cloudflare-ips 36 | # ``` 37 | # 38 | # HOW TO START: 39 | # ```bash 40 | # kubectl apply -f cloudflare.yml 41 | # ``` 42 | # 43 | # HOW TO VERIFY IT'S WORKING: 44 | # ```bash 45 | # # Check resources are created 46 | # kubectl get deployment,service,ingress -l app=webapp 47 | # 48 | # # Test via port-forward 49 | # kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80 50 | # curl -H "Host: myapp.example.local" http://localhost:8080 51 | # # Expected: 200 OK with "App Behind Cloudflare" 52 | # 53 | # # In production behind Cloudflare, the plugin will restore real client IPs 54 | # # from the CF-Connecting-IP header 55 | # ``` 56 | # 57 | # CLEAN UP: 58 | # ```bash 59 | # kubectl delete -f cloudflare.yml 60 | # ``` 61 | # 62 | # ============================================================================== 63 | 64 | --- 65 | apiVersion: v1 66 | kind: Service 67 | metadata: 68 | name: webapp-service 69 | namespace: default 70 | spec: 71 | ports: 72 | - port: 8080 73 | targetPort: 8080 74 | selector: 75 | app: webapp 76 | type: ClusterIP 77 | 78 | --- 79 | apiVersion: apps/v1 80 | kind: Deployment 81 | metadata: 82 | name: webapp 83 | namespace: default 84 | spec: 85 | replicas: 3 86 | selector: 87 | matchLabels: 88 | app: webapp 89 | template: 90 | metadata: 91 | labels: 92 | app: webapp 93 | spec: 94 | containers: 95 | - name: webapp 96 | image: byjg/static-httpserver 97 | ports: 98 | - containerPort: 8080 99 | env: 100 | - name: TITLE 101 | value: "App Behind Cloudflare" 102 | resources: 103 | limits: 104 | cpu: '0.1' 105 | memory: '64Mi' 106 | requests: 107 | cpu: '0.05' 108 | memory: '32Mi' 109 | 110 | --- 111 | apiVersion: networking.k8s.io/v1 112 | kind: Ingress 113 | metadata: 114 | annotations: 115 | kubernetes.io/ingress.class: easyhaproxy-ingress 116 | 117 | # Enable Cloudflare plugin 118 | easyhaproxy.plugins: "cloudflare" 119 | 120 | # Optional: Specify custom IP list path 121 | # easyhaproxy.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst" 122 | name: webapp-ingress-cloudflare 123 | namespace: default 124 | spec: 125 | rules: 126 | - host: myapp.example.local 127 | http: 128 | paths: 129 | - backend: 130 | service: 131 | name: webapp-service 132 | port: 133 | number: 8080 134 | pathType: ImplementationSpecific 135 | -------------------------------------------------------------------------------- /examples/swarm/cloudflare.yml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # EXAMPLE: Cloudflare IP Restoration (Swarm) 3 | # ============================================================================== 4 | # 5 | # WHAT THIS DEMONSTRATES: 6 | # - Restoring original visitor IPs when behind Cloudflare CDN 7 | # - Using Docker configs to manage Cloudflare IP lists 8 | # - Service discovery in Swarm mode with plugins 9 | # - Load balancing across multiple replicas 10 | # 11 | # REQUIREMENTS (run these first): 12 | # ```bash 13 | # # Initialize Docker Swarm (if not already initialized) 14 | # docker swarm init 15 | # 16 | # # Create overlay network (idempotent) 17 | # docker network ls | grep -q easyhaproxy || docker network create --driver overlay --attachable easyhaproxy 18 | # 19 | # # Ensure EasyHAProxy is deployed 20 | # docker stack deploy -c easyhaproxy.yml easyhaproxy 21 | # 22 | # # Download Cloudflare IP ranges and create Docker config 23 | # curl https://www.cloudflare.com/ips-v4 > cloudflare_ips.lst 24 | # echo "" >> cloudflare_ips.lst 25 | # curl https://www.cloudflare.com/ips-v6 >> cloudflare_ips.lst 26 | # docker config create cloudflare_ips cloudflare_ips.lst 27 | # rm cloudflare_ips.lst 28 | # 29 | # # Add to /etc/hosts for local testing (idempotent) 30 | # grep -q "myapp.example.com" /etc/hosts || echo "127.0.0.1 myapp.example.com" | sudo tee -a /etc/hosts 31 | # ``` 32 | # 33 | # HOW TO START: 34 | # ```bash 35 | # docker stack deploy -c cloudflare.yml webapp 36 | # ``` 37 | # 38 | # HOW TO VERIFY IT'S WORKING: 39 | # ```bash 40 | # # Check stack is deployed 41 | # docker stack ls | grep webapp 42 | # # Expected: webapp stack listed 43 | # 44 | # # Check service is running 45 | # docker service ls | grep webapp_webapp 46 | # # Expected: webapp_webapp with 4/4 replicas 47 | # 48 | # # Test the application 49 | # curl -H "Host: myapp.example.com" http://localhost/ 50 | # # Expected: 200 OK with "App Behind Cloudflare" 51 | # 52 | # # Check HAProxy config includes Cloudflare IPs 53 | # docker exec $(docker ps -q -f name=easyhaproxy_haproxy) cat /etc/haproxy/haproxy.cfg | grep -A 5 "cloudflare" 54 | # # Expected: ACL rules for Cloudflare IP ranges 55 | # ``` 56 | # 57 | # CLEAN UP: 58 | # ```bash 59 | # docker stack rm webapp 60 | # # To also remove the Cloudflare IPs config: 61 | # # docker config rm cloudflare_ips 62 | # ``` 63 | # 64 | # NOTE: This plugin is most useful when your site is actually behind Cloudflare CDN. 65 | # The plugin uses the CF-Connecting-IP header to restore the original visitor IP. 66 | # 67 | # ============================================================================== 68 | 69 | version: "3.7" 70 | 71 | services: 72 | haproxy: 73 | image: byjg/easy-haproxy:5.0.0 74 | volumes: 75 | - /var/run/docker.sock:/var/run/docker.sock 76 | configs: 77 | - source: cloudflare_ips 78 | target: /etc/haproxy/cloudflare_ips.lst 79 | deploy: 80 | replicas: 1 81 | placement: 82 | constraints: 83 | - node.role == manager 84 | environment: 85 | EASYHAPROXY_DISCOVER: swarm 86 | HAPROXY_USERNAME: admin 87 | HAPROXY_PASSWORD: password 88 | HAPROXY_STATS_PORT: 1936 89 | ports: 90 | - "80:80/tcp" 91 | - "443:443/tcp" 92 | - "1936:1936/tcp" 93 | networks: 94 | - easyhaproxy 95 | 96 | # Web application behind Cloudflare 97 | webapp: 98 | image: byjg/static-httpserver 99 | environment: 100 | TITLE: "App Behind Cloudflare" 101 | deploy: 102 | replicas: 4 103 | labels: 104 | easyhaproxy.http.host: "myapp.example.com" 105 | easyhaproxy.http.port: "80" 106 | easyhaproxy.http.localport: "8080" 107 | 108 | # Enable Cloudflare plugin 109 | easyhaproxy.http.plugins: "cloudflare" 110 | 111 | # Optional: Specify custom IP list path 112 | # easyhaproxy.http.plugin.cloudflare.ip_list_path: "/etc/haproxy/cloudflare_ips.lst" 113 | networks: 114 | - easyhaproxy 115 | 116 | networks: 117 | easyhaproxy: 118 | external: true 119 | 120 | configs: 121 | cloudflare_ips: 122 | external: true 123 | -------------------------------------------------------------------------------- /src/tests/expected/services-redirect-ssl.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 5 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 6 | ssl-default-bind-options no-sslv3 no-tls-tickets 7 | 8 | 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:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA 9 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 10 | ssl-default-server-options no-sslv3 no-tls-tickets 11 | 12 | ssl-dh-param-file /etc/haproxy/dhparam-1024 13 | 14 | 15 | defaults 16 | log global 17 | option httplog 18 | 19 | timeout connect 3s 20 | timeout client 10s 21 | timeout server 10m 22 | 23 | 24 | 25 | frontend http_in_80 26 | bind *:80 27 | mode http 28 | 29 | acl is_rule_host2_local_80_1 hdr(host) -i host2.local 30 | acl is_rule_host2_local_80_2 hdr(host) -i host2.local:80 31 | http-request redirect scheme https code 301 if is_rule_host2_local_80_1 OR is_rule_host2_local_80_2 32 | 33 | acl is_rule_host1_local_80_1 hdr(host) -i host1.local 34 | acl is_rule_host1_local_80_2 hdr(host) -i host1.local:80 35 | http-request redirect scheme https code 301 if is_rule_host1_local_80_1 OR is_rule_host1_local_80_2 36 | 37 | backend srv_host2_local_80 38 | balance roundrobin 39 | mode http 40 | option forwardfor 41 | http-request set-header X-Forwarded-Port %[dst_port] 42 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 43 | server srv-0 3571640c480a:80 check weight 1 44 | backend srv_host1_local_80 45 | balance roundrobin 46 | mode http 47 | option forwardfor 48 | http-request set-header X-Forwarded-Port %[dst_port] 49 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 50 | server srv-0 5b69bc7fea1b:80 check weight 1 51 | 52 | frontend http_in_443 53 | bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 54 | mode http 55 | 56 | acl is_rule_host2_local_443_1 hdr(host) -i host2.local 57 | acl is_rule_host2_local_443_2 hdr(host) -i host2.local:443 58 | use_backend srv_host2_local_443 if is_rule_host2_local_443_1 OR is_rule_host2_local_443_2 59 | 60 | acl is_rule_host1_local_443_1 hdr(host) -i host1.local 61 | acl is_rule_host1_local_443_2 hdr(host) -i host1.local:443 62 | use_backend srv_host1_local_443 if is_rule_host1_local_443_1 OR is_rule_host1_local_443_2 63 | 64 | backend srv_host2_local_443 65 | balance roundrobin 66 | mode http 67 | option forwardfor 68 | http-request set-header X-Forwarded-Port %[dst_port] 69 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 70 | server srv-0 3571640c480a:8080 check weight 1 71 | backend srv_host1_local_443 72 | balance roundrobin 73 | mode http 74 | option forwardfor 75 | http-request set-header X-Forwarded-Port %[dst_port] 76 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 77 | server srv-0 5b69bc7fea1b:8080 check weight 1 78 | 79 | backend certbot_backend 80 | mode http 81 | server certbot 127.0.0.1:2080 82 | -------------------------------------------------------------------------------- /examples/generate-keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Generate SSL Certificates and JWT Keys for EasyHAProxy Examples 5 | # This script creates all .pem files needed for the examples directory 6 | 7 | echo "Generating SSL certificates and JWT keys for EasyHAProxy examples..." 8 | echo "" 9 | 10 | # Create necessary directories 11 | mkdir -p examples/static 12 | mkdir -p examples/docker 13 | mkdir -p examples/docker/certs/haproxy 14 | mkdir -p examples/swarm/certs 15 | 16 | # ============================================================================ 17 | # Generate SSL Certificate for host1.local (4096-bit RSA, 10-year validity) 18 | # ============================================================================ 19 | echo "Generating host1.local certificate (4096-bit RSA, 10-year validity)..." 20 | openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ 21 | -keyout examples/static/host1.local.pem \ 22 | -out examples/static/host1.local.pem \ 23 | -subj "/C=US/ST=State/L=City/O=Organization/CN=host1.local" 24 | 25 | # Copy to swarm directory 26 | cp examples/static/host1.local.pem examples/swarm/certs/host1.local.pem 27 | echo " Created host1.local.pem (4096-bit, 10 years)" 28 | echo " - examples/static/host1.local.pem" 29 | echo " - examples/swarm/certs/host1.local.pem" 30 | echo "" 31 | 32 | # ============================================================================ 33 | # Generate SSL Certificate for host2.local (2048-bit RSA, 1-year validity) 34 | # ============================================================================ 35 | echo "Generating host2.local certificate (2048-bit RSA, 1-year validity)..." 36 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 37 | -keyout examples/docker/host2.local.pem \ 38 | -out examples/docker/host2.local.pem \ 39 | -subj "/C=US/ST=State/L=City/O=Organization/CN=host2.local" 40 | 41 | # Copy to swarm directory 42 | cp examples/docker/host2.local.pem examples/swarm/certs/host2.local.pem 43 | echo " Created host2.local.pem (2048-bit, 1 year)" 44 | echo " - examples/docker/host2.local.pem" 45 | echo " - examples/swarm/certs/host2.local.pem" 46 | echo "" 47 | 48 | # ============================================================================ 49 | # Generate JWT RSA Key Pair (2048-bit) 50 | # ============================================================================ 51 | echo "Generating JWT RSA key pair (2048-bit)..." 52 | 53 | # Generate private key 54 | openssl genrsa -out examples/docker/jwt_private.pem 2048 55 | 56 | # Extract public key 57 | openssl rsa -in examples/docker/jwt_private.pem -pubout -out examples/docker/jwt_pubkey.pem 58 | 59 | echo " Created JWT key pair (2048-bit)" 60 | echo " - examples/docker/jwt_private.pem (private key)" 61 | echo " - examples/docker/jwt_pubkey.pem (public key)" 62 | echo "" 63 | 64 | # ============================================================================ 65 | # Generate Placeholder Certificate 66 | # ============================================================================ 67 | echo "Generating placeholder certificate..." 68 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 69 | -keyout examples/docker/certs/haproxy/.place_holder_cert.pem \ 70 | -out examples/docker/certs/haproxy/.place_holder_cert.pem \ 71 | -subj "/C=US/ST=State/L=City/O=Organization/CN=placeholder" 72 | 73 | echo " Created placeholder certificate" 74 | echo " - examples/docker/certs/haproxy/.place_holder_cert.pem" 75 | echo "" 76 | 77 | # ============================================================================ 78 | # Summary 79 | # ============================================================================ 80 | echo "============================================" 81 | echo "All certificates and keys generated successfully!" 82 | echo "============================================" 83 | echo "" 84 | echo "SSL Certificates:" 85 | echo " - host1.local (4096-bit, 10 years)" 86 | echo " - host2.local (2048-bit, 1 year)" 87 | echo "" 88 | echo "JWT Keys:" 89 | echo " - jwt_private.pem (private key for signing)" 90 | echo " - jwt_pubkey.pem (public key for validation)" 91 | echo "" 92 | echo "IMPORTANT NOTES:" 93 | echo " - These are self-signed certificates for TESTING ONLY" 94 | echo " - DO NOT use these certificates in production" 95 | echo " - Browsers will show security warnings for self-signed certificates" 96 | echo " - JWT keys should be kept secure and rotated regularly" 97 | echo "" 98 | -------------------------------------------------------------------------------- /docs/Plugins/deny-pages.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 20 3 | --- 4 | 5 | # Deny Pages Plugin 6 | 7 | **Type:** Domain Plugin 8 | **Runs:** Once for each discovered domain/host 9 | 10 | ## Overview 11 | 12 | The Deny Pages plugin blocks access to specific paths for a domain, returning a configurable HTTP status code. 13 | 14 | ## Why Use It 15 | 16 | Protect admin panels, internal APIs, or debugging endpoints from public access. 17 | 18 | ## Configuration Options 19 | 20 | | Option | Description | Default | 21 | |---------------|----------------------------------------|------------| 22 | | `enabled` | Enable/disable plugin | `true` | 23 | | `paths` | Comma-separated list of paths to block | (required) | 24 | | `status_code` | HTTP status code to return | `403` | 25 | 26 | ## Configuration Examples 27 | 28 | ### Docker/Docker Compose (Basic) 29 | 30 | ```yaml 31 | services: 32 | webapp: 33 | labels: 34 | easyhaproxy.http.host: example.com 35 | easyhaproxy.http.plugins: deny_pages 36 | easyhaproxy.http.plugin.deny_pages.paths: /admin,/private,/debug 37 | easyhaproxy.http.plugin.deny_pages.status_code: 404 38 | ``` 39 | 40 | ### WordPress Protection 41 | 42 | ```yaml 43 | labels: 44 | easyhaproxy.http.host: wordpress.example.com 45 | easyhaproxy.http.plugins: deny_pages 46 | easyhaproxy.http.plugin.deny_pages.paths: /wp-admin,/wp-login.php,/.env 47 | easyhaproxy.http.plugin.deny_pages.status_code: 404 48 | ``` 49 | 50 | ### Kubernetes Annotations 51 | 52 | ```yaml 53 | apiVersion: networking.k8s.io/v1 54 | kind: Ingress 55 | metadata: 56 | annotations: 57 | easyhaproxy.plugins: "deny_pages" 58 | easyhaproxy.plugin.deny_pages.paths: "/admin,/private" 59 | easyhaproxy.plugin.deny_pages.status_code: "403" 60 | spec: 61 | rules: 62 | - host: example.com 63 | http: 64 | paths: 65 | - path: / 66 | backend: 67 | service: 68 | name: webapp 69 | port: 70 | number: 80 71 | ``` 72 | 73 | ### Static YAML Configuration 74 | 75 | ```yaml 76 | # /etc/haproxy/static/config.yaml 77 | easymapping: 78 | - host: example.com 79 | port: 80 80 | container: webapp:80 81 | plugins: 82 | - deny_pages 83 | plugin_config: 84 | deny_pages: 85 | paths: /admin,/private,/debug 86 | status_code: 403 87 | ``` 88 | 89 | ### Multiple Plugins (with Cloudflare) 90 | 91 | ```yaml 92 | labels: 93 | easyhaproxy.http.host: secure-app.com 94 | easyhaproxy.http.plugins: cloudflare,deny_pages 95 | easyhaproxy.http.plugin.deny_pages.paths: /admin,/config 96 | easyhaproxy.http.plugin.deny_pages.status_code: 403 97 | ``` 98 | 99 | ### Environment Variables 100 | 101 | Configure Deny Pages plugin defaults for all domains: 102 | 103 | | Environment Variable | Config Key | Type | Default | Description | 104 | |---------------------------------------------|---------------|---------|---------|----------------------------------------| 105 | | `EASYHAPROXY_PLUGIN_DENY_PAGES_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains | 106 | | `EASYHAPROXY_PLUGIN_DENY_PAGES_PATHS` | `paths` | string | - | Comma-separated list of paths to block | 107 | | `EASYHAPROXY_PLUGIN_DENY_PAGES_STATUS_CODE` | `status_code` | integer | `403` | HTTP status code to return | 108 | 109 | **Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations. 110 | 111 | ## Generated HAProxy Configuration 112 | 113 | ```haproxy 114 | # Deny Pages - Block specific paths 115 | acl denied_path path_beg /admin /private /debug 116 | http-request deny deny_status 404 if denied_path 117 | ``` 118 | 119 | ## Important Notes 120 | 121 | - The plugin runs once per domain during the discovery cycle 122 | - Path matching uses `path_beg` (prefix matching), so `/admin` blocks `/admin/*` too 123 | - Consider using `404` instead of `403` to hide the existence of blocked paths 124 | - Works well in combination with other security plugins 125 | 126 | ## Related Documentation 127 | 128 | - [Plugin System Overview](../plugins.md) 129 | - [Container Labels Reference](../container-labels.md) 130 | -------------------------------------------------------------------------------- /src/tests/expected/docker.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | 25 | 26 | frontend stats 27 | bind *:1936 28 | mode http 29 | http-request use-service prometheus-exporter if { path /metrics } 30 | stats enable 31 | stats hide-version 32 | stats realm Haproxy\ Statistics 33 | stats uri / 34 | default_backend srv_stats 35 | 36 | backend srv_stats 37 | mode http 38 | server Local 127.0.0.1:1936 39 | 40 | frontend http_in_443 41 | bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 42 | mode http 43 | 44 | acl is_rule_hostssl_local_443_1 hdr(host) -i hostssl.local 45 | acl is_rule_hostssl_local_443_2 hdr(host) -i hostssl.local:443 46 | use_backend srv_hostssl_local_443 if is_rule_hostssl_local_443_1 OR is_rule_hostssl_local_443_2 47 | 48 | acl is_rule_host2_local_443_1 hdr(host) -i host2.local 49 | acl is_rule_host2_local_443_2 hdr(host) -i host2.local:443 50 | use_backend srv_host2_local_443 if is_rule_host2_local_443_1 OR is_rule_host2_local_443_2 51 | 52 | backend srv_hostssl_local_443 53 | balance roundrobin 54 | mode http 55 | option forwardfor 56 | http-request set-header X-Forwarded-Port %[dst_port] 57 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 58 | server srv-0 test2_processor_docker:8080 check weight 1 59 | backend srv_host2_local_443 60 | balance roundrobin 61 | mode http 62 | option forwardfor 63 | http-request set-header X-Forwarded-Port %[dst_port] 64 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 65 | server srv-0 test_processor_docker:9000 check weight 1 66 | 67 | frontend http_in_80 68 | bind *:80 69 | mode http 70 | 71 | acl is_rule_host1_local_80_1 hdr(host) -i host1.local 72 | acl is_rule_host1_local_80_2 hdr(host) -i host1.local:80 73 | use_backend srv_host1_local_80 if is_rule_host1_local_80_1 OR is_rule_host1_local_80_2 74 | 75 | backend srv_host1_local_80 76 | balance roundrobin 77 | mode http 78 | option forwardfor 79 | http-request set-header X-Forwarded-Port %[dst_port] 80 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 81 | server srv-0 test_processor_docker:8080 check weight 1 82 | 83 | frontend http_in_90 84 | bind *:90 85 | mode http 86 | 87 | acl is_rule_host2_local_90_1 hdr(host) -i host2.local 88 | acl is_rule_host2_local_90_2 hdr(host) -i host2.local:90 89 | acl is_certbot_host2_local_90 path_beg /.well-known/acme-challenge/ 90 | use_backend certbot_backend if is_certbot_host2_local_90 is_rule_host2_local_90_1 OR is_certbot_host2_local_90 is_rule_host2_local_90_2 91 | use_backend srv_host2_local_90 if is_rule_host2_local_90_1 OR is_rule_host2_local_90_2 92 | 93 | backend srv_host2_local_90 94 | balance roundrobin 95 | mode http 96 | option forwardfor 97 | http-request set-header X-Forwarded-Port %[dst_port] 98 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 99 | server srv-0 test_processor_docker:9000 check weight 1 100 | 101 | backend certbot_backend 102 | mode http 103 | server certbot 127.0.0.1:2080 104 | -------------------------------------------------------------------------------- /src/tests/expected/services-letsencrypt.txt: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 2000 4 | tune.ssl.default-dh-param 2048 5 | 6 | # intermediate configuration 7 | 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 8 | ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 9 | ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 10 | 11 | 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 12 | ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 13 | ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets 14 | 15 | ssl-dh-param-file /etc/haproxy/dhparam 16 | 17 | defaults 18 | log global 19 | option httplog 20 | 21 | timeout connect 3s 22 | timeout client 10s 23 | timeout server 10m 24 | errorfile 400 /etc/haproxy/errors-custom/400.http 25 | errorfile 403 /etc/haproxy/errors-custom/403.http 26 | errorfile 408 /etc/haproxy/errors-custom/408.http 27 | errorfile 500 /etc/haproxy/errors-custom/500.http 28 | errorfile 502 /etc/haproxy/errors-custom/502.http 29 | errorfile 503 /etc/haproxy/errors-custom/503.http 30 | errorfile 504 /etc/haproxy/errors-custom/504.http 31 | 32 | 33 | frontend stats 34 | bind *:1936 35 | mode http 36 | http-request use-service prometheus-exporter if { path /metrics } 37 | stats enable 38 | stats hide-version 39 | stats realm Haproxy\ Statistics 40 | stats uri / 41 | stats auth admin:password 42 | default_backend srv_stats 43 | 44 | backend srv_stats 45 | mode http 46 | server Local 127.0.0.1:1936 47 | 48 | frontend http_in_80 49 | bind *:80 50 | mode http 51 | 52 | acl is_rule_test_example_org_80_1 hdr(host) -i test.example.org 53 | acl is_rule_test_example_org_80_2 hdr(host) -i test.example.org:80 54 | acl is_certbot_test_example_org_80 path_beg /.well-known/acme-challenge/ 55 | http-request redirect scheme https code 301 if !is_certbot_test_example_org_80 is_rule_test_example_org_80_1 OR !is_certbot_test_example_org_80 is_rule_test_example_org_80_2 56 | use_backend certbot_backend if is_certbot_test_example_org_80 is_rule_test_example_org_80_1 OR is_certbot_test_example_org_80 is_rule_test_example_org_80_2 57 | 58 | acl is_rule_test2_example_org_80_1 hdr(host) -i test2.example.org 59 | acl is_rule_test2_example_org_80_2 hdr(host) -i test2.example.org:80 60 | use_backend srv_test2_example_org_80 if is_rule_test2_example_org_80_1 OR is_rule_test2_example_org_80_2 61 | 62 | backend srv_test_example_org_80 63 | balance roundrobin 64 | mode http 65 | option forwardfor 66 | http-request set-header X-Forwarded-Port %[dst_port] 67 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 68 | server srv-0 f5c645a0dfc6:80 check weight 1 69 | server srv-1 b63438410b6a:80 check weight 1 70 | backend srv_test2_example_org_80 71 | balance roundrobin 72 | mode http 73 | option forwardfor 74 | http-request set-header X-Forwarded-Port %[dst_port] 75 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 76 | server srv-0 83d57d592e26:8080 check weight 1 77 | 78 | frontend http_in_443 79 | bind *:443 ssl crt /certs/certbot/ alpn h2,http/1.1 crt /certs/haproxy/ alpn h2,http/1.1 80 | mode http 81 | 82 | acl is_rule_test_example_org_443_1 hdr(host) -i test.example.org 83 | acl is_rule_test_example_org_443_2 hdr(host) -i test.example.org:443 84 | use_backend srv_test_example_org_443 if is_rule_test_example_org_443_1 OR is_rule_test_example_org_443_2 85 | 86 | backend srv_test_example_org_443 87 | balance roundrobin 88 | mode http 89 | option forwardfor 90 | http-request set-header X-Forwarded-Port %[dst_port] 91 | http-request add-header X-Forwarded-Proto https if { ssl_fc } 92 | server srv-0 f5c645a0dfc6:80 check weight 1 verify none 93 | server srv-1 b63438410b6a:80 check weight 1 verify none 94 | 95 | backend certbot_backend 96 | mode http 97 | server certbot 127.0.0.1:2080 98 | -------------------------------------------------------------------------------- /docs/Plugins/ip-whitelist.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 19 3 | --- 4 | 5 | # IP Whitelist Plugin 6 | 7 | **Type:** Domain Plugin 8 | **Runs:** Once for each discovered domain/host 9 | 10 | ## Overview 11 | 12 | The IP Whitelist plugin restricts access to a domain to only specific IP addresses or CIDR ranges. 13 | 14 | ## Why Use It 15 | 16 | Restrict access to internal tools, admin panels, or staging environments to only trusted IP addresses. 17 | 18 | ## Configuration Options 19 | 20 | | Option | Description | Default | 21 | |---------------|--------------------------------------------------|------------| 22 | | `enabled` | Enable/disable plugin | `true` | 23 | | `allowed_ips` | Comma-separated list of IPs/CIDR ranges to allow | (required) | 24 | | `status_code` | HTTP status code to return for blocked IPs | `403` | 25 | 26 | ## Configuration Examples 27 | 28 | ### Docker/Docker Compose (Basic) 29 | 30 | ```yaml 31 | services: 32 | admin: 33 | labels: 34 | easyhaproxy.http.host: admin.example.com 35 | easyhaproxy.http.plugins: ip_whitelist 36 | easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 192.168.1.0/24,10.0.0.5 37 | easyhaproxy.http.plugin.ip_whitelist.status_code: 403 38 | ``` 39 | 40 | ### Office Network Access 41 | 42 | ```yaml 43 | labels: 44 | easyhaproxy.http.host: admin.example.com 45 | easyhaproxy.http.plugins: ip_whitelist 46 | easyhaproxy.http.plugin.ip_whitelist.allowed_ips: 203.0.113.0/24,198.51.100.42 47 | ``` 48 | 49 | ### Kubernetes Annotations 50 | 51 | ```yaml 52 | apiVersion: networking.k8s.io/v1 53 | kind: Ingress 54 | metadata: 55 | annotations: 56 | easyhaproxy.plugins: "ip_whitelist" 57 | easyhaproxy.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.5" 58 | easyhaproxy.plugin.ip_whitelist.status_code: "403" 59 | spec: 60 | rules: 61 | - host: admin.example.com 62 | http: 63 | paths: 64 | - path: / 65 | backend: 66 | service: 67 | name: admin-panel 68 | port: 69 | number: 80 70 | ``` 71 | 72 | ### Static YAML Configuration 73 | 74 | ```yaml 75 | # /etc/haproxy/static/config.yaml 76 | easymapping: 77 | - host: admin.example.com 78 | port: 443 79 | container: admin-panel:443 80 | plugins: 81 | - ip_whitelist 82 | plugin_config: 83 | ip_whitelist: 84 | allowed_ips: 192.168.1.0/24,10.0.0.5 85 | status_code: 403 86 | ``` 87 | 88 | ### Environment Variables 89 | 90 | Configure IP Whitelist plugin defaults for all domains: 91 | 92 | | Environment Variable | Config Key | Type | Default | Description | 93 | |-----------------------------------------------|---------------|----------|---------|--------------------------------------------------| 94 | | `EASYHAPROXY_PLUGIN_IP_WHITELIST_ENABLED` | `enabled` | boolean | `true` | Enable/disable plugin for all domains | 95 | | `EASYHAPROXY_PLUGIN_IP_WHITELIST_ALLOWED_IPS` | `allowed_ips` | string | - | Comma-separated list of IPs/CIDR ranges to allow | 96 | | `EASYHAPROXY_PLUGIN_IP_WHITELIST_STATUS_CODE` | `status_code` | integer | `403` | HTTP status code to return for blocked IPs | 97 | 98 | **Note:** Environment variables set defaults for ALL domains. To configure per-domain, use container labels or Kubernetes annotations. 99 | 100 | ## Generated HAProxy Configuration 101 | 102 | ```haproxy 103 | # IP Whitelist - Only allow specific IPs 104 | acl whitelisted_ip src 192.168.1.0/24 10.0.0.5 105 | http-request deny deny_status 403 if !whitelisted_ip 106 | ``` 107 | 108 | ## IP Address Formats 109 | 110 | The plugin supports: 111 | - **Single IPs:** `10.0.0.5`, `203.0.113.42` 112 | - **CIDR ranges:** `192.168.1.0/24`, `10.0.0.0/8` 113 | - **Multiple entries:** Comma-separated list of IPs and/or CIDR ranges 114 | 115 | ## Important Notes 116 | 117 | - **Warning:** This blocks ALL IPs except those in the whitelist. Make sure to include your own IP! 118 | - The plugin runs once per domain during the discovery cycle 119 | - Test thoroughly before deploying to production 120 | - Consider using VPN CIDR ranges for remote access 121 | - Works well with staging and admin environments 122 | 123 | ## Related Documentation 124 | 125 | - [Plugin System Overview](../plugins.md) 126 | - [Container Labels Reference](../container-labels.md) 127 | --------------------------------------------------------------------------------