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*) Buy me a coffee @@ -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