diff --git a/roles/lego/defaults/main.yml b/roles/lego/defaults/main.yml new file mode 100644 index 0000000..67cde81 --- /dev/null +++ b/roles/lego/defaults/main.yml @@ -0,0 +1,14 @@ +--- +# lego_certificate_domains: +# - cn: "dns.ows.cx" +# sans: [*.dns.ows.cx] + +# lego_certificate_destination: +# path: /etc/nginx/certs +# owner: root +# group: nginx + +# lego_services_reload: +# name: nginx +# # OR +# command: /usr/sbin/nginx -s reload diff --git a/roles/lego/tasks/main.yml b/roles/lego/tasks/main.yml index 3f6807d..1889c2e 100644 --- a/roles/lego/tasks/main.yml +++ b/roles/lego/tasks/main.yml @@ -1,14 +1,21 @@ --- - - name: Get latest lego version - github_release: - user: go-acme - repo: lego - action: latest_release - token: "{{ vault_github_token }}" + become: false delegate_to: localhost run_once: true - register: lego_version + when: lego_version is undefined + block: + - name: Get latest version from Github + github_release: # needs Python Module github3.py + user: go-acme + repo: lego + action: latest_release + token: "{{ vault_github_token }}" + register: lego_github_version + + - name: "{{ lego_github_version }}" + set_fact: + lego_version: "{{ lego_github_version.tag }}" - name: Set architecture alias set_fact: @@ -20,10 +27,10 @@ architecture_alias: "arm64" # noqa: var-naming[no-role-prefix] when: ansible_architecture == "aarch64" -- name: "Download lego from GitHub ({{ lego_version.tag }})" +- name: "Download lego from GitHub" get_url: url: "https://github.com/go-acme/lego/releases/download/\ - {{ lego_version.tag }}/lego_{{ lego_version.tag }}\ + {{ lego_version }}/lego_{{ lego_version }}\ _linux_{{ architecture_alias }}.tar.gz" dest: "/var/tmp/lego.tar.gz" mode: "0644" @@ -48,22 +55,44 @@ mode: "0755" state: directory +- name: Copy ACME renew-hook script + template: + src: "renew-hook.sh.j2" + dest: "{{ lego_config_dir }}/renew-hook.sh" + mode: "0750" + vars: + lego_cert_dir: "{{ lego_config_dir }}/certificates" + - name: Register lego and create cert - command: | - {{ lego_install_dir }}/lego --accept-tos - {% for dns in certificate_domains %} - --domains="{{ dns }}" - {% endfor %} - {{ lego_cli_params | join(' ') }} - run + shell: > + {{ lego_install_dir }}/lego --accept-tos + --domains="{{ item.cn }}" + {% if item.sans is defined and item.sans %} + {% for san in item.sans %}--domains="{{ san }}" {% endfor %} + {% endif %} + {{ lego_cli_params | join(' ') }} + run && + {{ lego_config_dir }}/renew-hook.sh '{{ item.cn }}' args: - creates: "/var/lib/lego/accounts" + creates: "{{ lego_certificate_destination.path | default(lego_config_dir + '/certificates') }}/{{ item.cn }}.crt" environment: '{ "{{ lego_provider|upper }}_API_KEY": "{{ vault_ionos_token_dns }}" }' + loop: "{{ lego_certificate_domains }}" + loop_control: + label: "{{ item.cn }}" - name: Copy lego systemd service template: - src: "{{ item }}.j2" - dest: "/etc/systemd/system/{{ item }}" + src: "lego.{{ item.1 }}.j2" + dest: "/etc/systemd/system/lego_{{ item.0.cn }}.{{ item.1 }}" mode: "0644" - loop: [lego.service, lego.timer] - notify: [Restart lego_service, Restart lego_timer] + loop: "{{ lego_certificate_domains | product(['service', 'timer']) | list }}" + loop_control: + label: "lego_{{ item.0.cn }}.{{ item.1 }}" + +- name: Start lego_timer + systemd: + daemon_reload: true + enabled: true + name: "lego_{{ item.cn }}.timer" + state: started + loop: "{{ lego_certificate_domains }}" diff --git a/roles/lego/templates/lego.service.j2 b/roles/lego/templates/lego.service.j2 index 65526a5..3fad8d4 100644 --- a/roles/lego/templates/lego.service.j2 +++ b/roles/lego/templates/lego.service.j2 @@ -1,19 +1,31 @@ ## Managed by Ansible ## [Unit] -Description=Run lego renew +Description=Renew Lets Encrypt certificate for {{ item.0.cn }} After=network-online.target [Service] Type=oneshot +{% if lego_provider == "ionos" %} Environment={{ lego_provider|upper }}_API_KEY={{ vault_ionos_token_dns }} +{% endif %} ExecStart={{ lego_install_dir }}/lego \ - {% for dns in certificate_domains %} - --domains="{{ dns }}" \ - {% endfor %} - {{ lego_cli_params|join(' ') }} \ - renew + --domains="{{ item.0.cn }}" \ +{% if item.0.sans is defined and item.0.sans %} +{% for san in item.0.sans %} + --domains="{{ san }}" \ +{% endfor %} +{% endif %} + {{ lego_cli_params | join(' ') }} \ + renew \ + --renew-hook="{{ lego_config_dir }}/renew-hook.sh {{ item.0.cn }}" User=root +# Restart if renewal fails, but not too quickly +RestartSec=12h +Restart=on-failure +StartLimitInterval=72h +StartLimitBurst=3 + [Install] WantedBy=multi-user.target diff --git a/roles/lego/templates/lego.timer.j2 b/roles/lego/templates/lego.timer.j2 index 2d494ca..35c01aa 100644 --- a/roles/lego/templates/lego.timer.j2 +++ b/roles/lego/templates/lego.timer.j2 @@ -1,11 +1,11 @@ ## Managed by Ansible ## [Unit] -Description=Start lego renew +Description=Timer for Lets Encrypt certificate renewal of {{ item.0.cn }} [Timer] Persistent=true -OnCalendar=Mon 04:00:00 +OnCalendar=Mon 03:00:00 RandomizedDelaySec=1h [Install] diff --git a/roles/lego/templates/renew-hook.sh.j2 b/roles/lego/templates/renew-hook.sh.j2 new file mode 100644 index 0000000..8b19198 --- /dev/null +++ b/roles/lego/templates/renew-hook.sh.j2 @@ -0,0 +1,134 @@ +#!/usr/bin/bash + +## Managed by Ansible ## + +# Variables set by Ansible +cert_src_path="{{ lego_cert_dir }}" + +# Certificate destination variables (if defined) +cert_dest_path="{{ lego_certificate_destination.path | default('') }}" +cert_owner="{{ lego_certificate_destination.owner | default('') }}" +cert_group="{{ lego_certificate_destination.group | default('') }}" + +# Service reload variables (if defined) +service_name="{{ lego_services_reload.name | default('') }}" +service_command="{{ lego_services_reload.command | default('') }}" + +copy_certificate_files() { + local domain="$1" + local success=true + + # Check if destination is defined + if [ -z "$cert_dest_path" ]; then + echo "No certificate destination defined, skipping copy" + return 0 + fi + + echo "Copying certificate files for $domain..." + echo "Copying to $cert_dest_path..." + + # Create destination directory if it doesn't exist + mkdir -p "$cert_dest_path" + + # Copy certificate files + cp "$cert_src_path/${domain}.crt" "$cert_dest_path/${domain}.crt" || success=false + cp "$cert_src_path/${domain}.key" "$cert_dest_path/${domain}.key" || success=false + + # Copy issuer cert if it exists + if [ -f "$cert_src_path/${domain}.issuer.crt" ]; then + cp "$cert_src_path/${domain}.issuer.crt" "$cert_dest_path/${domain}.issuer.crt" || success=false + fi + + # Set standard secure permissions + # 644 for certificates, 600 for keys + chmod 644 "$cert_dest_path/${domain}.crt" || success=false + chmod 600 "$cert_dest_path/${domain}.key" || success=false + + # Set issuer cert permissions if it exists + if [ -f "$cert_dest_path/${domain}.issuer.crt" ]; then + chmod 644 "$cert_dest_path/${domain}.issuer.crt" || success=false + fi + + # Set ownership if specified + if [ -n "$cert_owner" ] && [ -n "$cert_group" ]; then + if [ -f "$cert_dest_path/${domain}.issuer.crt" ]; then + chown "$cert_owner":"$cert_group" "$cert_dest_path/${domain}.crt" "$cert_dest_path/${domain}.key" "$cert_dest_path/${domain}.issuer.crt" || success=false + else + chown "$cert_owner":"$cert_group" "$cert_dest_path/${domain}.crt" "$cert_dest_path/${domain}.key" || success=false + fi + fi + + if $success; then + echo "Certificate files copied successfully" + return 0 + else + echo "Error copying certificate files" + return 1 + fi +} + +reload_service() { + local domain="$1" + local success=true + + # Check if service reload is defined + if [ -z "$service_name" ] && [ -z "$service_command" ]; then + echo "No service reload defined, skipping reload" + return 0 + fi + + echo "Reloading service..." + + if [ -n "$service_command" ]; then + echo "Running command: $service_command" + eval "$service_command" || success=false + elif [ -n "$service_name" ]; then + echo "Reloading $service_name..." + systemctl reload "$service_name" || systemctl restart "$service_name" || success=false + fi + + if $success; then + echo "Service reloaded successfully" + return 0 + else + echo "Error reloading service" + return 1 + fi +} + +# Check if domain is provided as parameter +if [ $# -lt 1 ]; then + echo "Error: Domain parameter is required" + echo "Usage: $0 " + exit 1 +fi + +# Get domain from parameter +domain="$1" + +# Main execution +echo "Certificate renewal hook triggered for $domain" + +# Call the functions +copy_certificate_files "$domain" +copy_result=$? + +reload_service "$domain" +reload_result=$? + +# Send webhook notification +message="$domain certificate was successfully renewed" + +if [ -n "$cert_dest_path" ]; then + message="${message}, files copied" +fi + +if [ -n "$service_name" ] || [ -n "$service_command" ]; then + message="${message}, and service reloaded" +fi + +if [ $copy_result -eq 0 ] && [ $reload_result -eq 0 ]; then + echo "$message" +else + echo "$domain certificate was renewed but post-renewal tasks failed" +fi diff --git a/roles/lego/vars/main.yml b/roles/lego/vars/main.yml index 58d395a..ddc50bc 100644 --- a/roles/lego/vars/main.yml +++ b/roles/lego/vars/main.yml @@ -1,21 +1,17 @@ --- +# Lego lego_install_dir: "/usr/local/bin" lego_config_dir: "/var/lib/lego" lego_provider: "ionos" -lego_cert_mail: !vault | +lego_cli_params: + - --path={{ lego_config_dir }} + - --email={{ vault_lego_cert_mail }} + - --dns={{ lego_provider }} + - --key-type=ec256 +vault_lego_cert_mail: !vault | $ANSIBLE_VAULT;1.2;AES256;dtsv-dev 32353064653631636431646333633664363866666439306235303138306461313266343939346463 6565636462656666366133653638333433393730656362360a333363623561646436613530623662 34623331313964316464653333646134353037333065373063346164623037663235316361646666 3466623937663061340a643863633034633665316364313065303166643363653366363063303261 34316163616637633837333539626337356563616566346561333439646565373665 -lego_cli_params: - - --path={{ lego_config_dir }} - - --email={{ lego_cert_mail }} - - --dns={{ lego_provider }} - - --key-type=ec384 - -# Certificates -certificate_domains: - - "twirling.de" - - "*.twirling.de" diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml index 57fbdae..2a71dcf 100644 --- a/roles/nginx/tasks/main.yml +++ b/roles/nginx/tasks/main.yml @@ -34,11 +34,14 @@ path: /etc/nginx/conf.d/default.conf state: absent -- name: Create global config folder +- name: Create additional config folder file: - path: "/etc/nginx/global" + path: "/etc/nginx/{{ item }}" mode: "0755" state: directory + loop: + - global + - snippets - name: Copy Nginx SSL Config template: diff --git a/roles/webserver/meta/main.yml b/roles/webserver/meta/main.yml index 0961c32..dd14205 100644 --- a/roles/webserver/meta/main.yml +++ b/roles/webserver/meta/main.yml @@ -1,5 +1,14 @@ --- dependencies: + - role: lego + vars: + lego_certificate_domains: + - cn: "{{ webserver_domain }}" + sans: ["*.{{ webserver_domain }}"] + lego_certificate_destination: + path: "{{ webserver_nginx_cert_path }}" + lego_services_reload: + name: nginx - role: rclone - role: nginx - role: nginx_exporter diff --git a/roles/webserver/tasks/nginx.yml b/roles/webserver/tasks/nginx.yml index d20d627..4a5d734 100644 --- a/roles/webserver/tasks/nginx.yml +++ b/roles/webserver/tasks/nginx.yml @@ -7,10 +7,10 @@ mode: "0644" loop: - {src: "nginx.conf.j2", dest: "/etc/nginx/nginx.conf"} - - {src: "cert.conf.j2", dest: "/etc/nginx/global/cert.conf"} + - {src: "cert.conf.j2", dest: "/etc/nginx/snippets/cert.conf"} - {src: "header.conf.j2", dest: "/etc/nginx/global/header.conf"} - {src: "proxy.conf.j2", dest: "/etc/nginx/global/proxy.conf"} - - {src: "php_optimization.j2", dest: "/etc/nginx/global/php_optimization"} + - {src: "php_optimization.j2", dest: "/etc/nginx/snippets/php_optimization.conf"} notify: Reload nginx - name: Copy virtual server configs @@ -21,26 +21,6 @@ with_fileglob: "../templates/conf.d/*.j2" notify: Reload nginx -## Certificates - -- name: Create Certificate directory - file: - path: "{{ webserver_nginx_cert_path }}" - state: directory - mode: "0755" - -- name: "Copy SSL certificates for {{ webserver_domain }}" - copy: - remote_src: true - # make sure that ssl certs are available - src: "{{ lego_config_dir }}/certificates/{{ webserver_domain }}.{{ item }}" - dest: "{{ webserver_nginx_cert_path }}/{{ webserver_domain }}.{{ item }}" - owner: root - group: root - mode: "0600" - loop: [crt, key, issuer.crt] - notify: Reload nginx - - name: Create nginx.service.d directory file: path: /etc/systemd/system/nginx.service.d diff --git a/roles/webserver/templates/conf.d/cloud.conf.j2 b/roles/webserver/templates/conf.d/cloud.conf.j2 index dd87c38..4960aff 100644 --- a/roles/webserver/templates/conf.d/cloud.conf.j2 +++ b/roles/webserver/templates/conf.d/cloud.conf.j2 @@ -28,7 +28,7 @@ server { http2 on; server_name {{ nextcloud_domain_name }} www.{{ nextcloud_domain_name }}; - include global/cert.conf; + include snippets/cert.conf; # Path to the root of your installation root {{ nextcloud_dir }}; @@ -83,7 +83,7 @@ server { # only for Nextcloud like below: include mime.types; types { - text/javascript js mjs; + text/javascript mjs; application/wasm wasm; } @@ -141,7 +141,7 @@ server { # to the URI, resulting in a HTTP 500 error response. location ~ \.php(?:$|/) { # Required for legacy support - rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri; + rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri; fastcgi_split_path_info ^(.+?\.php)(/.*)$; set $path_info $fastcgi_path_info; @@ -164,7 +164,7 @@ server { } # Serve static files - location ~ \.(?:css|js|mjs|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ { + location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac)$ { try_files $uri /index.php$request_uri; # HTTP response headers borrowed from Nextcloud `.htaccess` add_header Cache-Control "public, max-age=15778463$asset_immutable"; @@ -177,7 +177,7 @@ server { access_log off; # Optional: Don't log access to assets } - location ~ \.woff2?$ { + location ~ \.(otf|woff2?)$ { try_files $uri /index.php$request_uri; expires 7d; # Cache-Control policy borrowed from `.htaccess` access_log off; # Optional: Don't log access to assets diff --git a/roles/webserver/templates/conf.d/twirling.conf.j2 b/roles/webserver/templates/conf.d/twirling.conf.j2 index 0939da5..b788521 100644 --- a/roles/webserver/templates/conf.d/twirling.conf.j2 +++ b/roles/webserver/templates/conf.d/twirling.conf.j2 @@ -21,7 +21,7 @@ server { quic_gso on; server_name {{ webserver_domain }} www.{{ webserver_domain }}; - include global/cert.conf; + include snippets/cert.conf; include global/header.conf; # Path to the root of your installation diff --git a/web.yml b/web.yml index c0e3c8f..bbbc4fa 100644 --- a/web.yml +++ b/web.yml @@ -3,7 +3,6 @@ - name: Install Webserver hosts: WEB roles: - - lego - nextcloud - wordpress become: true