diff --git a/README.md b/README.md index 0c9380e..d9824d1 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,15 @@ EasyHAProxy can detect and configure HAProxy automatically on the following plat - Docker Swarm - Kubernetes +## EasyHAProxy Mission + +Easy to set up and low configuration to numerous features. + ## Features EasyHAProxy will discover the services based on the Docker Tags of the containers running on a Docker host or Docker Swarm cluster and dynamically set up the `haproxy.cfg`. Below, EasyHAProxy main features: -- Automatic SSL Certificate issued by any ACME compatible service as Letsencrypt and ZeroSSL +- Support Automatic Certificate Management Environment (ACME) protocol compatible with Let's encrypt and other issuers. - Set your custom SSL certificates - Balance traffic between multiple replicas - Set SSL with three different levels of validations and according to the most recent definitions. @@ -59,6 +63,13 @@ Or you can install using tools: [![MicroK8s](docs/easyhaproxy_microk8s.png)](docs/microk8s.md) [![Dokku](docs/easyhaproxy_dokku.png)](docs/dokku.md) +## Special Topics + +If you already set up the EasyHAProxy, is time to go deeper: + +- [Custom SSL](docs/ssl.md) +- Automatic Certificate Issuing (e.g. Let's Encrypt) + ## See EasyHAProxy in action Click on the image to see the videos (use HD for better visualization) diff --git a/docs/acme.md b/docs/acme.md new file mode 100644 index 0000000..9de7088 --- /dev/null +++ b/docs/acme.md @@ -0,0 +1,36 @@ +# SSL - Automatic Certificate Management Environment (ACME) + +The Automatic Certificate Management Environment (ACME) protocol +allow automating interactions between certificate authorities and their users' servers, +allowing the automated deployment of public key infrastructure. + +Most of the issuers offers Automatic Issuing free of cost. + +To enable the ACME protocol we need to enable Certbot in EasyHAProxy by setting up to 4 environment variables: + +- EASYHAPROXY_CERTBOT_AUTOCONFIG (optional): Will use pre-sets for your Certificate Authority (CA). See table below. +- EASYHAPROXY_CERTBOT_EMAIL (required): Your email in the certificate authority. +- EASYHAPROXY_CERTBOT_SERVER (optional): The ACME Endpoint of your certificate authority. If you use AUTOCONFIG, it is set automatically. See table below. +- EASYHAPROXY_CERTBOT_EAB_KID (optional): External Account Binding (EAB) Key Identifier (KID) provided by your certificate authority. Some CA require it. See table below. +- EASYHAPROXY_CERTBOT_EAB_HMAC_KEY (optional): External Account Binding (EAB) HMAC Key provided by your certificate authority. Some CA require it. See table below. + +Here are detailed instructions per Certificate Authority (CA). If anyone is missing, please let's know. + +| CA | Auto Config | Free? | Account Required? | EAB KID? | EAB HMAC Key? | More Info | +|----------------------|------------------|-------|--------------------|----------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Let's Encrypt | - | Yes | No | No | No | [Link](letsencrypt.md) | +| Let's Encrypt (Test) | letsencrypt_test | Yes | No | No | No | [Link](letsencrypt.md) | +| ZeroSSL | zerossl | Yes | No | No | No | [Link](https://zerossl.com/documentation/acme/) | +| BuyPass | buypass | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) | +| BuyPass (test) | buypass_test | Yes | No | No | No | [Link](https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints-updated-14-05-2020) | +| Google | google | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) | +| Google Test | google_test | Yes | Yes | Yes | Yes | [Link](https://cloud.google.com/blog/products/identity-security/automate-public-certificate-lifecycle-management-via--acme-client-api) | +| SSLCOM RCA | sslcom_rca | Yes | No. Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) | +| SSLCOM ECC | sslcom_ecc | Yes | No. Keys by email. | Yes | Yes | [Link](https://www.ssl.com/blogs/sslcom-supports-acme-protocol-ssl-tls-certificate-automation/) | +| Digicert | - | No | Yes | Yes | Yes | [Link](https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/use-a-third-party-acme-client-for-host-automations.html) | +| Entrust | - | No | Yes | Yes | Yes | [Link](https://www.entrust.com/knowledgebase/ssl/how-to-use-acme-to-install-ssl-tls-certificates-in-entrust-certificate-services-apache) | +| Sectigo | - | No | Yes | Yes | Yes | [Link](https://www.sectigo.com/resource-library/sectigos-acme-automation) | + +This configuration is global. After set up ACME properly, is necessary enable for each domain the certificate request. + +To do that add the label: `easyhaproxy..certbot=true`. See the method of installation you are using to learn how to set up properly. diff --git a/docs/container-labels.md b/docs/container-labels.md index 56b5a1f..41e89b4 100644 --- a/docs/container-labels.md +++ b/docs/container-labels.md @@ -12,7 +12,7 @@ | easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` is enabled. | *empty* | base64 cert + key | | easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | | easyhaproxy.[definition].health-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | -| easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. | false | true OR false | +| easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | false | true OR false | | easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | | easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use* this with `ssl` or `certbot` parameters | false | true OR false | diff --git a/docs/docker.md b/docs/docker.md index edf9fc6..643ad48 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -54,13 +54,9 @@ Please follow the [docker label configuration](container-labels.md) to see other You can configure the behavior of the EasyHAProxy by setup specific environment variables. To get a list of the variables, please follow the [docker container environment](docker-environment.md) -## Setup certificates with Letsencrypt +## Setup certificates with ACME (e.g. Letsencrypt) -Follow [this link](letsencrypt.md) - -## Setup certificates with ZeroSSL - -Follow [this link](zerossl.md) +Follow [this link](acme.md) ## Setup your own certificates diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 005687d..12d5c5d 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -90,7 +90,7 @@ Caveats: **Important**: The annotations are per ingress and applied to all hosts in that ingress configuration. -## Letsencrypt +## Certbot / ACME / Letsencrypt It is necessary add the annotation `easyhaproxy.certbot` to the ingress configuration: @@ -106,7 +106,9 @@ spec: .... ``` -Make sure your cluster is accessible both through ports 80 and 443. +More info [here](acme.md). + +Make sure your cluster is accessible both through ports 80 and 443. ## Custom SSL Certificates diff --git a/docs/ssl.md b/docs/ssl.md index c2f551d..9b49e0c 100644 --- a/docs/ssl.md +++ b/docs/ssl.md @@ -1,6 +1,6 @@ # Setup custom certificates -You can use your certificates with EasyHAProxy. +You can use your own certificates with EasyHAProxy. You just need to let EasyHAProxy know that certificate. There are two ways to do that. diff --git a/docs/zerossl.md b/docs/zerossl.md deleted file mode 100644 index cd7ff27..0000000 --- a/docs/zerossl.md +++ /dev/null @@ -1,70 +0,0 @@ -# ZeroSSL - -EasyHAProxy can issue a ZeroSSL certificate. Follow the steps below: - -## Getting the Keys from ZeroSSL - -- Login to [ZeroSSL](https://zerossl.com?fpr=byjg) -- If you don't have an account you can use our referral link: [https://zerossl.com?fpr=byjg](https://zerossl.com?fpr=byjg) -- If you created recently and would like to support us open an issue, and I'll try to add you as our referred. -- In the ZeroSSL dashboard click in 'Developer' and the Generate EAB Keys - -![zerossl_1.png](zerossl_1.png) - -- Once you generate the keys, please **take note**. We will need this later, and it isn't saved anywhere. - -![zerossl_2.png](zerossl_2.png) - -```text -Important: if you to this process again the old values will be lost and you need to setup EasyHAProxy again. -``` - -## Setting up EasyHAProxy - -Run the EasyHAProxy container with the following parameters: - -```bash -docker run \ - ... \ - -e EASYHAPROXY_CERTBOT_EMAIL="your zerossl email" \ - -e EASYHAPROXY_CERTBOT_EAB_KID="the eab_kid from the previous step" \ - -e EASYHAPROXY_CERTBOT_EAB_HMAC_KEY="the eab_hmac_key from the previous step" \ - -e EASYHAPROXY_CERTBOT_SERVER="https://acme.zerossl.com/v2/DV90" \ - -p 80:80 \ - -p 443:443 \ - -v /path/to/guest/certbot/certs:/certs/certbot \ - ... \ - byjg/easy-haproxy -``` - -Notes: - -- If you don't setup all `EASYHAPROXY_CERTBOT_*` environment variables with the proper values, EasyHAProxy will fail silently and **will not request** a certificate. -- The ports 80 and 443 needs to accessible through the internet as [Let's Encrypt requirement](https://letsencrypt.org/docs/allow-port-80/) - -**It is required you to persist the container folder `/certs/certbot` outside the container.** - -You cannot delete or change it contents. -If you do not persist, or change/delete the contents, Let's encrypt will not work properly. - - -## Setting up your container to use ZeroSSL - -```bash -docker run \ - ... \ - --label easyhaproxy.express.port=80 \ - --label easyhaproxy.express.localport=3000 \ - --label easyhaproxy.express.host=example.org \ - --label easyhaproxy.express.certbot=true \ - ... \ - some/myimage -``` - -Requirements: - -- Your container **must** listen to port 80. Letsencrypt will not issue the certificate if `easyhaproxy..port` is in another port, and EasyHAProxy will fail silently. -- You cannot set port 443 for the container with the Letsencrypt because EasyHAProxy will create this port automatically once the certificate is issued. - ----- -[Open source ByJG](http://opensource.byjg.com) diff --git a/docs/zerossl_1.png b/docs/zerossl_1.png deleted file mode 100644 index 06810d1..0000000 Binary files a/docs/zerossl_1.png and /dev/null differ diff --git a/docs/zerossl_2.png b/docs/zerossl_2.png deleted file mode 100644 index aeb5841..0000000 Binary files a/docs/zerossl_2.png and /dev/null differ diff --git a/src/functions/__init__.py b/src/functions/__init__.py index e595dd4..4349d71 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -28,7 +28,7 @@ class ContainerEnv: "EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" env_vars["certbot"] = { - "autoconfig": os.getenv("EASYHAPROXY_CERTBOT_AUTOCONFIG", "letsencrypt"), + "autoconfig": os.getenv("EASYHAPROXY_CERTBOT_AUTOCONFIG", ""), "email": os.getenv("EASYHAPROXY_CERTBOT_EMAIL", ""), "server": os.getenv("EASYHAPROXY_CERTBOT_SERVER", False), "eab_kid": os.getenv("EASYHAPROXY_CERTBOT_EAB_KID", ""), @@ -164,8 +164,9 @@ class Functions: Functions.log(source, Functions.WARN, process.stderr.readlines()) break - return output + return [return_code, output] except Exception as e: + return [-99, e] Functions.log(source, Functions.ERROR, "%s" % e) diff --git a/src/tests/fixtures/run_bash.sh b/src/tests/fixtures/run_bash.sh new file mode 100755 index 0000000..75a66bb --- /dev/null +++ b/src/tests/fixtures/run_bash.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Just return the exit code +echo "Processing run_bash.sh" +if [ -n "$1" ]; then + exit "$1" +fi diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py index 3c60928..46df944 100644 --- a/src/tests/test_containerenv.py +++ b/src/tests/test_containerenv.py @@ -8,7 +8,7 @@ def test_container_env_empty(): "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -25,7 +25,7 @@ def test_container_env_customerrors(): "customerrors": True, "ssl_mode": "default", "lookup_label": "easyhaproxy", - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -42,7 +42,7 @@ def test_container_env_sslmode(): "customerrors": False, "ssl_mode": "strict", "lookup_label": "easyhaproxy", - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -60,7 +60,7 @@ def test_container_env_stats(): "customerrors": False, "ssl_mode": "default", "lookup_label": "easyhaproxy", - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -84,7 +84,7 @@ def test_container_env_stats_password(): "port": "1936" }, - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -108,7 +108,7 @@ def test_container_env_stats_password_2(): "password": "xyz", "port": "2101" }, - "certbot": {"autoconfig": "letsencrypt", + "certbot": {"autoconfig": "", "eab_hmac_key": "", "eab_kid": "", "email": "", @@ -128,11 +128,11 @@ def test_container_env_certbot_email(): "ssl_mode": "default", "lookup_label": "easyhaproxy", "certbot": { - "autoconfig": "letsencrypt", + "autoconfig": "", 'eab_hmac_key': "", 'eab_kid': "", "email": "acme@example.org", - "server": 'https://acme-v02.api.letsencrypt.org/directory' + "server": False } } == ContainerEnv.read() finally: @@ -150,7 +150,7 @@ def test_container_env_certbot_full(): "ssl_mode": "default", "lookup_label": "easyhaproxy", "certbot": { - "autoconfig": "letsencrypt", + "autoconfig": "", "email": "acme@example.org", "server": "schema://url/a", 'eab_hmac_key': 'eab_hmac_key', diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index 861e8d1..728ab80 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -65,8 +65,9 @@ def test_functions_run_bash_log_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 1'", log_output=True, - return_result=False) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 1'", log_output=True, + return_result=False) + assert return_code == 0 assert result == [] assert len(Functions.debug_log) == 1 assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 1", Functions.debug_log[0]) @@ -78,8 +79,9 @@ def test_functions_run_bash_no_log_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 2'", log_output=False, - return_result=False) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 2'", log_output=False, + return_result=False) + assert return_code == 0 assert result == [] assert len(Functions.debug_log) == 0 finally: @@ -90,8 +92,9 @@ def test_functions_run_bash_return(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 3'", log_output=False, - return_result=True) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 3'", log_output=False, + return_result=True) + assert return_code == 0 assert len(Functions.debug_log) == 0 assert "".join(result) == 'test run 3' finally: @@ -102,9 +105,51 @@ def test_functions_run_bash_log_and_return_output(): print() Functions.debug_log = [] try: - result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 4'", log_output=True, return_result=True) + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "echo 'test run 4'", log_output=True, return_result=True) + assert return_code == 0 assert "".join(result) == 'test run 4' assert len(Functions.debug_log) == 1 assert re.match("\[EASYHAPROXY\] .* \[INFO\]: test run 4", Functions.debug_log[0]) finally: Functions.debug_log = None + + +def test_functions_run_bash_ok(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "%s/fixtures/run_bash.sh" % os.path.dirname(__file__), log_output=True, + return_result=False) + assert return_code == 0 + assert result == [] + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Processing run_bash.sh", Functions.debug_log[0]) + finally: + Functions.debug_log = None + + +def test_functions_run_bash_fail(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "%s/fixtures/run_bash.sh 15" % os.path.dirname(__file__), log_output=True, + return_result=False) + assert return_code == 15 + assert result == [] + assert len(Functions.debug_log) == 1 + assert re.match("\[EASYHAPROXY\] .* \[INFO\]: Processing run_bash.sh", Functions.debug_log[0]) + finally: + Functions.debug_log = None + + +def test_functions_run_command_not_found(): + print() + Functions.debug_log = [] + try: + return_code, result = Functions.run_bash(Functions.EASYHAPROXY_LOG, "no_command_here", log_output=True, + return_result=False) + assert return_code == -99 + assert str(result) == "[Errno 2] No such file or directory: 'no_command_here'" + assert len(Functions.debug_log) == 0 + finally: + Functions.debug_log = None