diff --git a/README.md b/README.md
index 83cb831..1e05302 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,6 @@
# Ansible Role - HAProxy Community
-**WARNING**: The role is still in early development! **DO NOT TRY TO USE IN PRODUCTION**!
-
Role to deploy HAProxy (*Focus on the Community Version*)
@@ -60,14 +58,12 @@ ansible-galaxy install -r requirements.yml
* Redirect non SSL traffic to SSL if in HTTP mode
* Logging User-Agent
* Setting basic security-headers
- * Backend
- * Basic Check (*httpchk if in http mode*)
* **Default opt-outs**:
* Stats http listener
* Frontend
- * [ACME/LetsEncrypt](https://www.haproxy.com/blog/haproxy-and-let-s-encrypt) (*yet to be implemented*)
+ * [ACME/LetsEncrypt](https://github.com/dehydrated-io/dehydrated)
* [GeoIP Lookups](https://github.com/superstes/haproxy-geoip)
@@ -95,15 +91,15 @@ ansible-galaxy install -r requirements.yml
**Attribution**: `This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.`
-* **Info**: If you want to self-manage the databases - the role will assume they are placed at `/var/local/lib/geoip` and be named `asn.mmdb` & `country.mmdb`.
-
-
* **Info**: For GeoIP Tokens you will have to create a free account:
* **IPInfo**: [Login/Register](https://ipinfo.io/login)
* **MaxMind**: [Login/Register](https://www.maxmind.com/en/account/login) - Set the `token` to `:`
+* **Info**: If you want to self-manage the GeoIP-databases (*not recommended*) - the role will assume they are placed at `/var/local/lib/geoip` and be named `asn.mmdb` & `country.mmdb`.
+
+
* **Info**: You can test the [GeoIP Lookup Microservice](https://github.com/superstes/haproxy-geoip) manually by using curl: `curl 'http://127.0.0.1:10069/?lookup=country&ip=1.1.1.1'`
@@ -120,13 +116,19 @@ ansible-galaxy install -r requirements.yml
```yaml
haproxy:
+ acme:
+ enable: true
+ email: 'webmaster@template.ansibleguy.net'
+
frontends:
fe_web:
bind: ['[::]:80 v4v6', '[::]:443 v4v6 ssl']
- acme: true
- acme_domains: ['app.template.ansibleguy.net']
+ acme:
+ enable: true
+ domains: ['app.template.ansibleguy.net']
- route:
+
+ routes:
be_intern:
filter_ip: '10.0.0.0/8'
filter_not_ip: '10.100.0.0/24'
@@ -135,7 +137,9 @@ haproxy:
backends:
be_intern:
-
+ servers:
+ - 'srv-1 192.168.10.11:80'
+ - 'srv-2 192.168.10.12:80'
be_fallback:
lines: 'http-request redirect code 301 location https://github.com/ansibleguy'
@@ -146,15 +150,19 @@ Define the config as needed:
```yaml
haproxy:
version: '2.8'
+ acme:
+ enable: true
+ email: 'webmaster@template.ansibleguy.net'
# FRONTENDS
frontends:
fe_web:
bind: ['[::]:80 v4v6', '[::]:443 v4v6 ssl']
- acme: true
- acme_domains: ['app.template.ansibleguy.net'] # domains from routes will also be added
+ acme:
+ enable: true
+ domains: ['app.template.ansibleguy.net'] # domains from routes will also be added
- route:
+ routes:
be_app01:
domains: ['app01.template.ansibleguy.net', 'hello.template.ansibleguy.net']
@@ -170,14 +178,13 @@ haproxy:
default_backend: 'be_db'
fe_restricted:
- bind: ['[::]:80 v4v6', '[::]:443 v4v6 ssl']
- acme: true
+ bind: ['[::]:8080 v4v6', '[::]:8443 v4v6 ssl /etc/myapp/mycert.pem']
geoip:
enable: true
- backends:
- be_app01:
+ routes:
+ be_app02:
filter_country: ['AT', 'DE', 'CH']
# filter_ip: ['10.0.0.0/8']
domains: ['app01.template.ansibleguy.net', 'hello.template.ansibleguy.net']
@@ -199,6 +206,13 @@ haproxy:
check_uri: '/health'
check_expect: 'status 200'
+ be_app02:
+ ssl: true
+ ssl_verify: 'none' # default; example: 'required ca-file /etc/ssl/certs/my_ca.crt verifyhost host01.intern'
+ servers:
+ - 'app02-1 10.0.1.1:443'
+ - 'app02-2 10.0.1.2:443'
+
be_db:
mode: 'tcp'
balance: 'roundrobin'
@@ -253,7 +267,7 @@ ansible-playbook -K -D -i inventory/hosts.yml playbook.yml
There are also some useful **tags** available:
* install
* config
-* ssl
+* ssl or acme
* geoip
To debug errors - you can set the 'debug' variable at runtime:
diff --git a/defaults/main/0_hardcoded.yml b/defaults/main/0_hardcoded.yml
index 27b2ef6..89fa139 100644
--- a/defaults/main/0_hardcoded.yml
+++ b/defaults/main/0_hardcoded.yml
@@ -1,12 +1,14 @@
---
+version_geoip_lookup: '1.0'
+version_dehydrated: '0.7.1'
+
cpu_arch: "{{ 'amd64' if ansible_architecture == 'x86_64' else ansible_architecture }}"
HAPROXY_HC:
valid_versions: ['2.6', '2.7', '2.8', '2.9']
path:
config: '/etc/haproxy/conf.d'
- acme: '/etc/ssl/haproxy_acme'
map: '/etc/haproxy/map'
lua: '/etc/haproxy/lua'
geoip_bin: '/usr/local/bin/geoip-lookup'
@@ -16,16 +18,25 @@ HAPROXY_HC:
map_geoip_country: '/etc/haproxy/map/geoip_country.map'
map_geoip_asn: '/etc/haproxy/map/geoip_asn.map'
map_geoip_as_name: '/etc/haproxy/map/geoip_as_name.map'
+ acme_script: '/usr/local/bin/dehydrated.sh'
+ acme_script_hook: '/usr/local/bin/dehydrated_hook.sh'
+ acme_script_src: 'dehydrated'
+ acme: '/etc/ssl/haproxy_acme'
+ acme_certs: '/etc/ssl/haproxy_acme/certs'
+ acme_certs_raw: '/etc/ssl/haproxy_acme/certs_raw'
+ acme_challenges: '/var/www/haproxy_acme'
+ acme_config: '/etc/dehydrated'
user: 'haproxy'
group: 'haproxy'
url:
- geoip_bin: "https://github.com/superstes/geoip-lookup-service/releases/download/1.0/geoip-lookup-linux-{{ cpu_arch }}-CGO0.tar.gz"
+ geoip_bin: "https://github.com/superstes/geoip-lookup-service/releases/download/{{ version_geoip_lookup }}/geoip-lookup-linux-{{ cpu_arch }}-CGO0.tar.gz"
geoip_ipinfo_country: "https://ipinfo.io/data/free/country.mmdb?token="
geoip_ipinfo_asn: "https://ipinfo.io/data/free/asn.mmdb?token="
geoip_maxmind_country: "https://download.maxmind.com/geoip/databases/GeoLite2-ASN/download?suffix=tar.gz"
geoip_maxmind_asn: "https://download.maxmind.com/geoip/databases/GeoLite2-ASN/download?suffix=tar.gz"
+ acme_script: "https://github.com/dehydrated-io/dehydrated/releases/download/v{{ version_dehydrated }}/dehydrated-{{ version_dehydrated }}.tar.gz"
valid_geoip_providers: ['ipinfo', 'maxmind']
geoip_lookup_port: 10069
@@ -43,3 +54,9 @@ HAPROXY_HC:
country: 'country.iso_code'
asn: 'autonomous_system_number'
as_name: 'autonomous_system_organization'
+
+ user_acme: 'haproxy-acme'
+ acme_update_timer: '*-*-* 02:00:00'
+ acme_reload_timer: '*-*-* 03:00:00'
+ service_acme: 'haproxy-acme'
+ service_acme_reload: 'haproxy-acme-reload'
diff --git a/defaults/main/1_main.yml b/defaults/main/1_main.yml
index a613636..8099eeb 100644
--- a/defaults/main/1_main.yml
+++ b/defaults/main/1_main.yml
@@ -21,6 +21,15 @@ defaults_haproxy:
pwd: 'monitor'
realm: 'Authorized Personal Only'
+ acme:
+ enable: false
+ email:
+ ocsp_must_staple: false
+ ocsp_fetch: false
+ manage_challenge: true # set-up nginx-light to handle acme challenge-responses
+ challenge_port: 8405 # port the webserver for challenge-responses listens on
+ domains: []
+
geoip:
enable: false
manage_db: true
@@ -64,7 +73,10 @@ defaults_frontend:
# ipv4 only: ':80'
# ssl with acme enabled: ':443 ssl'
# ssl with custom cert: ':443 ssl crt /etc/ssl/haproxy/site.pem'
- acme: false
+ acme:
+ enable: false
+ domains: []
+
ssl_redirect: true
security_headers: true
log:
@@ -78,7 +90,7 @@ defaults_frontend:
lines: {} # raw config lines to add - section to lines mapping to make resulting config human-readable
- route: {}
+ routes: {}
# backend_name:
# domains: ['hostname1', 'hostname2']
# filter_country: ['AT', 'DE', 'CH']
@@ -123,6 +135,7 @@ defaults_backend:
# for health-checks see: https://www.haproxy.com/blog/how-to-enable-health-checks-in-haproxy
# more complex ones should be implemented by supplying the raw config-lines
check: true
+ check_http: false # httpchk
check_method: 'GET'
check_uri:
check_expect:
diff --git a/files/etc/ssl/haproxy_acme/certs/placeholder.pem b/files/etc/ssl/haproxy_acme/certs/placeholder.pem
new file mode 100644
index 0000000..68c83da
--- /dev/null
+++ b/files/etc/ssl/haproxy_acme/certs/placeholder.pem
@@ -0,0 +1,48 @@
+-----BEGIN CERTIFICATE-----
+MIIDMTCCAhkCFCXtQHlUk/7cEzQVuSinmYCO11hBMA0GCSqGSIb3DQEBCwUAMFUx
+CzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZTdHlyaWExEzARBgNVBAoMCkFuc2libGVH
+dXkxIDAeBgNVBAMMF1BsYWNlaG9sZGVyIENlcnRpZmljYXRlMB4XDTI0MDUwMzEy
+MzM0NloXDTM0MDUwMTEyMzM0NlowVTELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlN0
+eXJpYTETMBEGA1UECgwKQW5zaWJsZUd1eTEgMB4GA1UEAwwXUGxhY2Vob2xkZXIg
+Q2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6w1bI
+9T5etLqi5cRMsLJgfuGXzxI6eT3CrPOZXWsq1+F+wr3TcprUeNniD+8TTZ+YJu1r
+E5M5PGjODsNRbYj2uXtSu18TsJJn2qV1X8bvQgC4rswMUttkYO0QsrlCM3VEqI6R
+GA534gMfJ4slSf3Fiz/noWBllEM/NpphgHyoRSM5yw2TARRLrVE0Oc41qbzeIN6f
+nYritVw64/dsBQbOMtBwHRm57wz3kSrn+zDbXn2VZiDDXVJOAZsXCezsFuk3TlmC
+DjfJoRucfgHMxNoVaRoHJR9pPumHzRaqx2eDg2Cav/tE4nO2ANWPtD2736zpqsuh
+YqF6+eQage70EJ+vAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAImDt32BDBGohFNC
+AKXygBTMfcEYQO3Zp/gycpder7CsWyt+l551F5Rqgd0uHxa0VE4cwtyzXgmKkxh/
+lslcLysIzBCyfl3vJBw5cGQ+Ctg0D4i3XDDgJwU15ZXlnk9G3bIhNfHnoKUcxDle
+gp7E4Bub48PoE7LHf4yrHGNXUKKOIPaax18rQUxDUiWVCI1g4dMKjp4+yX0tiRUv
+PZcejNpw3FoKqkkGQVHvS01PS5r/CU0nw0muRXpoKVRZ8nMvulkvihN4BK4zjhs1
+y+xFY4GbTzJqPYTPwt+zkc/P8YB8T/abNYpo42xe+Kf8I9dWFBd6jNe//lg93v1r
+4WLf/tg=
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6w1bI9T5etLqi
+5cRMsLJgfuGXzxI6eT3CrPOZXWsq1+F+wr3TcprUeNniD+8TTZ+YJu1rE5M5PGjO
+DsNRbYj2uXtSu18TsJJn2qV1X8bvQgC4rswMUttkYO0QsrlCM3VEqI6RGA534gMf
+J4slSf3Fiz/noWBllEM/NpphgHyoRSM5yw2TARRLrVE0Oc41qbzeIN6fnYritVw6
+4/dsBQbOMtBwHRm57wz3kSrn+zDbXn2VZiDDXVJOAZsXCezsFuk3TlmCDjfJoRuc
+fgHMxNoVaRoHJR9pPumHzRaqx2eDg2Cav/tE4nO2ANWPtD2736zpqsuhYqF6+eQa
+ge70EJ+vAgMBAAECggEABF7Pbq+hrTQSRsv7P/nFv53rrlKMkEAhQqdeO/FRSxT2
++rEyVMeSkyfdnjCi3SzGPOU/gQkmoZw/esyI1ySQPYj+apbMrsVFZ8pYcA3oHDxk
+wyDNnZKcZHwjXNSrb4uMuI9izRQAoqvwFSnAhwR1CWTGZg4DF3cH69H3SUWccfbB
+27ObKI6AhRApIr2yBTihEL4mCqPTXtWbRQdcAM3kb07V7YxbsJz51Qac0l8ZmEph
+MzBzCJMuU65RwgalPS5hTzSwP5xHEIXjAIJ8mPH/QrIrSajReIiJGfXRbO/xljhg
+90yGTSX9HwnrC92+mJEszCzwI4nCCJmT2/jQSt/gIQKBgQC7bho/xBM9WExfqGiP
+uAghNBzxws8FzFexeLkM1ZNStJTuJv6ax0wnV4FXhwENp6lB38Hd7DoihOV80WLX
+gSfmDxf74nw3QXrjKOeEKjwxRB+qLiEsN8r4ThgyeAfGjRLitOKZjGRxfFPgbazJ
+e3wRkgBgdMJjaCxaYR6MOWu+QwKBgQD/FsOOe3SJkaVZwjsEC/IE7uuwoxJsmxO7
+8jRb8ABi+Sd+MtYtMQXYEghOVxupg2/PR1Pl/cMWEYPoQ3Lbn3quoJnP4qd02MiN
+puL1hFIj09pEExaoUU01lTrNkgCkm5HhLGSo4uj/Ygg4I+Z1d9jlXck24UI7z23Y
+Y4q8hXdgJQKBgQCf9qcmdvSorXx5Q6UBy+H8XJq7ZzUC0NSjHdJpdrpGouJcoyE2
+/hMrnI5CInGussJM+2hdPCidn2iw7495N7zSp10j17eF/TehOh7leJpovah8uOQM
+9g0fgJ88K58PQQW2QQUIYX60MJTxfQkz6FUKNd5mdCAXcSgxdqP4r2UaOwKBgFas
+aW30TLihoElLUboiRO2gML0n646zcpUdyuSiO79lYSHkLBnW2mF8Xw4fUuraGheX
+6M3w12ScNvGoWVJ+cbT8JMcaAEQXlK1s0xkRCMfbqAIRalVuqolWV1CaF1XW9k5I
+QzuPPhPoP1qz+A5Z1ny4zTG0gEjKRkyMJgvAXbtxAoGAPSRzPJU81AwxrUMspLEf
+J+S5PKeGoicJ4U1QFExcMjLnJwnkBwcs2BUgt8uocOhHVQstyicQRvibXm8nwFZd
+SOjUQRPvKrilgO1NVgT/hi51fN1TBN7zncZCdOLCctfAUBzdK9pdO6Qu6uvtFNMd
+iKYEFn5fm6rrNIU6GQKdEZo=
+-----END PRIVATE KEY-----
diff --git a/filter_plugins/utils.py b/filter_plugins/utils.py
index bcd57a4..1fbb18f 100644
--- a/filter_plugins/utils.py
+++ b/filter_plugins/utils.py
@@ -1,3 +1,6 @@
+from re import sub as regex_replace
+
+
class FilterModule(object):
def filters(self):
@@ -5,6 +8,7 @@ def filters(self):
"ensure_list": self.ensure_list,
"is_string": self.is_string,
"is_dict": self.is_dict,
+ "safe_key": self.safe_key,
}
@staticmethod
@@ -22,3 +26,8 @@ def is_string(data) -> bool:
@staticmethod
def is_dict(data) -> bool:
return isinstance(data, dict)
+
+ @staticmethod
+ def safe_key(key: str) -> str:
+ return regex_replace('[^0-9a-zA-Z_]+', '', key.replace(' ', '_'))
+
diff --git a/tasks/debian/acme.yml b/tasks/debian/acme.yml
new file mode 100644
index 0000000..7b3d158
--- /dev/null
+++ b/tasks/debian/acme.yml
@@ -0,0 +1,171 @@
+---
+
+- name: HAProxy | ACME | Install Dependencies
+ ansible.builtin.apt:
+ pkg: ['curl', 'sed', 'grep', 'sendmail']
+ state: present
+
+- name: HAProxy | ACME | Creating service user
+ ansible.builtin.user:
+ name: "{{ HAPROXY_HC.user_acme }}"
+ shell: '/usr/sbin/nologin'
+ comment: 'HAProxy ACME Serviceuser'
+
+- name: HAProxy | ACME | Create ACME directories
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: directory
+ owner: "{{ HAPROXY_HC.user_acme }}"
+ group: 'haproxy'
+ mode: 0750
+ loop:
+ - "{{ HAPROXY_HC.path.acme }}"
+ - "{{ HAPROXY_HC.path.acme_certs }}"
+ - "{{ HAPROXY_HC.path.acme_config }}"
+
+- name: HAProxy | ACME | Checking if ACME script exists
+ ansible.builtin.stat:
+ path: "{{ HAPROXY_HC.path.acme_script }}"
+ register: acme_script
+
+- name: HAProxy | ACME | Download ACME script
+ ansible.builtin.unarchive:
+ src: "{{ HAPROXY_HC.url.acme_script }}"
+ dest: '/tmp/haproxy'
+ remote_src: yes
+ extra_opts: ['--strip-components=1']
+ mode: 0750
+ when: not acme_script.stat.exists or force_update | bool
+
+- name: HAProxy | ACME | Move ACME script
+ ansible.builtin.copy:
+ src: "/tmp/haproxy/{{ HAPROXY_HC.path.acme_script_src }}"
+ remote_src: yes
+ dest: "{{ HAPROXY_HC.path.acme_script }}"
+ owner: 'root'
+ group: "{{ HAPROXY_HC.user_acme }}"
+ mode: 0750
+ when: not acme_script.stat.exists or force_update | bool
+
+- name: HAProxy | ACME | Testing
+ ansible.builtin.command: "bash {{ HAPROXY_HC.path.acme_script }} --version"
+ changed_when: false
+ when: not acme_script.stat.exists or force_update | bool
+
+- name: HAProxy | ACME | Config
+ ansible.builtin.copy:
+ content: |
+ # ansible_managed
+ # ansibleguy.infra_haproxy
+
+ # see: https://github.com/dehydrated-io/dehydrated/blob/master/docs/examples/config
+
+ BASEDIR="{{ HAPROXY_HC.path.acme }}"
+ CERTDIR="{{ HAPROXY_HC.path.acme_certs_raw }}"
+ DOMAINS_TXT="{{ HAPROXY_HC.path.acme_config }}/domains.txt"
+ WELLKNOWN="{{ HAPROXY_HC.path.acme_challenges }}"
+ CONTACT_EMAIL="{{ HAPROXY_CONFIG.acme.email }}"
+ HOOK="{{ HAPROXY_HC.path.acme_script_hook }}"
+
+ CA="letsencrypt"
+ KEYSIZE="4096"
+ KEY_ALGO="secp384r1"
+ RENEW_DAYS="30"
+
+ AUTO_CLEANUP="yes"
+ OCSP_FETCH="{{ 'yes' if HAPROXY_CONFIG.acme.ocsp_fetch|bool else 'no' }}"
+ OCSP_MUST_STAPLE="{{ 'yes' if HAPROXY_CONFIG.acme.ocsp_must_staple|bool else 'no' }}"
+ dest: "{{ HAPROXY_HC.path.acme_config }}/config"
+ owner: 'root'
+ group: "{{ HAPROXY_HC.user_acme }}"
+ mode: 0640
+ register: acme_cnf
+
+- name: HAProxy | ACME | Hook script
+ ansible.builtin.template:
+ src: "templates/{{ HAPROXY_HC.path.acme_script_hook }}.j2"
+ dest: "{{ HAPROXY_HC.path.acme_script_hook }}"
+ owner: 'root'
+ group: "{{ HAPROXY_HC.user_acme }}"
+ mode: 0750
+
+- name: HAProxy | ACME | Registering
+ ansible.builtin.command: "{{ HAPROXY_HC.path.acme_script }} --register --accept-terms"
+ become_user: "{{ HAPROXY_HC.user_acme }}"
+ become: true
+ when: acme_cnf.changed
+
+# NOTE: haproxy fails to start if no certificates exist
+- name: HAProxy | ACME | Copy placeholder certificate
+ ansible.builtin.copy:
+ src: 'files{{ HAPROXY_HC.path.acme_certs }}/placeholder.pem'
+ dest: "{{ HAPROXY_HC.path.acme_certs }}/placeholder.pem"
+ owner: "{{ HAPROXY_HC.user_acme }}"
+ group: 'haproxy'
+ mode: 0640
+ no_log: true
+
+- name: HAProxy | ACME | Certificate Config
+ ansible.builtin.template:
+ src: "templates{{ HAPROXY_HC.path.acme_config }}/domains.txt.j2"
+ dest: "{{ HAPROXY_HC.path.acme_config }}/domains.txt"
+ owner: 'root'
+ group: "{{ HAPROXY_HC.user_acme }}"
+ mode: 0640
+ tags: [config]
+ register: acme_cert_cnf
+
+- name: HAProxy | ACME | Update Service/Timer
+ ansible.builtin.template:
+ src: "templates/etc/systemd/system/{{ HAPROXY_HC.service_acme }}.{{ item }}.j2"
+ dest: "/etc/systemd/system/{{ HAPROXY_HC.service_acme }}.{{ item }}"
+ owner: 'root'
+ group: 'root'
+ mode: 0644
+ loop:
+ - 'service'
+ - 'timer'
+ register: acme_renewal_svc
+
+# NOTE: could be removed if the ACME user was granted privileges to reload the haproxy service and the reload-commands were uncommented in the hook-script
+- name: HAProxy | ACME | Reload Service/Timer
+ ansible.builtin.template:
+ src: "templates/etc/systemd/system/{{ HAPROXY_HC.service_acme_reload }}.{{ item }}.j2"
+ dest: "/etc/systemd/system/{{ HAPROXY_HC.service_acme_reload }}.{{ item }}"
+ owner: 'root'
+ group: 'root'
+ mode: 0644
+ loop:
+ - 'service'
+ - 'timer'
+ register: acme_reload_svc
+ notify: haproxy-reload
+
+- name: HAProxy | ACME | Systemd Daemon-reload
+ ansible.builtin.systemd:
+ daemon_reload: true
+ when: acme_renewal_svc.changed or acme_reload_svc.changed
+
+- name: HAProxy | ACME | Enable & Start Renewal Timer
+ ansible.builtin.systemd:
+ name: "{{ HAPROXY_HC.service_acme }}.timer"
+ enabled: true
+ state: started
+
+- name: HAProxy | ACME | Enable & Start Reload Timer
+ ansible.builtin.systemd:
+ name: "{{ HAPROXY_HC.service_acme_reload }}.timer"
+ enabled: true
+ state: started
+
+- name: HAProxy | ACME | Challenge-Response
+ ansible.builtin.import_tasks: acme_challenge.yml
+ when: HAPROXY_CONFIG.acme.manage_challenge | bool
+
+- name: HAProxy | ACME | Initialize/Renew Certificates (in Background)
+ ansible.builtin.systemd:
+ name: "{{ HAPROXY_HC.service_acme }}.service"
+ state: started
+ no_block: true
+ tags: [config]
+ when: acme_renewal_svc.changed | default(false) or acme_cert_cnf.changed
diff --git a/tasks/debian/acme_challenge.yml b/tasks/debian/acme_challenge.yml
new file mode 100644
index 0000000..8ceba0b
--- /dev/null
+++ b/tasks/debian/acme_challenge.yml
@@ -0,0 +1,49 @@
+---
+
+- name: HAProxy | ACME | Install Nginx-Light to handle Challenge-Responses
+ ansible.builtin.apt:
+ pkg: ['nginx-light']
+ state: present
+
+- name: HAProxy | ACME | Remove Nginx default-config
+ ansible.builtin.file:
+ path: '/etc/nginx/sites-enabled/default'
+ state: absent
+
+- name: HAProxy | ACME | Create ACME challenge-directory
+ ansible.builtin.file:
+ path: "{{ HAPROXY_HC.path.acme_challenges }}"
+ state: directory
+ owner: "{{ HAPROXY_HC.user_acme }}"
+ group: 'www-data'
+ mode: 0755
+
+- name: HAProxy | ACME | Add Nginx config
+ ansible.builtin.copy:
+ content: |
+ # ansible_managed
+
+ server {
+ listen 127.0.0.1:{{ HAPROXY_CONFIG.acme.challenge_port }};
+
+ autoindex off;
+ server_tokens off;
+
+ location ^~ /.well-known/acme-challenge {
+ alias {{ HAPROXY_HC.path.acme_challenges }};
+ }
+ location / {
+ deny all;
+ }
+ }
+ dest: '/etc/nginx/sites-enabled/haproxy_acme'
+ mode: 0644
+ owner: 'root'
+ group: 'root'
+
+- name: HAProxy | ACME | Enable & Start Nginx
+ ansible.builtin.systemd:
+ name: 'nginx.service'
+ enabled: true
+ state: restarted
+ changed_when: false
diff --git a/tasks/debian/certs.yml b/tasks/debian/certs.yml
deleted file mode 100644
index f9d23b4..0000000
--- a/tasks/debian/certs.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-
-- name: HAProxy | Certificates | Create ACME directory
- ansible.builtin.file:
- path: "{{ HAPROXY_HC.path.acme }}"
- state: directory
- owner: 'root'
- group: 'haproxy'
- mode: 0750
diff --git a/tasks/debian/geoip.yml b/tasks/debian/geoip.yml
index e4a0e90..26c35e5 100644
--- a/tasks/debian/geoip.yml
+++ b/tasks/debian/geoip.yml
@@ -55,7 +55,7 @@
- name: HAProxy | GeoIP | Download Lookup-Service Binary
ansible.builtin.unarchive:
src: "{{ HAPROXY_HC.url.geoip_bin }}"
- dest: '/tmp'
+ dest: '/tmp/haproxy'
remote_src: yes
extra_opts: ['--strip-components=1']
mode: 0750
@@ -63,7 +63,7 @@
- name: HAProxy | GeoIP | Move Lookup-Service Binary
ansible.builtin.copy:
- src: "/tmp/{{ HAPROXY_HC.path.geoip_bin_src }}"
+ src: "/tmp/haproxy/{{ HAPROXY_HC.path.geoip_bin_src }}"
remote_src: yes
dest: "{{ HAPROXY_HC.path.geoip_bin }}"
owner: 'root'
@@ -100,7 +100,7 @@
when: HAPROXY_CONFIG.geoip.manage_db | bool
register: geoip_update_svc
-- name: HAProxy | GeoIP | Lookup Service/Timer
+- name: HAProxy | GeoIP | Lookup Service
ansible.builtin.template:
src: "templates/etc/systemd/system/{{ HAPROXY_HC.service_geoip_lookup }}.service.j2"
dest: "/etc/systemd/system/{{ HAPROXY_HC.service_geoip_lookup }}.service"
diff --git a/tasks/debian/install.yml b/tasks/debian/install.yml
index 3c9dcc7..cb9c56a 100644
--- a/tasks/debian/install.yml
+++ b/tasks/debian/install.yml
@@ -46,24 +46,8 @@
# log errors and keep us from killing the active process with invalid config
# add conf.d as config source
- name: HAProxy | Install | Create service-override
- ansible.builtin.copy:
- content: |
- # ansible_managed
- # ansibleguy.infra_haproxy
-
- [Unit]
- Documentation=https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/
- Documentation=https://github.com/ansibleguy/infra_haproxy
-
- [Service]
- ExecStartPre=/usr/sbin/haproxy -c -f $CONFIG -f {{ HAPROXY_HC.path.config }}/
- ExecStart=
- ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f {{ HAPROXY_HC.path.config }}/ -p $PIDFILE $EXTRAOPTS
-
- ExecReload=
- ExecReload=/usr/sbin/haproxy -c -f $CONFIG -f {{ HAPROXY_HC.path.config }}/
- ExecReload=/bin/kill -USR2 $MAINPID
-
+ ansible.builtin.template:
+ src: 'templates/etc/systemd/system/haproxy.service.d/override.conf.j2'
dest: '/etc/systemd/system/haproxy.service.d/override.conf'
owner: 'root'
group: 'root'
diff --git a/tasks/debian/main.yml b/tasks/debian/main.yml
index 4ed09e9..ee4f3b7 100644
--- a/tasks/debian/main.yml
+++ b/tasks/debian/main.yml
@@ -5,11 +5,13 @@
tags: [install]
- name: HAProxy | SSL/TLS Certificates
- ansible.builtin.import_tasks: debian/certs.yml
- tags: [ssl]
+ ansible.builtin.import_tasks: debian/acme.yml
+ when: HAPROXY_CONFIG.acme.enable | bool
+ tags: [ssl, acme]
- name: HAProxy | GeoIP
ansible.builtin.import_tasks: debian/geoip.yml
+ when: HAPROXY_CONFIG.geoip.enable | bool
tags: [geoip]
- name: HAProxy | Config
diff --git a/tasks/main.yml b/tasks/main.yml
index 4d69b46..f49edd9 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -8,6 +8,7 @@
# make sure we will be able to auto-download geoip dbs
- not HAPROXY_CONFIG.geoip.enable|bool or not HAPROXY_CONFIG.geoip.manage_db|bool or HAPROXY_CONFIG.geoip.token|default(none, true) is not none
- not HAPROXY_CONFIG.geoip.enable|bool or not HAPROXY_CONFIG.geoip.manage_db|bool or HAPROXY_CONFIG.geoip.provider in HAPROXY_HC.valid_geoip_providers
+ - not HAPROXY_CONFIG.acme.enable|bool or HAPROXY_CONFIG.acme.email|default(none, true) is not none
tags: always
- name: HAProxy | Processing debian config
diff --git a/templates/etc/dehydrated/domains.txt.j2 b/templates/etc/dehydrated/domains.txt.j2
new file mode 100644
index 0000000..4be1ac0
--- /dev/null
+++ b/templates/etc/dehydrated/domains.txt.j2
@@ -0,0 +1,20 @@
+# {{ ansible_managed }}
+
+{% if HAPROXY_CONFIG.acme.domains | ensure_list | length > 0 %}
+{{ HAPROXY_CONFIG.acme.domains | ensure_list | join(' ') }} > base
+{% endif %}
+
+{% for fe_name, fe_cnf_user in HAPROXY_CONFIG.frontends.items() %}
+{% set fe_cnf = defaults_frontend | combine(fe_cnf_user, recursive=true) %}
+# FRONTEND: {{ fe_name }}
+{% if fe_cnf.acme.domains | ensure_list | length > 0 %}
+{{ fe_cnf.acme.domains | ensure_list | join(' ') }} > {{ fe_name | safe_key }}
+{% endif %}
+{% for be_name, be_cnf_user in fe_cnf.routes.items() %}
+{% set be_cnf = defaults_backend | combine(be_cnf_user, recursive=true) %}
+## BACKEND: {{ be_name }}
+{% if be_cnf.domains | ensure_list | length > 0 %}
+{{ be_cnf.domains | ensure_list | join(' ') }} > {{ fe_name | safe_key }}-{{ be_name | safe_key }}
+{% endif %}
+{% endfor %}
+{% endfor %}
diff --git a/templates/etc/haproxy/conf.d/backend.cfg.j2 b/templates/etc/haproxy/conf.d/backend.cfg.j2
index 89552af..b792a8a 100644
--- a/templates/etc/haproxy/conf.d/backend.cfg.j2
+++ b/templates/etc/haproxy/conf.d/backend.cfg.j2
@@ -9,7 +9,9 @@ backend {{ name }}
balance {{ cnf.balance }}
{% if cnf.check | bool and cnf.mode == 'http' %}
+{% if cnf.check_http | bool %}
option httpchk
+{% endif %}
{% if cnf.check_uri | default(none, true) is not none %}
http-check send meth {{ cnf.check_method }} uri {{ cnf.check_uri }}
{% endif %}
@@ -38,4 +40,9 @@ backend {{ name }}
server {{ server }}{% if cnf.check | bool %} check{% endif %}{% if cnf.ssl | bool %} ssl verify {{ cnf.ssl_verify }}{% endif %}
{% endfor %}
-{% endfor %}
\ No newline at end of file
+{% endfor %}
+
+{% if HAPROXY_CONFIG.acme.enable | bool and HAPROXY_CONFIG.acme.manage_challenge | bool %}
+backend be_haproxy_acme
+ server haproxy_acme 127.0.0.1:{{ HAPROXY_CONFIG.acme.challenge_port }} check
+{% endif %}
diff --git a/templates/etc/haproxy/conf.d/frontend.cfg.j2 b/templates/etc/haproxy/conf.d/frontend.cfg.j2
index 208b2f5..feeb9e9 100644
--- a/templates/etc/haproxy/conf.d/frontend.cfg.j2
+++ b/templates/etc/haproxy/conf.d/frontend.cfg.j2
@@ -6,16 +6,20 @@
frontend {{ name }}
mode {{ cnf.mode }}
{% for bind in cnf.bind | ensure_list %}
-{% if cnf.acme | bool %}
- bind {{ bind | regex_replace('ssl$', 'ssl crt ' + HAPROXY_HC.path.acme) }}
+{% if cnf.acme.enable | bool %}
+ bind {{ bind | regex_replace('ssl$', 'ssl crt ' + HAPROXY_HC.path.acme_certs) }}
{% else %}
bind {{ bind }}
{% endif %}
{% endfor %}
{% if cnf.mode == 'http' %}
-{% if cnf.ssl_redirect | bool and 'ssl' in cnf.bind.join('-') %}
+{% if cnf.ssl_redirect | bool and 'ssl' in (cnf.bind | join('-')) %}
+{% if cnf.acme.enable | bool %}
+ http-request redirect scheme https code 301 if !{ ssl_fc } !{ path_beg -i /.well-known/acme-challenge/ }
+{% else %}
http-request redirect scheme https code 301 unless { ssl_fc }
+{% endif %}
{% endif %}
{% endif %}
@@ -49,24 +53,28 @@ frontend {{ name }}
{% endif %}
-{% for be_name, be_cnf_user in cnf.route.items() %}
+{% if cnf.acme.enable | bool %}
+ use_backend be_haproxy_acme if { path_beg -i /.well-known/acme-challenge/ }
+{% endif %}
+
+{% for be_name, be_cnf_user in cnf.routes.items() %}
{% set be_cnf = defaults_frontend_route | combine(be_cnf_user, recursive=true) %}
# BACKEND {{ be_name }}
- acl {{ be_name }}_domains req.hdr(host) -i {{ (be_cnf.domains | ensure_list).join(' ') }}
- acl {{ be_name }}_filter_ip {% if be_cnf.filter_ip | length > 0 %}src (be_cnf.filter_ip | ensure_list).join(' '){% else %}always_true{% endif %}
- acl {{ be_name }}_filter_not_ip {% if be_cnf.filter_not_ip | length > 0 %}src (be_cnf.filter_not_ip | ensure_list).join(' '){% else %}always_false{% endif %}
+ acl {{ be_name }}_domains req.hdr(host) -i {{ be_cnf.domains | ensure_list | join(' ') }}
+ acl {{ be_name }}_filter_ip {% if be_cnf.filter_ip | length > 0 %}src {{ be_cnf.filter_ip | ensure_list | join(' ') }}{% else %}always_true{% endif +%}
+ acl {{ be_name }}_filter_not_ip {% if be_cnf.filter_not_ip | length > 0 %}src {{ be_cnf.filter_not_ip | ensure_list | join(' ') }}{% else %}always_false{% endif +%}
{% if HAPROXY_CONFIG.geoip.enable | bool and cnf.geoip.enable | bool %}
{% if cnf.geoip.country | bool %}
- acl {{ be_name }}_filter_country {% if be_cnf.filter_country | length > 0 %}var(txn.geoip_country) -i (be_cnf.filter_country | ensure_list).join(' '){% else %}always_true{% endif %}
- acl {{ be_name }}_filter_not_country {% if be_cnf.filter_not_country | length > 0 %}var(txn.geoip_country) -i (be_cnf.filter_not_country | ensure_list).join(' '){% else %}always_false{% endif %}
+ acl {{ be_name }}_filter_country {% if be_cnf.filter_country | length > 0 %}var(txn.geoip_country) -m str -i {{ be_cnf.filter_country | ensure_list | join(' ') }}{% else %}always_true{% endif +%}
+ acl {{ be_name }}_filter_not_country {% if be_cnf.filter_not_country | length > 0 %}var(txn.geoip_country) -m str -i {{ be_cnf.filter_not_country | ensure_list | join(' ') }}{% else %}always_false{% endif +%}
{% else %}
acl {{ be_name }}_filter_country always_true
acl {{ be_name }}_filter_not_country always_false
{% endif %}
{% if cnf.geoip.asn | bool %}
- acl {{ be_name }}_filter_asn {% if be_cnf.filter_asn | length > 0 %}var(txn.geoip_asn) -i (be_cnf.filter_asn | ensure_list).join(' '){% else %}always_true{% endif %}
- acl {{ be_name }}_filter_not_asn {% if be_cnf.filter_not_asn | length > 0 %}var(txn.geoip_asn) -i (be_cnf.filter_not_asn | ensure_list).join(' '){% else %}always_false{% endif %}
+ acl {{ be_name }}_filter_asn {% if be_cnf.filter_asn | length > 0 %}var(txn.geoip_asn) -m str -i {{ be_cnf.filter_asn | ensure_list | join(' ') }}{% else %}always_true{% endif +%}
+ acl {{ be_name }}_filter_not_asn {% if be_cnf.filter_not_asn | length > 0 %}var(txn.geoip_asn) -m str -i {{ be_cnf.filter_not_asn | ensure_list | join(' ') }}{% else %}always_false{% endif +%}
{% else %}
acl {{ be_name }}_filter_asn always_true
acl {{ be_name }}_filter_not_asn always_false
diff --git a/templates/etc/haproxy/lua/geoip.lua.j2 b/templates/etc/haproxy/lua/geoip.lua.j2
index 1cb9ce6..b316ce1 100644
--- a/templates/etc/haproxy/lua/geoip.lua.j2
+++ b/templates/etc/haproxy/lua/geoip.lua.j2
@@ -2,7 +2,7 @@
-- ansibleguy.infra_haproxy
-- source: https://raw.githubusercontent.com/superstes/haproxy-geoip/latest/lua/geoip_lookup_w_go_backend.lua
-local function http_request(lookup, filter, src)
+local function http_request(lookup, filter, src, ltrim)
local s = core.tcp()
local addr = '127.0.0.1'
@@ -33,21 +33,21 @@ local function http_request(lookup, filter, src)
if res_body == nil then
return '00'
end
- return string.sub(res_body, 1, -2)
+ return string.sub(res_body, 1 + ltrim, -2)
end
local function lookup_geoip_country(txn)
- country_code = http_request('country', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].country }}', txn.f:src())
+ country_code = http_request('country', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].country }}', txn.f:src(), 0)
txn:set_var('txn.geoip_country', country_code)
end
local function lookup_geoip_asn(txn)
- asn = http_request('asn', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].asn }}', txn.f:src())
+ asn = http_request('asn', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].asn }}', txn.f:src(), {{ 0 if HAPROXY_CONFIG.geoip.provider == 'maxmind' else 2 }})
txn:set_var('txn.geoip_asn', asn)
end
local function lookup_geoip_as_name(txn)
- as_name = http_request('asn', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].as_name }}', txn.f:src())
+ as_name = http_request('asn', '{{ HAPROXY_HC.geoip_lookup_filters[HAPROXY_CONFIG.geoip.provider].as_name }}', txn.f:src(), 0)
txn:set_var('txn.geoip_as_name', as_name)
end
diff --git a/templates/etc/systemd/system/haproxy-acme-reload.service.j2 b/templates/etc/systemd/system/haproxy-acme-reload.service.j2
new file mode 100644
index 0000000..028a7ee
--- /dev/null
+++ b/templates/etc/systemd/system/haproxy-acme-reload.service.j2
@@ -0,0 +1,20 @@
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+[Unit]
+Description=HAProxy reload for certificate Renewal
+Documentation=https://github.com/ansibleguy/infra_haproxy
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/systemctl reload haproxy.service
+
+User=root
+Group=root
+
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=haproxy-acme
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/etc/systemd/system/haproxy-acme-reload.timer.j2 b/templates/etc/systemd/system/haproxy-acme-reload.timer.j2
new file mode 100644
index 0000000..532e198
--- /dev/null
+++ b/templates/etc/systemd/system/haproxy-acme-reload.timer.j2
@@ -0,0 +1,14 @@
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+
+[Unit]
+Description=Timer to perform HAProxy reload for certificate Renewal
+
+[Timer]
+OnCalendar={{ HAPROXY_HC.acme_reload_timer }}
+Persistent=false
+WakeSystem=false
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/etc/systemd/system/haproxy-acme.service.j2 b/templates/etc/systemd/system/haproxy-acme.service.j2
new file mode 100644
index 0000000..c109d32
--- /dev/null
+++ b/templates/etc/systemd/system/haproxy-acme.service.j2
@@ -0,0 +1,21 @@
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+[Unit]
+Description=HAProxy ACME Certificate Renewal Service
+Documentation=https://github.com/dehydrated-io/dehydrated/wiki
+Documentation=https://github.com/ansibleguy/infra_haproxy
+
+[Service]
+Type=oneshot
+ExecStart={{ HAPROXY_HC.path.acme_script }} -c
+
+User={{ HAPROXY_HC.user_acme }}
+Group={{ HAPROXY_HC.user_acme }}
+
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=haproxy-acme
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/etc/systemd/system/haproxy-acme.timer.j2 b/templates/etc/systemd/system/haproxy-acme.timer.j2
new file mode 100644
index 0000000..f3dcfa0
--- /dev/null
+++ b/templates/etc/systemd/system/haproxy-acme.timer.j2
@@ -0,0 +1,14 @@
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+
+[Unit]
+Description=Timer to start HAProxy ACME Certificate Renewal
+
+[Timer]
+OnCalendar={{ HAPROXY_HC.acme_update_timer }}
+Persistent=false
+WakeSystem=false
+
+[Install]
+WantedBy=multi-user.target
diff --git a/templates/etc/systemd/system/haproxy.service.d/override.conf.j2 b/templates/etc/systemd/system/haproxy.service.d/override.conf.j2
new file mode 100644
index 0000000..8af3207
--- /dev/null
+++ b/templates/etc/systemd/system/haproxy.service.d/override.conf.j2
@@ -0,0 +1,22 @@
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+[Unit]
+Documentation=https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/
+Documentation=https://github.com/ansibleguy/infra_haproxy
+
+[Service]
+ExecStartPre=/usr/sbin/haproxy -c -f $CONFIG -f {{ HAPROXY_HC.path.config }}/
+ExecStart=
+ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f {{ HAPROXY_HC.path.config }}/ -p $PIDFILE $EXTRAOPTS
+
+ExecReload=
+ExecReload=/usr/sbin/haproxy -c -f $CONFIG -f {{ HAPROXY_HC.path.config }}/
+ExecReload=/bin/kill -USR2 $MAINPID
+
+RestartSec=10s
+Restart=on-failure
+
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=haproxy
diff --git a/templates/usr/local/bin/dehydrated_hook.sh.j2 b/templates/usr/local/bin/dehydrated_hook.sh.j2
new file mode 100644
index 0000000..94d2267
--- /dev/null
+++ b/templates/usr/local/bin/dehydrated_hook.sh.j2
@@ -0,0 +1,196 @@
+#!/usr/bin/env bash
+
+# {{ ansible_managed }}
+# ansibleguy.infra_haproxy
+
+MAIL_TO="{{ HAPROXY_CONFIG.acme.email }}"
+HAPROXY_CERT_DIR="{{ HAPROXY_HC.path.acme_certs }}"
+
+deploy_challenge() {
+ local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
+
+ # This hook is called once for every domain that needs to be
+ # validated, including any alternative names you may have listed.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The domain name (CN or subject alternative name) being
+ # validated.
+ # - TOKEN_FILENAME
+ # The name of the file containing the token to be served for HTTP
+ # validation. Should be served by your web server as
+ # /.well-known/acme-challenge/${TOKEN_FILENAME}.
+ # - TOKEN_VALUE
+ # The token value that needs to be served for validation. For DNS
+ # validation, this is what you want to put in the _acme-challenge
+ # TXT record. For HTTP validation it is the value that is expected
+ # be found in the $TOKEN_FILENAME file.
+}
+
+clean_challenge() {
+ local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
+
+ # This hook is called after attempting to validate each domain,
+ # whether or not validation was successful. Here you can delete
+ # files or DNS records that are no longer needed.
+ #
+ # The parameters are the same as for deploy_challenge.
+}
+
+deploy_cert() {
+ local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
+ local CERTDIR=$(dirname "${FULLCHAINFILE}")
+ local SHOPCODE=$(dirname "${CERTDIR}")
+
+ # This hook is called once for each certificate that has been
+ # produced. Here you might, for instance, copy your new certificates
+ # to service-specific locations and reload the service.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The primary domain name, i.e. the certificate common
+ # name (CN).
+ # - KEYFILE
+ # The path of the file containing the private key.
+ # - CERTFILE
+ # The path of the file containing the signed certificate.
+ # - FULLCHAINFILE
+ # The path of the file containing the full certificate chain.
+ # - CHAINFILE
+ # The path of the file containing the intermediate certificate(s).
+ # - TIMESTAMP
+ # Timestamp when the specified certificate was created.
+
+ cat "${FULLCHAINFILE}" "${KEYFILE}" > "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"
+ # sudo systemctl reload haproxy.service
+}
+
+deploy_ocsp() {
+ local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
+ local CERTDIR=$(dirname "${OCSPFILE}")
+
+ # This hook is called once for each updated ocsp stapling file that has
+ # been produced. Here you might, for instance, copy your new ocsp stapling
+ # files to service-specific locations and reload the service.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The primary domain name, i.e. the certificate common
+ # name (CN).
+ # - OCSPFILE
+ # The path of the ocsp stapling file
+ # - TIMESTAMP
+ # Timestamp when the specified ocsp stapling file was created.
+
+ cp "${OCSPFILE}" "${HAPROXY_CERT_DIR}/${DOMAIN}.pem.ocsp"
+ # sudo systemctl reload haproxy.service
+}
+
+
+unchanged_cert() {
+ local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
+
+ # This hook is called once for each certificate that is still
+ # valid and therefore wasn't reissued.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The primary domain name, i.e. the certificate common
+ # name (CN).
+ # - KEYFILE
+ # The path of the file containing the private key.
+ # - CERTFILE
+ # The path of the file containing the signed certificate.
+ # - FULLCHAINFILE
+ # The path of the file containing the full certificate chain.
+ # - CHAINFILE
+ # The path of the file containing the intermediate certificate(s).
+}
+
+invalid_challenge() {
+ local DOMAIN="${1}" RESPONSE="${2}"
+
+ # This hook is called if the challenge response has failed, so domain
+ # owners can be aware and act accordingly.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The primary domain name, i.e. the certificate common
+ # name (CN).
+ # - RESPONSE
+ # The response that the verification server returned
+
+ msg="Validation of ${DOMAIN} failed!\n\nResponse: ${RESPONSE}"
+ echo "ERROR: ${msg}"
+
+ if which sendmail
+ then
+ printf "Subject: %s" % msg | sendmail "$MAIL_TO"
+ fi
+}
+
+request_failure() {
+ local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
+
+ # This hook is called when an HTTP request fails (e.g., when the ACME
+ # server is busy, returns an error, etc). It will be called upon any
+ # response code that does not start with '2'. Useful to alert admins
+ # about problems with requests.
+ #
+ # Parameters:
+ # - STATUSCODE
+ # The HTML status code that originated the error.
+ # - REASON
+ # The specified reason for the error.
+ # - REQTYPE
+ # The kind of request that was made (GET, POST...)
+ # - HEADERS
+ # HTTP headers returned by the CA
+
+ msg="HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE} on $(hostname)!\nReason: ${REASON}\nREQTYPE: ${REQTYPE}"
+ echo "ERROR: ${msg}"
+
+ if which sendmail
+ then
+ printf "Subject: %s" % msg | sendmail "$MAIL_TO"
+ fi
+}
+
+generate_csr() {
+ local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"
+
+ # This hook is called before any certificate signing operation takes place.
+ # It can be used to generate or fetch a certificate signing request with external
+ # tools.
+ # The output should be just the cerificate signing request formatted as PEM.
+ #
+ # Parameters:
+ # - DOMAIN
+ # The primary domain as specified in domains.txt. This does not need to
+ # match with the domains in the CSR, it's basically just the directory name.
+ # - CERTDIR
+ # Certificate output directory for this particular certificate. Can be used
+ # for storing additional files.
+ # - ALTNAMES
+ # All domain names for the current certificate as specified in domains.txt.
+ # Again, this doesn't need to match with the CSR, it's just there for convenience.
+}
+
+startup_hook() {
+ # This hook is called before the cron command to do some initial tasks
+ # (e.g. starting a webserver).
+
+ :
+}
+
+exit_hook() {
+ # This hook is called at the end of the cron command and can be used to
+ # do some final (cleanup or other) tasks.
+
+ :
+}
+
+HANDLER="$1"; shift
+if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
+ "$HANDLER" "$@"
+fi