diff --git a/Ansible/playbook_netcup_tlsa_dane.yaml b/Ansible/playbook_netcup_tlsa_dane.yaml new file mode 100644 index 0000000..12aa6c6 --- /dev/null +++ b/Ansible/playbook_netcup_tlsa_dane.yaml @@ -0,0 +1,303 @@ +--- +- name: Zertifikate auf Mailcow (vom NPM) aktualisieren + hosts: localhost + gather_facts: false + + vars: + # Host-Konfiguration + debug_dane: true + + nginxproxymanager_host: npm + mailcow_host: mailcow + tlsa_host_label: "_25._tcp.mail" + + # Mailcow Variablen + mailcow_cert_path: "/opt/mailcow-dockerized/data/assets/ssl/cert.pem" + mailcow_key_path: "/opt/mailcow-dockerized/data/assets/ssl/key.pem" + mailcow_owner: "root" + mailcow_group: "root" + + + tasks: + ################################################################ + # 1) Neues Zertifikat/Key vom NginxProxyManager einlesen + ################################################################ + - name: "Neues Zertifikat vom NginxProxyManager slurpen" + slurp: + src: "{{ letsencrypt_cert }}" + delegate_to: "{{ nginxproxymanager_host }}" + register: new_cert_slurp + + - name: "Neuen Key vom NginxProxyManager slurpen" + slurp: + src: "{{ letsencrypt_key }}" + delegate_to: "{{ nginxproxymanager_host }}" + register: new_key_slurp + + ################################################################ + # 2) Mailcow-Check und -Update + ################################################################ + - name: "Mailcow: Altes Zertifikat (cert.pem) auslesen" + slurp: + src: "{{ mailcow_cert_path }}" + delegate_to: "{{ mailcow_host }}" + register: mailcow_old_cert + ignore_errors: true + + - name: "Mailcow: Alter Key (key.pem) auslesen" + slurp: + src: "{{ mailcow_key_path }}" + delegate_to: "{{ mailcow_host }}" + register: mailcow_old_key + ignore_errors: true + + - name: "Mailcow: Prüfen, ob Zertifikat oder Key neu sind" + set_fact: + mailcow_cert_is_new: >- + {{ + (new_cert_slurp.content | b64decode) != + (mailcow_old_cert.content is defined + and (mailcow_old_cert.content | b64decode) + or '') + or + (new_key_slurp.content | b64decode) != + (mailcow_old_key.content is defined + and (mailcow_old_key.content | b64decode) + or '') + }} + + - name: "Mailcow: Debug-Ausgabe, ob das Zertifikat neu ist" + debug: + msg: "Mailcow: Zertifikat neu? {{ mailcow_cert_is_new }}" + + - name: "Mailcow: Zertifikatsupdate" + block: + - name: "Mailcow: Docker Compose stoppen" + ansible.builtin.shell: | + docker compose down + args: + chdir: "/opt/mailcow-dockerized/" + delegate_to: "{{ mailcow_host }}" + + - name: "Mailcow: Neues Zertifikat (cert.pem) kopieren" + copy: + content: "{{ new_cert_slurp.content | b64decode }}" + dest: "{{ mailcow_cert_path }}" + owner: "{{ mailcow_owner }}" + group: "{{ mailcow_group }}" + mode: "0644" + delegate_to: "{{ mailcow_host }}" + + - name: "Mailcow: Neuen Key (key.pem) kopieren" + copy: + content: "{{ new_key_slurp.content | b64decode }}" + dest: "{{ mailcow_key_path }}" + owner: "{{ mailcow_owner }}" + group: "{{ mailcow_group }}" + mode: "0644" + delegate_to: "{{ mailcow_host }}" + + - name: "Mailcow: Docker Compose neu starten (force-recreate)" + ansible.builtin.shell: | + docker compose up -d --force-recreate + args: + chdir: "/opt/mailcow-dockerized/" + delegate_to: "{{ mailcow_host }}" + when: mailcow_cert_is_new + + ################################################################ + # 3) TLSA-Record bei Netcup aktualisieren + ################################################################ + - name: "DANE: TLSA-Hash aus slurped cert.pem erzeugen (3 1 1)" + shell: | + echo "{{ mailcow_old_cert.content | b64decode }}" | + openssl x509 -pubkey -noout | + openssl pkey -pubin -outform DER | + openssl dgst -sha256 -binary | + hexdump -ve '1/1 "%.2x"' + register: tlsa_hash_result + changed_when: false + delegate_to: localhost + + - name: "DANE: CA Issuer URI aus slurped cert.pem extrahieren" + shell: | + echo "{{ mailcow_old_cert.content | b64decode }}" | + openssl x509 -text -noout | + grep "CA Issuers" | sed -n 's/.*URI://p' + register: ca_uri_result + changed_when: false + delegate_to: localhost + + - name: "DANE: TLSA-Hash der CA erzeugen (2 1 1)" + shell: | + wget -qO- "{{ ca_uri_result.stdout }}" | + openssl x509 -inform DER -outform PEM | + openssl x509 -pubkey -noout | + openssl pkey -pubin -outform DER | + openssl dgst -sha256 -binary | + hexdump -ve '1/1 "%.2x"' + register: tlsa_hash_ca_result + changed_when: false + when: ca_uri_result.stdout | length > 0 + delegate_to: localhost + + - name: "DANE: TLSA-Hashes speichern" + set_fact: + tlsa_hash_current: "{{ tlsa_hash_result.stdout }}" + tlsa_hash_ca: "{{ tlsa_hash_ca_result.stdout | default('') }}" + + - name: "DANE: Netcup API Login" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + return_content: true + body: + action: "login" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apipassword: "{{ netcup_api_password }}" + register: netcup_login + delegate_to: localhost + + - name: "DANE: Session-ID sichern" + set_fact: + netcup_session_id: "{{ netcup_login.json.responsedata.apisessionid }}" + + - name: "DANE: DNS-Records abrufen" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + return_content: true + body: + action: "infoDnsRecords" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apisessionid: "{{ netcup_session_id }}" + domainname: "{{ netcup_domain }}" + register: netcup_dns_info + delegate_to: localhost + + - name: "DANE: Bestehende TLSA-Records extrahieren" + set_fact: + existing_tlsa_record: >- + {{ + (netcup_dns_info.json.responsedata.dnsrecords | selectattr('hostname', 'equalto', tlsa_host_label) + | selectattr('type', 'equalto', 'TLSA') + | selectattr('destination', 'search', '^3 1 1 ') + | list | first) + }} + existing_tlsa_record_ca: >- + {{ + (netcup_dns_info.json.responsedata.dnsrecords | selectattr('hostname', 'equalto', tlsa_host_label) + | selectattr('type', 'equalto', 'TLSA') + | selectattr('destination', 'search', '^2 1 1 ') + | list | first) + }} + + - name: "DANE: TLSA-Record (3 1 1) aktualisieren" + block: + - name: "DANE: TLSA-Record (3 1 1) wird aktualisiert" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + status_code: 200 + body: + action: "updateDnsRecords" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apisessionid: "{{ netcup_session_id }}" + domainname: "{{ netcup_domain }}" + dnsrecordset: + dnsrecords: + - id: "{{ existing_tlsa_record.id if existing_tlsa_record is defined else omit }}" + hostname: "{{ tlsa_host_label }}" + type: "TLSA" + destination: "3 1 1 {{ tlsa_hash_current }}" + state: "yes" + delegate_to: localhost + + - name: "DANE: TLSA-Record (3 1 1) prüfen" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + return_content: true + body: + action: "infoDnsRecords" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apisessionid: "{{ netcup_session_id }}" + domainname: "{{ netcup_domain }}" + register: netcup_verify_311 + delegate_to: localhost + + - name: "DANE: TLSA-Record (3 1 1) verifizieren" + set_fact: + verified_tlsa_311: >- + {{ + (netcup_verify_311.json.responsedata.dnsrecords | selectattr('hostname', 'equalto', tlsa_host_label) + | selectattr('type', 'equalto', 'TLSA') + | selectattr('destination', 'equalto', '3 1 1 ' ~ tlsa_hash_current) + | list | length > 0) + }} + when: existing_tlsa_record is not defined or existing_tlsa_record.destination != ('3 1 1 ' ~ tlsa_hash_current) + + - name: "DANE: TLSA-Record (2 1 1) aktualisieren" + block: + - name: "DANE: TLSA-Record (2 1 1) wird aktualisiert" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + status_code: 200 + body: + action: "updateDnsRecords" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apisessionid: "{{ netcup_session_id }}" + domainname: "{{ netcup_domain }}" + dnsrecordset: + dnsrecords: + - id: "{{ existing_tlsa_record_ca.id if existing_tlsa_record_ca is defined else omit }}" + hostname: "{{ tlsa_host_label }}" + type: "TLSA" + destination: "2 1 1 {{ tlsa_hash_ca }}" + state: "yes" + delegate_to: localhost + + - name: "DANE: TLSA-Record (2 1 1) prüfen" + uri: + url: "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" + method: POST + body_format: json + return_content: true + body: + action: "infoDnsRecords" + param: + customernumber: "{{ netcup_customer_number }}" + apikey: "{{ netcup_api_key }}" + apisessionid: "{{ netcup_session_id }}" + domainname: "{{ netcup_domain }}" + register: netcup_verify_211 + delegate_to: localhost + + - name: "DANE: TLSA-Record (2 1 1) verifizieren" + set_fact: + verified_tlsa_211: >- + {{ + (netcup_verify_211.json.responsedata.dnsrecords | selectattr('hostname', 'equalto', tlsa_host_label) + | selectattr('type', 'equalto', 'TLSA') + | selectattr('destination', 'equalto', '2 1 1 ' ~ tlsa_hash_ca) + | list | length > 0) + }} + when: tlsa_hash_ca | length > 0 and + (existing_tlsa_record_ca is not defined or + existing_tlsa_record_ca.destination != ('2 1 1 ' ~ tlsa_hash_ca)) \ No newline at end of file