diff --git a/acmebot/defaults/main.yml b/acmebot/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..edc0c9607e632df0bd0e3ae5bc4884bac472b012 --- /dev/null +++ b/acmebot/defaults/main.yml @@ -0,0 +1,223 @@ +--- + +acmebot_account_mail: "{{ adminaddr }}" +acmebot_after_nginx_proxy: true + +acmebot_settings: {} +acmebot_default_settings: + log_level: "detail" + color_output: true + + acme_directory_url: "https://acme-v02.api.letsencrypt.org/directory" + public_suffix_list_url: "https://publicsuffix.org/list/public_suffix_list.dat" + ocsp_responder_urls: + - "http://ocsp.int-x3.letsencrypt.org" + reload_zone_command: null + nsupdate_command: null + hpkp_report_uri: null + ct_submit_logs: + - "google_icarus" + - "google_pilot" + + file_user: root + file_group: ssl-cert + + # TODO default to both key types or single one? default to non-/custom params? + key_size: 4096 # null to turn off RSA certificates + key_curve: "secp384r1" # null to turn off ECDSA certificates + key_cipher: null + key_passphrase: null # null to turn off private key encryption + dhparam_size: 2048 # null to turn off custom dhparams + ecparam_curve: "secp384r1" # null to turn off custom EC params + + follower_mode: false + ocsp_must_staple: false # application support isn't good enough + auto_rollover: true # must be false on followers + pin_subdomains: false + verify: null # e.g. [443] + services: null # e.g. [nginx-proxy] + + hpkp_days: 60 + renewal_days: 30 + expiration_days: 730 + max_dns_lookup_attempts: 60 + dns_lookup_delay: 10 + max_domains_per_order: 100 + max_authorization_attempts: 30 + authorization_delay: 10 + cert_poll_time: 30 + max_ocsp_verify_attempts: 10 + ocsp_verify_retry_delay: 5 + min_run_delay: 300 + max_run_delay: 3600 + +# can be empty string, e.g. when using only one key type +acmebot_key_suffixes: {} +acmebot_default_key_suffixes: + rsa: ".rsa" + ecdsa: ".ecdsa" + +# format strings with: name (of privkey or cert), key_type, suffix, server +# http_challenge uses: zone, host (without zone, "." if fqdn == zone), fqdn +# if http_challenge is set, defaults to http-01 +# set to null for specified certs to use dns-01 for those +acmebot_directories: {} +acmebot_default_directories: + pid: "/run" + log: "/var/log/acmebot" + resource: "/var/lib/acmebot" + temp: null + + # TODO layout equivalent to acmetool + private_key: /etc/ssl/acmebot/privkey + backup_key: /etc/ssl/acmebot/backup_privkey + previous_key: null + full_key: /etc/ssl/acmebot/full_privkey # maybe null to turn off + certificate: /etc/ssl/acmebot/cert + full_certificate: /etc/ssl/acmebot/full_cert # maybe null + chain: /etc/ssl/acmebot/chain # maybe null + param: /etc/ssl/acmebot/params # maybe null + challenge: /etc/ssl/acmebot/challenges # for dns-01 only + http_challenge: /var/www/acme-challenge # maybe null + hpkp: /etc/ssl/acmebot/hpkp # maybe null + ocsp: /etc/ssl/acmebot/ocsp # maybe null + sct: "/etc/ssl/acmebot/scts/{name}/{key_type}" # maybe null + update_key: /etc/ssl/acmebot/update_keys + archive: /etc/ssl/acmebot/archive + +# format strings with: name (of privkey or cert), key_type, suffix, server +acmebot_file_names: {} +acmebot_default_file_names: + log: "acmebot.log" + + # TODO layout equivalent to acmetool + private_key: "{name}{suffix}.key" + backup_key: "{name}_backup{suffix}.key" + previous_key: "{name}_previous{suffix}.key" + full_key: "{name}_full{suffix}.key" + certificate: "{name}{suffix}.pem" + full_certificate: "{name}+root{suffix}.pem" + chain: "{name}_chain{suffix}.pem" + param: "{name}_param.pem" + challenge: "{name}" + hpkp: "{name}.{server}" + ocsp: "{name}{suffix}.ocsp" + sct: "{ct_log_name}.sct" + +# override with null +acmebot_hpkp_headers: {} +acmebot_default_hpkp_headers: + apache: "Header always set Public-Key-Pins \"{header}\"\n" + nginx: "add_header Public-Key-Pins \"{header}\" always;\n" + +acmebot_services: {} +acmebot_default_services: + dovecot: "systemctl restart dovecot" + mysql: "systemctl reload mysql" + nginx: "systemctl reload nginx" + nginx-proxy: "systemctl reload nginx-proxy" + postfix: "systemctl reload postfix" + postgresql: "systemctl reload postgresql" + prosody: "systemctl restart prosody" + +# authorizations to maintain without certficates (e.g. for master/follower) +acmebot_authorizations: {} +# <zone-name>: +# - <host-name> +# - <host-name> + +# when global http_challenges directory set: use null to revert back to dns-01 +# else: override dns-01 default with http-01 per domain +acmebot_http_challenges: {} +# <domain-name>: <challenge-directory> + +# for doing DNSSEC manually, specify TSIG keys +acmebot_zone_update_keys: {} + +# when using HPKP it may be beneficial to share private keys between certs +# this dict contains multiple certificate sections per private key, +# all key-specific config moved up +acmebot_private_keys: {} + +acmebot_certificates: {} +# <certificate-name>: +# common_name: <common-name> +# alt_names: +# <zone-name>: +# - "@", +# - <host-name> +# services: +# - <service-name> +# tlsa_records: +# <zone-name>: +# - <host-name> +# - host: <host-name> +# port: <port-number> +# usage: pkix-ee +# selector: spki +# protocol: tcp +# ttl: 300 +# dhparam_size: 2048 +# ecparam_curve: secp384r1 +# key_types: +# - rsa +# - ecdsa +# key_size: 4096 +# key_curve: secp384r1 +# key_cipher: blowfish +# key_passphrase: +# expiration_days: 730 +# auto_rollover: false +# hpkp_days: 30 +# pin_subdomains: true +# hpkp_report_uri: +# ocsp_must_staple: false +# ocsp_responder_urls: +# - "http://ocsp.int-x3.letsencrypt.org" +# ct_submit_logs: +# - google_icarus +# - google_pilot +# verify: +# - 443, +# - port: 25 +# hosts: +# - <domain-name> +# - <domain-name> +# starttls: smtp +# key_types: +# - rsa +# - ecdsa + +# all empty per default, see README for possible hook names +acmebot_hooks: {} + +# see also: https://www.certificate-transparency.org/known-logs +acmebot_ct_logs: {} +acmebot_default_ct_logs: + google_pilot: + url: "https://ct.googleapis.com/pilot" + id: "pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=" + google_icarus: + url: "https://ct.googleapis.com/icarus" + id: "KTxRllTIOWW6qlD8WAfUt2+/WHopctykwwz05UVH9Hg=" + google_rocketeer: + url: "https://ct.googleapis.com/rocketeer" + id: "7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/cs=" + google_skydiver: + url: "https://ct.googleapis.com/skydiver" + id: "u9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YU=" + google_argon2018: + url: "https://ct.googleapis.com/logs/argon2018" + id: "pFASaQVaFVReYhGrN7wQP2KuVXakXksXFEU+GyIQaiU=" + digicert: + url: "https://ct1.digicert-ct.com/log" + id: "VhQGmi/XwuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0=" + symantec_ct: + url: "https://ct.ws.symantec.com" + id: "3esdK3oNT6Ygi4GtgWhwfi6OnQHVXIiNPRHEzbbsvsw=" + symantec_vega: + url: "https://vega.ws.symantec.com" + id: "vHjh38X2PGhGSTNNoQ+hXwl5aSAJwIG08/aRfz7ZuKU=" + cloudflare_nimbus2018: + url: "https://ct.cloudflare.com/logs/nimbus2018" + id: "23Sv7ssp7LH+yj5xbSzluaq7NveEcYPHXZ1PN7Yfv2Q=" diff --git a/acmebot/files/service-after.conf b/acmebot/files/service-after.conf new file mode 100644 index 0000000000000000000000000000000000000000..a54ec72b702a5b82200d813310d1f8f5fc2fe5e0 --- /dev/null +++ b/acmebot/files/service-after.conf @@ -0,0 +1,2 @@ +[Unit] +After=nginx-proxy.service diff --git a/acmebot/handlers/main.yml b/acmebot/handlers/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..109aa69fe24fa659bfe116cdc86cc8a5b0ed44fb --- /dev/null +++ b/acmebot/handlers/main.yml @@ -0,0 +1,7 @@ +--- + +- name: reload systemd service files + systemd: daemon_reload=yes + +- name: update certificates + systemd: name=acmebot.service state=restarted diff --git a/acmebot/tasks/main.yml b/acmebot/tasks/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..5cb2fcbf697848da27409c1507037ec90f6d46bc --- /dev/null +++ b/acmebot/tasks/main.yml @@ -0,0 +1,83 @@ +--- +# TODO import account info from acmetool if present + +- name: ensure acmebot is installed + apt: + name: acmebot + state: present + +- name: ensure we can modify the systemd unit + file: + path: /etc/systemd/system/acmebot.service.d + state: directory + owner: root + group: root + mode: '0755' + notify: + - reload systemd service files + when: acmebot_after_nginx_proxy + +- name: ensure systemd waits for proxy service + copy: + src: service-after.conf + dest: /etc/systemd/system/acmebot.service.d/nginx-proxy.conf + owner: root + group: root + mode: '0644' + notify: + - reload systemd service files + when: acmebot_after_nginx_proxy + +- name: ensure systemd does not wait for proxy service + file: + path: /etc/systemd/system/acmebot.service.d/nginx-proxy.conf + state: absent + notify: + - reload systemd service files + when: not acmebot_after_nginx_proxy + +- name: ensure the acmebot is configured + template: + src: acmebot.yaml.j2 + dest: /etc/acmebot/acmebot.yaml + owner: root + group: root + mode: '0644' + notify: + - update certificates + +- name: ensure the LE root certificates are linked + file: + path: "/etc/acmebot/{{ item }}" + src: /etc/ssl/certs/ISRG_Root_X1.pem + state: link + with_items: + - root_cert.rsa.pem + - root_cert.ecdsa.pem + notify: + - update certificates + +# TODO initial run accepting TOS +- name: check if acmebot is configured + command: acmetool status + register: acmetool_status + changed_when: false +- name: initially configure acmebot + command: acmebot --detail + when: not acmetool_status.stdout is search(acmetool_endpoint) + +# TODO force run when cert store does not match configured certificates +- name: test if the desired certificates are present + stat: + path: "/var/lib/acme/live/{{item.hostnames[0]}}" + register: live_stat + changed_when: not live_stat.stat.exists + with_items: "{{acmetool_certificates}}" + notify: + - update certificates + +- name: ensure certificates are updated regularly + systemd: + name: acmebot.timer + enabled: true + state: started diff --git a/acmebot/templates/acmebot.yaml.j2 b/acmebot/templates/acmebot.yaml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..b2c9564349daabc08a960d4f5a2d11428dc940ea --- /dev/null +++ b/acmebot/templates/acmebot.yaml.j2 @@ -0,0 +1,43 @@ +--- + +{% set certificates = dict(certificates=acmebot_certificates) %} +{{ certificates|to_nice_yaml }} + +{% set private_keys = dict(private_keys=acmebot_private_keys) %} +{{ private_keys|to_nice_yaml }} + +account: + email: "{{ acmebot_account_mail }}" + +{% set settings = dict(settings=acmebot_default_settings|combine(acmebot_settings, recursive=True)) %} +{{ settings|to_nice_yaml }} + +{% set directories = dict(directories=acmebot_default_directories|combine(acmebot_directories)) %} +{{ directories|to_nice_yaml }} + +{% set key_type_suffixes = dict(key_type_suffixes=acmebot_default_key_suffixes|combine(acmebot_key_suffixes)) %} +{{ key_type_suffixes|to_nice_yaml }} + +{% set file_names = dict(file_names=acmebot_default_file_names|combine(acmebot_file_names)) %} +{{ file_names|to_nice_yaml }} + +{% set hpkp_headers = dict(hpkp_headers=acmebot_default_hpkp_headers|combine(acmebot_hpkp_headers)) %} +{{ hpkp_headers|to_nice_yaml }} + +{% set services = dict(services=acmebot_default_services|combine(acmebot_services)) %} +{{ services|to_nice_yaml }} + +{% set authorizations = dict(authorizations=acmebot_authorizations) %} +{{ authorizations|to_nice_yaml }} + +{% set http_challenges = dict(http_challenges=acmebot_http_challenges) %} +{{ http_challenges|to_nice_yaml }} + +{% set zone_update_keys = dict(zone_update_keys=acmebot_zone_update_keys) %} +{{ zone_update_keys|to_nice_yaml }} + +{% set hooks = dict(hooks=acmebot_hooks) %} +{{ hooks|to_nice_yaml }} + +{% set ct_logs = dict(ct_logs=acmebot_default_ct_logs|combine(acmebot_ct_logs, recursive=True)) %} +{{ ct_logs|to_nice_yaml }}