diff --git a/README.md b/README.md index a6e428b..49d0e4a 100644 --- a/README.md +++ b/README.md @@ -26,31 +26,41 @@ You need to have the Python library `cryptography` in version `>1.2.3` available * `ca_manage_openssl`: Install `openssl` package? (default: `true`) * `ca_ca_dir`: Directory to place CA and certificates (default: `/opt/ca`) +* `ca_ca_dir_owner`: CA directory owner (default: `root`) +* `ca_ca_dir_group`: CA directory group (default: `root`) * `ca_ca_days`: Runtime of the CA certificate (default: `3650`) -* `ca_ca_password`: Password of CA key (default: `ChangeMe`) +* `ca_ca_password`: Password of CA key (no default, should be defined by user) * `ca_localdir`: Temporary directory on Ansible management host (default: `/tmp/ca`) * `ca_local_become`: Use `become` on the Ansible controller. Used for creation of `ca_localdir`. (default: `false`) * `ca_ca_host`: Hostname of the CA host (default: `localhost`) -* `ca_country`: Setting for certificates (default: `EX`) -* `ca_state`: Setting for certificates (default: `EX`) -* `ca_locality`: Setting for certificates (default: `EX`) -* `ca_postalcode`: Setting for certificates (default: `1234`) -* `ca_organization`: Setting for certificates (default: `example`) -* `ca_organizationalunit`: Setting for certificates (default: `example`) +* `ca_country`: Setting for certificates (omitted by default) +* `ca_state`: Setting for certificates (omitted by default) +* `ca_locality`: Setting for certificates (omitted by default) +* `ca_postalcode`: Setting for certificates (omitted by default) +* `ca_organization`: Setting for certificates (omitted by default) +* `ca_organizationalunit`: Setting for certificates (omitted by default) * `ca_common_name`: CN for certificates (default: `{{ inventory_hostname }}`) -* `ca_email`: E-Mail address for certificates (default: `root@{{ ansible_fqdn }}`) -* `ca_altname_1`: First alt name (default: `{{ ansible_fqdn }}`) +* `ca_email`: E-Mail address for certificates (omitted by default) +* `ca_subject_alternative_name`: Value for certificate `subjectAltName` field (default: `DNS:{{ ca_altname_1 }},DNS:{{ ca_altname_2 }},DNS:{{ ca_altname_3}}`, omitted if all `ca_altnameX` varaibles are `null`) +* `ca_altname_1`: First default alt name (default: `{{ ansible_hostname }}`). Omitted when set to `null`. +* `ca_altname_2`: Second default alt name (default: `{{ ansible_fqdn }}`). Omitted when set to `null`. +* `ca_altname_3`: Third default alt name (default: `{{ inventory_hostname }}`). Omitted when set to `null`. +* `ca_ca_signing_key_algorithm`: CA key generation algorithm (default: `RSA`) +* `ca_ca_signing_key_params`: CA key generation command options (empty by default) * `ca_ca_keylength`: CA keylength (default: `2048`) -* `ca_server_cert`: Create server certificate as well (default: `true`) +* `ca_cert`: Create certificate (default skips CA host: `{{ inventory_hostname != ca_ca_host }}`). It's up to an operator to configure the certificate for TLS client or/and TLS server. +* `ca_extended_key_usage`: Configures certificate `extendedKeyUsage` field. For example, to support both client and server authentication pass `['clientAuth', 'serverAuth']` (default: omitted) +* `ca_server_cert`: Create server certificate as well (default: `false`) * `ca_logstash`: Create Logstash compatible certificate as well. Needs `ca_server_cert` to be set. (default: `false`) * `ca_etcd`: Create additional etcd compatible certificates. Requires `ca_etcd_group` to be defined. (default: `false`) * `ca_etcd_group`: Needs to be set to the group name of etcd nodes and will add the default IPv4 address of each node to the certificates. 127.0.0.1 will also be added by the role to the SAN for loopback purposes.(default: `undefined`) -* `ca_keypassphrase`: Password for the client key, default not defined +* `ca_keypassphrase`: Password for the leaf certificate key, default not defined * `ca_openssl_cipher`: Cipher to use for key creation, default not defined -* `ca_client_ca_dir`: Directory to place CA and certificates on the clients (default: `/opt/ca`) -* `ca_client_ca_dir_owner`: User to own the certificate directory on the clients (default: `root`) -* `ca_client_ca_dir_group`: Group to own the certificate directory on the clients (default: `root`) -* `ca_client_ca_dir_mode`: Permissions of the certificate directory on the clients (default: `0700`) +* `ca_certs_dir`: Directory to place key, CA and leaf certificates on the hosts (default: `/opt/ca`) +* `ca_certs_dir_owner`: User to own the certificate directory on the hosts (default: `root`) +* `ca_certs_dir_group`: Group to own the certificate directory on the hosts (default: `root`) +* `ca_certs_dir_mode`: Permissions of the certificate directory on the hosts (default: `0700`) +* `ca_key_algorithm`: End-user key generation algorithm (default: `{{ ca_ca_signing_key_algorithm }}`) * `ca_renew`: Renew certificates if they expire within `ca_check_valid_time` timeframe (default: `false`) * `ca_valid_time`: Valid time of new created certificates (default: `+365d`) * `ca_check_valid_time`: Timeframe to check if certificates will expire (default: `+2w`) @@ -66,11 +76,28 @@ All of these have the default value `false`. * `ca_ls7_workaround`: Enable pinning key parameters for a Logstash compatible key. These settings make sure the key works with a certain combination of OpenSSL and Logstash. Symptom: Logstash logs that a valid PKCS8 key is invalid. * `ca_ls7_workaround_cipher`: The cipher to use for the workaround (default: `PBE-SHA1-RC4-128`) +## Notification handlers + +It's possible to register handlers to run actions on certificate change. For example, to reload service and use the updated certificate. + +The following handler names are available for registration: + +* `Ansible-role-ca : on certificate change`: runs on certificate change +* `Ansible-role-ca : on server certificate change`: runs on server certificate change +* `Ansible-role-ca : on etcd certificate change`: runs on etcd certificate change +* `Ansible-role-ca : on etcd server certificate change`: runs on etcd server certificate change + + ## Example Playbook ## - hosts: all roles: - ca + handlers: + - name: "Ansible-role-ca : on certificate change" + ansible.builtin.systemd_service: + name: my_tls_service + state: reloaded ## Contributing ## diff --git a/defaults/main.yml b/defaults/main.yml index f00a5c0..a1afafa 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,31 +1,29 @@ --- - ca_manage_openssl: true ca_ca_dir: /opt/ca -ca_client_ca_dir: /opt/ca -ca_client_ca_dir_owner: root -ca_client_ca_dir_group: root -ca_client_ca_dir_mode: 0700 -ca_ca_password: ChangeMe +ca_ca_dir_owner: root +ca_ca_dir_group: root +ca_certs_dir: /opt/ca +ca_certs_dir_owner: root +ca_certs_dir_group: root +ca_certs_dir_mode: 0700 +ca_key_algorithm: "{{ ca_ca_signing_key_algorithm }}" ca_localdir: /tmp/ca ca_local_become: false ca_ca_host: localhost +ca_ca_signing_key_algorithm: RSA +ca_ca_signing_key_params: "" -ca_server_cert: true +ca_cert: "{{ inventory_hostname != ca_ca_host }}" +ca_server_cert: false ca_logstash: false ca_etcd: false -ca_country: EX -ca_state: EX -ca_locality: EX -ca_postalcode: 1234 -ca_organization: example -ca_organizationalunit: example ca_common_name: "{{ inventory_hostname }}" -ca_email: "root@{{ ansible_fqdn }}" ca_altname_1: "{{ ansible_hostname }}" ca_altname_2: "{{ ansible_fqdn }}" ca_altname_3: "{{ inventory_hostname }}" +ca_subject_alternative_name: "{{ [ca_altname_1, ca_altname_2, ca_altname_3] | reject('none') | map('regex_replace', '^(.*)$', 'DNS:\\1') | join(',') }}" ca_ca_keylength: 2048 ca_ls7_workaround: false @@ -35,5 +33,3 @@ ca_renew: false ca_ca_days: 3650 ca_valid_time: +365d ca_check_valid_time: +2w - -_ca_ca_openssl_version_3: false diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..3813edb --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,11 @@ +- name: "Ansible-role-ca : on certificate change" + ansible.builtin.meta: noop + +- name: "Ansible-role-ca : on server certificate change" + ansible.builtin.meta: noop + +- name: "Ansible-role-ca : on etcd certificate change" + ansible.builtin.meta: noop + +- name: "Ansible-role-ca : on etcd server certificate change" + ansible.builtin.meta: noop diff --git a/molecule/ca-renew/converge.yml b/molecule/ca-renew/converge.yml index 7ee9462..287edca 100644 --- a/molecule/ca-renew/converge.yml +++ b/molecule/ca-renew/converge.yml @@ -7,13 +7,30 @@ hosts: all vars: ca_ca_host: ca_default + ca_ca_password: ChangeMe + # for CA server separate CA and cert client directories allow to trigger notify action on client certificate change + ca_certs_dir: /opt/certs + ca_cert: true + ca_server_cert: true ca_logstash: true ca_etcd: true ca_etcd_group: molecule ca_ca_days: 3650 ca_valid_time: +365d ca_renew: true - tasks: - - name: "Include CA role" - include_role: - name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 + + handlers: + # runs once for both CAs for each notified host + - name: "Ansible-role-ca : on certificate change" + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.renewed_certificate" + state: touch + mode: 0600 diff --git a/molecule/ca-renew/prepare.yml b/molecule/ca-renew/prepare.yml index 6c96a2c..1c2ebbe 100644 --- a/molecule/ca-renew/prepare.yml +++ b/molecule/ca-renew/prepare.yml @@ -3,35 +3,53 @@ hosts: all vars: ca_ca_host: ca_default + ca_ca_password: ChangeMe + # for CA server separate CA and cert client directories allow to trigger notify action on client certificate change + ca_certs_dir: /opt/certs + ca_cert: true + ca_server_cert: true ca_logstash: true ca_etcd: true ca_etcd_group: molecule ca_ca_days: 5 ca_valid_time: "+14d" ca_renew: true - tasks: + pre_tasks: - name: Install Python libraries - pip: + ansible.builtin.pip: name: cryptography>= 1.2.3 - name: Install packages for RHEL - package: + ansible.builtin.package: name: - iproute - NetworkManager when: ansible_os_family == "RedHat" - name: Start NetworkManager - service: + ansible.builtin.service: name: NetworkManager state: started enabled: yes when: ansible_os_family == "RedHat" - name: Gather facts again to define ansible_default_ipv4 - setup: + ansible.builtin.setup: - - name: "Include CA role" - include_role: - name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 + + handlers: + # runs once for both CAs for each notified host + - name: "Ansible-role-ca : on certificate change" + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.initial_certificate" + state: touch + mode: 0600 diff --git a/molecule/ca-renew/tasks/verify_tasks.yml b/molecule/ca-renew/tasks/verify_tasks.yml new file mode 100644 index 0000000..77e382f --- /dev/null +++ b/molecule/ca-renew/tasks/verify_tasks.yml @@ -0,0 +1,178 @@ +- name: Verify signature on certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}.crt + changed_when: false + +- name: Verify signature on server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt + changed_when: false + +- name: Check if instance key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + register: instance_key_stat + +- name: Fail if instance key is missing + ansible.builtin.fail: + msg: "Instance key is missing" + when: + - not instance_key_stat.stat.exists | bool + +- name: Check if Logstash key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" + register: logstash_key_stat + +- name: Fail if Logstash key is missing + ansible.builtin.fail: + msg: "Logstash key is missing" + when: + - not logstash_key_stat.stat.exists | bool + +- name: Verify signature on etcd peer certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Register SAN of etcd peer certificate + ansible.builtin.command: > + openssl x509 + -text + -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + register: etcd_san_peer_stat + changed_when: false + +- name: Register SAN of etcd server certificate + ansible.builtin.command: > + openssl x509 + -text + -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + + register: etcd_san_server_stat + changed_when: false + +- name: Fail if SAN of etcd peer certificate is missing IP addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd peer certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_peer_stat.stdout + - '"127.0.0.1" not in etcd_san_peer_stat.stdout' + +- name: Fail if SAN of etcd server certificate is missing IP addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd server certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_server_stat.stdout + - '"127.0.0.1" not in etcd_san_server_stat.stdout' + +- name: Check if notAfter of client certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}.crt + changed_when: false + +- name: Check if notAfter of server certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt + changed_when: false + +- name: Check if notAfter of etcd certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + changed_when: false + +- name: Check if notAfter of etcd server certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Check if notAfter of CA certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ ca_ca_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/ca.crt + changed_when: false + +- name: Register if 'on certificate change' handler has run for initial client certificate + ansible.builtin.stat: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.initial_certificate" + register: initial_handler_file + +- name: Fail if initial file of 'on certificate change' handler does not exist + ansible.builtin.fail: + msg: "Failed because 'on certificate change' handler hasn't created initial file" + when: not initial_handler_file.stat.exists + +- name: Register if 'on certificate change' handler has run for renewed client certificate + ansible.builtin.stat: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.renewed_certificate" + register: renewed_handler_file + +- name: Fail if renewed file of 'on certificate change' handler does not exist + ansible.builtin.fail: + msg: "Failed because 'on certificate change handler' hasn't created renewed file" + when: not renewed_handler_file.stat.exists diff --git a/molecule/ca-renew/verify.yml b/molecule/ca-renew/verify.yml index beb2c9b..4149dbe 100644 --- a/molecule/ca-renew/verify.yml +++ b/molecule/ca-renew/verify.yml @@ -1,206 +1,24 @@ --- - - name: Verify hosts: all vars: - ca_ca_dir: /opt/ca - ca_client_ca_dir: /opt/ca + ca_ca_password: ChangeMe + ca_cert: true + ca_server_cert: true + valid_days: 365 + ca_ca_days: 5 + # allowed delta between certificate generation and notAfter validation + max_test_duration_seconds: 900 tasks: - - - name: Verify signature on certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt - - - name: Verify signature on server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt - - - name: Check if instance key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - register: instance_key_stat - - - name: Fail if instance key is missing - fail: - msg: "Instance key is missing" - when: - - not instance_key_stat.stat.exists | bool - - - name: Check if Logstash key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" - register: logstash_key_stat - - - name: Fail if Logstash key is missing - fail: - msg: "Logstash key is missing" - when: - - not logstash_key_stat.stat.exists | bool - - - name: Verify signature on etcd peer certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Register SAN of etcd peer certificate - command: > - openssl x509 - -text - -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - register: etcd_san_peer_stat - - - name: Register SAN of etcd server certificate - command: > - openssl x509 - -text - -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - - register: etcd_san_server_stat - - - name: Fail if SAN of etcd peer certificate is missing IP addresses - fail: - msg: "Default IPv4 address in etcd peer certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_peer_stat.stdout - - '"127.0.0.1" not in etcd_san_peer_stat.stdout' - - - name: Fail if SAN of etcd server certificate is missing IP addresses - fail: - msg: "Default IPv4 address in etcd server certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_server_stat.stdout - - '"127.0.0.1" not in etcd_san_server_stat.stdout' - - - name: Get next year - set_fact: - next_year: "{{ ( ansible_date_time.date.split('-')[0] | int ) +1 }}" - - - name: Register notAfter of client certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt - | grep notAfter - register: client_crt_stat - - - name: Fail if notAfter of client certificate is not next year - fail: - msg: "Failed: notAfter of client certificate is not next year" - when: next_year | string not in client_crt_stat.stdout - - - name: Register notAfter of server certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt - | grep notAfter - register: server_crt_stat - - - name: Fail if notAfter of server certificate is not next year - fail: - msg: "Failed: notAfter of server certificate is not next year" - when: next_year | string not in server_crt_stat.stdout - - - name: Register notAfter of etcd certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - | grep notAfter - register: etcd_crt_stat - - - name: Fail if notAfter of etcd certificate is not next year - fail: - msg: "Failed: notAfter of etcd certificate is not next year" - when: next_year | string not in etcd_crt_stat.stdout - - - name: Register notAfter of etcd server certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - | grep notAfter - register: etcd_server_crt_stat - - - name: Fail if notAfter of etcd server certificate is not next year - fail: - msg: "Failed: notAfter of etcd server certificate is not next year" - when: next_year | string not in etcd_server_crt_stat.stdout - - - name: Get year of next decade to check CA - set_fact: - next_decade: "{{ ( ansible_date_time.date.split('-')[0] | int ) +10 }}" - - - name: Register notAfter of CA certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/ca.crt - | grep notAfter - register: ca_crt_stat - - # Caution: - # Can fail at end or beginning of year, since - # leap year days can be +- 3 - - name: Fail if notAfter of CA certificate is not next decade - fail: - msg: "Failed: notAfter of CA certificate is not next decade" - when: next_decade | string not in ca_crt_stat.stdout + - name: Verify RSA + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_dir: /opt/ca + ca_certs_dir: /opt/certs + - name: Verify Ed25519 + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index fddc1cd..b027e82 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -7,10 +7,17 @@ hosts: all vars: ca_ca_host: ca_default + ca_ca_password: ChangeMe + ca_cert: true + ca_server_cert: true ca_logstash: true ca_etcd: true ca_etcd_group: molecule - tasks: - - name: "Include CA role" - include_role: - name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/ca-ed25519 + ca_localdir: /tmp/ca-ed25519 diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml index f18d0b1..d645126 100644 --- a/molecule/default/prepare.yml +++ b/molecule/default/prepare.yml @@ -4,18 +4,18 @@ tasks: - name: Install Python libraries - pip: + ansible.builtin.pip: name: cryptography>= 1.2.3 - name: Install packages for RHEL - package: + ansible.builtin.package: name: - iproute - NetworkManager when: ansible_os_family == "RedHat" - name: Start NetworkManager - service: + ansible.builtin.service: name: NetworkManager state: started enabled: yes diff --git a/molecule/default/tasks/verify_tasks.yml b/molecule/default/tasks/verify_tasks.yml new file mode 100644 index 0000000..2129bb1 --- /dev/null +++ b/molecule/default/tasks/verify_tasks.yml @@ -0,0 +1,116 @@ +- name: Verify signature on certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}.crt + changed_when: false + +- name: Verify signature on server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt + changed_when: false + +- name: Check if instance key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + register: instance_key_stat + +- name: Fail if instance key is missing + ansible.builtin.fail: + msg: "Instance key is missing" + when: + - not instance_key_stat.stat.exists | bool + +- name: Check if Logstash key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" + register: logstash_key_stat + +- name: Fail if Logstash key is missing + ansible.builtin.fail: + msg: "Logstash key is missing" + when: + - not logstash_key_stat.stat.exists | bool + +- name: Verify signature on etcd peer certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Register SAN of etcd peer certificate + ansible.builtin.command: > + openssl x509 + -text -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + register: etcd_san_peer_stat + changed_when: false + +- name: Register SAN of etcd server certificate + ansible.builtin.command: > + openssl x509 + -text + -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + register: etcd_san_server_stat + changed_when: false + +- name: Fail if SAN of etcd peer certificate is missing IP addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd peer certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_peer_stat.stdout + - '"127.0.0.1" not in etcd_san_peer_stat.stdout' + +- name: Fail if SAN of etcd server certificate is missing IP addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd server certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_server_stat.stdout + - '"127.0.0.1" not in etcd_san_server_stat.stdout' diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 789fab0..22b9996 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -1,118 +1,20 @@ --- - - name: Verify hosts: all vars: - ca_ca_dir: /opt/ca - ca_client_ca_dir: /opt/ca + ca_ca_password: ChangeMe + ca_cert: true + ca_server_cert: true tasks: - - - name: Verify signature on certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt - - - name: Verify signature on server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt - - - name: Check if instance key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - register: instance_key_stat - - - name: Fail if instance key is missing - fail: - msg: "Instance key is missing" - when: - - not instance_key_stat.stat.exists | bool - - - name: Check if Logstash key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" - register: logstash_key_stat - - - name: Fail if Logstash key is missing - fail: - msg: "Logstash key is missing" - when: - - not logstash_key_stat.stat.exists | bool - - - name: Verify signature on etcd peer certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Register SAN of etcd peer certificate - command: > - openssl x509 - -text -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - register: etcd_san_peer_stat - - - name: Register SAN of etcd server certificate - command: > - openssl x509 - -text - -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - register: etcd_san_server_stat - - - name: Fail if SAN of etcd peer certificate is missing IP addresses - fail: - msg: "Default IPv4 address in etcd peer certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_peer_stat.stdout - - '"127.0.0.1" not in etcd_san_peer_stat.stdout' - - - name: Fail if SAN of etcd server certificate is missing IP addresses - fail: - msg: "Default IPv4 address in etcd server certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_server_stat.stdout - - '"127.0.0.1" not in etcd_san_server_stat.stdout' + - name: Verify RSA + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_dir: /opt/ca + ca_certs_dir: /opt/ca + - name: Verify Ed25519 + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/ca-ed25519 + ca_localdir: /tmp/ca-ed25519 diff --git a/molecule/renew/converge.yml b/molecule/renew/converge.yml index 4290d53..2c81929 100644 --- a/molecule/renew/converge.yml +++ b/molecule/renew/converge.yml @@ -7,14 +7,31 @@ hosts: all vars: ca_ca_host: ca_default + ca_ca_password: ChangeMe + # for CA server separate CA and cert client directories allow to trigger notify action on client certificate change + ca_certs_dir: /opt/certs + ca_cert: true + ca_server_cert: true ca_logstash: true ca_etcd: true ca_etcd_group: molecule - ca_ca_days: 3650 ca_valid_time: +365d ca_check_valid_time: +1w ca_renew: true - tasks: - - name: "Include CA role" - include_role: - name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 + + handlers: + # runs once for both CAs for each notified host + - name: "Ansible-role-ca : on certificate change" + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.renewed_certificate" + state: touch + mode: 0600 diff --git a/molecule/renew/prepare.yml b/molecule/renew/prepare.yml index 55f3ae4..c6ecc7f 100644 --- a/molecule/renew/prepare.yml +++ b/molecule/renew/prepare.yml @@ -3,34 +3,51 @@ hosts: all vars: ca_ca_host: ca_default + ca_ca_password: ChangeMe + # for CA server separate CA and cert client directories allow to trigger notify action on client certificate change + ca_certs_dir: /opt/certs + ca_cert: true + ca_server_cert: true ca_logstash: true ca_etcd: true ca_etcd_group: molecule ca_valid_time: "+5d" ca_renew: true - tasks: - + pre_tasks: - name: Install Python libraries - pip: + ansible.builtin.pip: name: cryptography>= 1.2.3 - name: Install packages for RHEL - package: + ansible.builtin.package: name: - iproute - NetworkManager when: ansible_os_family == "RedHat" - name: Start NetworkManager - service: + ansible.builtin.service: name: NetworkManager state: started enabled: yes when: ansible_os_family == "RedHat" - name: Gather facts again to define ansible_default_ipv4 - setup: + ansible.builtin.setup: + + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 - - name: "Include CA role" - include_role: - name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + handlers: + # runs once for both CAs for each notified host + - name: "Ansible-role-ca : on certificate change" + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.initial_certificate" + state: touch + mode: 0600 diff --git a/molecule/renew/tasks/verify_tasks.yml b/molecule/renew/tasks/verify_tasks.yml new file mode 100644 index 0000000..7c90e59 --- /dev/null +++ b/molecule/renew/tasks/verify_tasks.yml @@ -0,0 +1,177 @@ +- name: Verify signature on certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}.crt + changed_when: false + +- name: Verify signature on server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt + changed_when: false + +- name: Check if instance key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + register: instance_key_stat + +- name: Fail if instance key is missing + ansible.builtin.fail: + msg: "Instance key is missing" + when: + - not instance_key_stat.stat.exists | bool + +- name: Check if Logstash key is present + ansible.builtin.stat: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" + register: logstash_key_stat + +- name: Fail if Logstash key is missing + ansible.builtin.fail: + msg: "Logstash key is missing" + when: + - not logstash_key_stat.stat.exists | bool + +- name: Verify signature on etcd peer certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Verify signature on etcd server certificate + ansible.builtin.command: > + openssl verify + -verbose + -CAfile {{ ca_certs_dir }}/ca.crt + {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Register SAN of etcd peer certificate + ansible.builtin.command: > + openssl x509 + -text + -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + register: etcd_san_peer_stat + changed_when: false + +- name: Register SAN of etcd server certificate + ansible.builtin.command: > + openssl x509 + -text + -noout + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + -certopt " + no_subject, + no_header, + no_version, + no_serial, + no_signame, + no_validity, + no_issuer, + no_pubkey, + no_sigdump, + no_aux" + register: etcd_san_server_stat + changed_when: false + +- name: Fail if SAN of etcd peer certificate is missing addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd peer certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_peer_stat.stdout + - '"127.0.0.1" not in etcd_san_peer_stat.stdout' + +- name: Fail if SAN of etcd server certificate is missing IP addresses + ansible.builtin.fail: + msg: "Default IPv4 address in etcd server certifcate are missing" + when: + - hostvars['ca_default_client']['ansible_default_ipv4']['address'] + not in etcd_san_server_stat.stdout + - '"127.0.0.1" not in etcd_san_server_stat.stdout' + +- name: Check if notAfter of client certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}.crt + changed_when: false + +- name: Check if notAfter of server certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt + changed_when: false + +- name: Check if notAfter of etcd certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt + changed_when: false + +- name: Check if notAfter of etcd server certificate is within valid_days={{ valid_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ valid_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt + changed_when: false + +- name: Check if notAfter of CA certificate is valid within valid_days={{ ca_ca_days }} + ansible.builtin.command: > + openssl x509 + -noout + -checkend {{ ca_ca_days * 24 * 60 * 60 - max_test_duration_seconds }} + -in {{ ca_certs_dir }}/ca.crt + changed_when: false + +- name: Register if on certificate change handler has run for initial client certificate + ansible.builtin.stat: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.initial_certificate" + register: initial_handler_file + +- name: Fail if initial file of on certificate change handler does not exist + ansible.builtin.fail: + msg: "Failed because on certificate change handler hasn't created initial file" + when: not initial_handler_file.stat.exists + +- name: Register if on certificate change handler has run for renewed client certificate + ansible.builtin.stat: + path: "{{ ansible_env.HOME }}/{{ inventory_hostname }}.renewed_certificate" + register: renewed_handler_file + +- name: Fail if renewed file of on certificate change handler does not exist + ansible.builtin.fail: + msg: "Failed because on certificate change handler hasn't created renewed file" + when: not renewed_handler_file.stat.exists diff --git a/molecule/renew/verify.yml b/molecule/renew/verify.yml index 147a8a8..08e4b1b 100644 --- a/molecule/renew/verify.yml +++ b/molecule/renew/verify.yml @@ -1,205 +1,24 @@ --- - - name: Verify hosts: all vars: - ca_ca_dir: /opt/ca - ca_client_ca_dir: /opt/ca + ca_ca_password: ChangeMe + ca_ca_days: 3650 + valid_days: 5 + ca_cert: true + ca_server_cert: true + # allowed delta between certificate generation and notAfter validation + max_test_duration_seconds: 900 tasks: - - - name: Verify signature on certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt - - - name: Verify signature on server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt - - - name: Check if instance key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - register: instance_key_stat - - - name: Fail if instance key is missing - fail: - msg: "Instance key is missing" - when: - - not instance_key_stat.stat.exists | bool - - - name: Check if Logstash key is present - stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" - register: logstash_key_stat - - - name: Fail if Logstash key is missing - fail: - msg: "Logstash key is missing" - when: - - not logstash_key_stat.stat.exists | bool - - - name: Verify signature on etcd peer certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Verify signature on etcd server certificate - command: > - openssl verify - -verbose - -CAfile {{ ca_ca_dir }}/ca.crt - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - - - name: Register SAN of etcd peer certificate - command: > - openssl x509 - -text - -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - register: etcd_san_peer_stat - - - name: Register SAN of etcd server certificate - command: > - openssl x509 - -text - -noout - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - -certopt " - no_subject, - no_header, - no_version, - no_serial, - no_signame, - no_validity, - no_issuer, - no_pubkey, - no_sigdump, - no_aux" - register: etcd_san_server_stat - - - name: Fail if SAN of etcd peer certificate is missing addresses - fail: - msg: "Default IPv4 address in etcd peer certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_peer_stat.stdout - - '"127.0.0.1" not in etcd_san_peer_stat.stdout' - - - name: Fail if SAN of etcd server certificate is missing IP addresses - fail: - msg: "Default IPv4 address in etcd server certifcate are missing" - when: - - hostvars['ca_default_client']['ansible_default_ipv4']['address'] - not in etcd_san_server_stat.stdout - - '"127.0.0.1" not in etcd_san_server_stat.stdout' - - - name: Get next year - set_fact: - next_year: "{{ ( ansible_date_time.date.split('-')[0] | int ) +1 }}" - - - name: Register notAfter of client certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt - | grep notAfter - register: client_crt_stat - - - name: Fail if notAfter of client certificate is not next year - fail: - msg: "Failed: notAfter of client certificate is not next year" - when: next_year | string not in client_crt_stat.stdout - - - name: Register notAfter of server certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt - | grep notAfter - register: server_crt_stat - - - name: Fail if notAfter of server certificate is not next year - fail: - msg: "Failed: notAfter of server certificate is not next year" - when: next_year | string not in server_crt_stat.stdout - - - name: Register notAfter of etcd certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt - | grep notAfter - register: etcd_crt_stat - - - name: Fail if notAfter of etcd certificate is not next year - fail: - msg: "Failed: notAfter of etcd certificate is not next year" - when: next_year | string not in etcd_crt_stat.stdout - - - name: Register notAfter of etcd server certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt - | grep notAfter - register: etcd_server_crt_stat - - - name: Fail if notAfter of etcd server certificate is not next year - fail: - msg: "Failed: notAfter of etcd server certificate isn't next year" - when: next_year | string not in etcd_server_crt_stat.stdout - - - name: Get year of next decade to check CA - set_fact: - next_decade: "{{ ( ansible_date_time.date.split('-')[0] | int ) +10 }}" - - - name: Register notAfter of CA certificate - shell: > - if test -n "$(ps -p $$ | grep bash)"; then set -o pipefail; fi; - openssl x509 - -noout - -dates - -in {{ ca_client_ca_dir }}/ca.crt - | grep notAfter - register: ca_crt_stat - - # Caution: - # Can fail at end or beginning of year, since - # leap year days can be +- 3 - - name: Fail if notAfter of CA certificate is not next decade - fail: - msg: "Failed because notAfter of CA certificate is not next decade" - when: next_decade | string not in ca_crt_stat.stdout + - name: Verify RSA + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_dir: /opt/ca + ca_certs_dir: /opt/certs + - name: Verify Ed25519 + ansible.builtin.include_tasks: tasks/verify_tasks.yml + vars: + ca_ca_signing_key_algorithm: Ed25519 + ca_ca_dir: /opt/ca-ed25519 + ca_certs_dir: /opt/certs-ed25519 + ca_localdir: /tmp/ca-ed25519 diff --git a/tasks/ca.yml b/tasks/ca.yml index a0da62d..e05acbc 100644 --- a/tasks/ca.yml +++ b/tasks/ca.yml @@ -1,11 +1,10 @@ --- - - name: Place CA configuration file ansible.builtin.template: src: ca.conf.j2 dest: "{{ ca_ca_dir }}/ca.conf" - owner: root - group: root + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" mode: 0600 - name: Check if CA key is already created @@ -16,7 +15,6 @@ - name: Test and prepare key when: cakey.stat.exists block: - - name: Test CA key if present community.crypto.openssl_privatekey_info: path: "{{ ca_ca_dir }}/ca.key" @@ -28,7 +26,6 @@ when: - not cakeyinfo.can_parse_key | bool block: - - name: Move old key ansible.builtin.command: > mv @@ -45,20 +42,37 @@ args: creates: "{{ ca_ca_dir }}/ca.key.{{ ansible_date_time.iso8601 }}" +- name: Generate RSA CA key + ansible.builtin.command: + cmd: > + openssl genrsa + -aes256 + -passout stdin + -out {{ ca_ca_dir }}/ca.key + {{ ca_ca_signing_key_params }} + {{ ca_ca_keylength }} + stdin: "{{ ca_ca_password }}" + args: + creates: "{{ ca_ca_dir }}/ca.key" + when: ca_ca_signing_key_algorithm == 'RSA' + - name: Generate CA key - ansible.builtin.command: > - openssl genrsa - -aes256 - -passout pass:{{ ca_ca_password }} - -out {{ ca_ca_dir }}/ca.key - {{ ca_ca_keylength }} + ansible.builtin.command: + cmd: > + openssl genpkey + -algorithm {{ ca_ca_signing_key_algorithm }} + -pass stdin + -aes256 + -out {{ ca_ca_dir }}/ca.key + {{ ca_ca_signing_key_params }} + stdin: "{{ ca_ca_password }}" args: creates: "{{ ca_ca_dir }}/ca.key" + when: ca_ca_signing_key_algorithm != 'RSA' - name: Renew CA certificate when: ca_renew | bool block: - - name: Check if CA certificate has to be renewed ansible.builtin.import_tasks: renew.yml vars: @@ -80,17 +94,17 @@ when: crt_exists.stat.exists | bool - name: Generate CA certificate - ansible.builtin.command: > - openssl req - -x509 - -new - -nodes - -key {{ ca_ca_dir }}/ca.key - -passin pass:{{ ca_ca_password }} - -sha256 - {{ _ca_openssl_ca_ext_opts }} - -days {{ ca_ca_days }} - -out {{ ca_ca_dir }}/ca.crt - -config {{ ca_ca_dir }}/ca.conf + ansible.builtin.command: + cmd: > + openssl req + -new + -x509 + -key {{ ca_ca_dir }}/ca.key + -passin stdin + -sha256 + -days {{ ca_ca_days }} + -out {{ ca_ca_dir }}/ca.crt + -config {{ ca_ca_dir }}/ca.conf + stdin: "{{ ca_ca_password }}" args: creates: "{{ ca_ca_dir }}/ca.crt" diff --git a/tasks/main.yml b/tasks/main.yml index f7e5435..36eb17e 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,21 +1,13 @@ --- - -- name: Ensure CA directory exists on clients - ansible.builtin.file: - path: "{{ ca_client_ca_dir }}" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" - state: directory - - name: Ensure CA directory exists ansible.builtin.file: path: "{{ ca_ca_dir }}" - owner: root - group: root + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" mode: 0700 state: directory delegate_to: "{{ ca_ca_host }}" + run_once: true - name: Ensure local directory on Ansible host exists ansible.builtin.file: @@ -24,99 +16,17 @@ mode: 0700 become: "{{ ca_local_become }}" delegate_to: localhost + run_once: true - name: Install openssl ansible.builtin.package: name: openssl when: ca_manage_openssl | bool -- name: Check OpenSSL version - ansible.builtin.import_tasks: openssl_detect.yml - - name: Set up ca ansible.builtin.import_tasks: ca.yml when: ca_ca_host == inventory_hostname -- name: Set ca_san to hostname, FQDN, and inventory hostname - ansible.builtin.set_fact: - ca_san: >- - DNS:{{ ansible_hostname }}, - DNS:{{ ansible_fqdn }}, - DNS:{{ inventory_hostname }} - -### client certificate ### - -- name: Create key - community.crypto.openssl_privatekey: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - passphrase: "{{ ca_keypassphrase | default(omit, true) }}" - cipher: "{{ ca_openssl_cipher | default(omit, true) }}" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" - register: key - -- name: Create CSR - community.crypto.openssl_csr: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.csr" - privatekey_path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - privatekey_passphrase: "{{ ca_keypassphrase | default(omit, true) }}" - country_name: "{{ ca_country }}" - organization_name: "{{ ca_organization }}" - common_name: "{{ inventory_hostname }}" - subject_alt_name: "{{ ca_san | regex_replace(' ', '') }}" - extended_key_usage: "{{ extended_key_usage | default(omit, true) }}" - -- name: Pull CSR - ansible.builtin.fetch: - src: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.csr" - dest: "{{ ca_localdir }}/{{ inventory_hostname }}.csr" - flat: true - -- name: Push CSR to CA host - ansible.builtin.copy: - src: "{{ ca_localdir }}/{{ inventory_hostname }}.csr" - dest: "{{ ca_ca_dir }}/{{ inventory_hostname }}.csr" - owner: root - group: root - mode: 0600 - delegate_to: "{{ ca_ca_host }}" - -- name: Check if client certificate has to be renewed - ansible.builtin.import_tasks: renew.yml - vars: - crt_path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt" - when: ca_renew | bool - -- name: (Re)sign CSR with CA key - community.crypto.x509_certificate: - path: "{{ ca_ca_dir }}/{{ inventory_hostname }}.crt" - csr_path: "{{ ca_ca_dir }}/{{ inventory_hostname }}.csr" - ownca_path: "{{ ca_ca_dir }}/ca.crt" - ownca_privatekey_path: "{{ ca_ca_dir }}/ca.key" - ownca_privatekey_passphrase: "{{ ca_ca_password }}" - ownca_not_after: "{{ ca_valid_time }}" - provider: ownca - force: - "{{ not crt_info.valid_at.check_period | default(omit) or - hostvars[ca_ca_host]['ca_ca_renewed'] | default(omit) }}" - backup: true - delegate_to: "{{ ca_ca_host }}" - -- name: Fetch certificate - ansible.builtin.fetch: - src: "{{ ca_ca_dir }}/{{ inventory_hostname }}.crt" - dest: "{{ ca_localdir }}/{{ inventory_hostname }}.crt" - flat: true - delegate_to: "{{ ca_ca_host }}" - -- name: Push certificate to client - ansible.builtin.copy: - src: "{{ ca_localdir }}/{{ inventory_hostname }}.crt" - dest: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.crt" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" - name: Fetch CA certificate ansible.builtin.fetch: @@ -124,35 +34,124 @@ dest: "{{ ca_localdir }}/ca.crt" flat: true delegate_to: "{{ ca_ca_host }}" + run_once: true + +### certificate ### + +- name: Create certificate + when: ca_cert | bool + block: + - name: Ensure CA cert directory exists + ansible.builtin.file: + path: "{{ ca_certs_dir }}" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: "{{ ca_certs_dir_mode }}" + state: directory + + - name: Create key + community.crypto.openssl_privatekey: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + passphrase: "{{ ca_keypassphrase | default(omit, true) }}" + cipher: "{{ ca_openssl_cipher | default(omit, true) }}" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: 0600 + type: "{{ ca_key_algorithm }}" + register: key + + - name: Create CSR + community.crypto.openssl_csr: + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.csr" + privatekey_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + privatekey_passphrase: "{{ ca_keypassphrase | default(omit, true) }}" + country_name: "{{ ca_country | default(omit, true) }}" + organization_name: "{{ ca_organization | default(omit, true) }}" + common_name: "{{ ca_common_name }}" + subject_alt_name: "{{ ca_subject_alternative_name | default(omit, true) }}" + extended_key_usage: "{{ (ca_extended_key_usage | join(',')) if ca_extended_key_usage is defined else omit }}" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: 0600 + + - name: Pull CSR + ansible.builtin.fetch: + src: "{{ ca_certs_dir }}/{{ inventory_hostname }}.csr" + dest: "{{ ca_localdir }}/{{ inventory_hostname }}.csr" + flat: true + + - name: Push CSR to CA host + ansible.builtin.copy: + src: "{{ ca_localdir }}/{{ inventory_hostname }}.csr" + dest: "{{ ca_ca_dir }}/{{ inventory_hostname }}.csr" + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" + mode: 0600 + delegate_to: "{{ ca_ca_host }}" + + - name: Check if client certificate has to be renewed + ansible.builtin.import_tasks: renew.yml + vars: + crt_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.crt" + when: ca_renew | bool + + - name: (Re)sign CSR with CA key + community.crypto.x509_certificate: + path: "{{ ca_ca_dir }}/{{ inventory_hostname }}.crt" + csr_path: "{{ ca_ca_dir }}/{{ inventory_hostname }}.csr" + ownca_path: "{{ ca_ca_dir }}/ca.crt" + ownca_privatekey_path: "{{ ca_ca_dir }}/ca.key" + ownca_privatekey_passphrase: "{{ ca_ca_password }}" + ownca_not_after: "{{ ca_valid_time }}" + provider: ownca + force: "{{ not crt_info.valid_at.check_period | default(omit) or + hostvars[ca_ca_host]['ca_ca_renewed'] | default(omit) }}" + backup: true + delegate_to: "{{ ca_ca_host }}" + + - name: Fetch certificate + ansible.builtin.fetch: + src: "{{ ca_ca_dir }}/{{ inventory_hostname }}.crt" + dest: "{{ ca_localdir }}/{{ inventory_hostname }}.crt" + flat: true + delegate_to: "{{ ca_ca_host }}" + + - name: Push certificate to client + ansible.builtin.copy: + src: "{{ ca_localdir }}/{{ inventory_hostname }}.crt" + dest: "{{ ca_certs_dir }}/{{ inventory_hostname }}.crt" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: 0600 + notify: "Ansible-role-ca : on certificate change" -- name: Push CA certificate to client - ansible.builtin.copy: - src: "{{ ca_localdir }}/ca.crt" - dest: "{{ ca_client_ca_dir }}/ca.crt" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" + - name: Push CA certificate + ansible.builtin.copy: + src: "{{ ca_localdir }}/ca.crt" + dest: "{{ ca_certs_dir }}/ca.crt" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: "0600" ### server certificate ### - name: Create server certificate when: ca_server_cert | bool block: - - name: Create server CSR community.crypto.openssl_csr: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.csr" - privatekey_path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-server.csr" + privatekey_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" privatekey_passphrase: "{{ ca_keypassphrase | default(omit, true) }}" - country_name: "{{ ca_country }}" - organization_name: "{{ ca_organization }}" - common_name: "{{ inventory_hostname }}" - subject_alt_name: "{{ ca_san | regex_replace(' ', '') }}" - extended_key_usage: "{{ extended_key_usage | default(omit, true) }}" + country_name: "{{ ca_country | default(omit, true) }}" + organization_name: "{{ ca_organization | default(omit, true) }}" + common_name: "{{ ca_common_name }}" + subject_alt_name: "{{ ca_subject_alternative_name | default(omit, true) }}" + extended_key_usage: "{{ (ca_extended_key_usage | join(',')) if ca_extended_key_usage is defined else omit }}" - name: Pull server CSR ansible.builtin.fetch: - src: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.csr" + src: "{{ ca_certs_dir }}/{{ inventory_hostname }}-server.csr" dest: "{{ ca_localdir }}/{{ inventory_hostname }}-server.csr" flat: true @@ -160,15 +159,15 @@ ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-server.csr" dest: "{{ ca_ca_dir }}/{{ inventory_hostname }}-server.csr" - owner: root - group: root + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" mode: 0600 delegate_to: "{{ ca_ca_host }}" - name: Check if server certificate has to be renewed ansible.builtin.import_tasks: renew.yml vars: - crt_path: "{{ ca_ca_dir }}/{{ inventory_hostname }}-server.crt" + crt_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt" when: ca_renew | bool - name: (Re)sign server CSR with CA key @@ -180,8 +179,7 @@ ownca_privatekey_passphrase: "{{ ca_ca_password }}" ownca_not_after: "{{ ca_valid_time }}" provider: ownca - force: - "{{ not crt_info.valid_at.check_period | default(omit) or + force: "{{ not crt_info.valid_at.check_period | default(omit) or hostvars[ca_ca_host]['ca_ca_renewed'] | default(omit) }}" backup: true delegate_to: "{{ ca_ca_host }}" @@ -196,25 +194,25 @@ - name: Push server certificate to client ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-server.crt" - dest: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-server.crt" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" + dest: "{{ ca_certs_dir }}/{{ inventory_hostname }}-server.crt" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: "0600" + notify: "Ansible-role-ca : on server certificate change" - name: Handle Logstash compatible key when: ca_logstash | bool block: - - name: Check if Logstash key is present ansible.builtin.stat: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" register: key_stat - name: Move old Logstash key if common key was replaced ansible.builtin.command: > mv - {{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key - {{ ca_client_ca_dir }}/\ + {{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key + {{ ca_certs_dir }}/\ {{ inventory_hostname }}-pkcs8.key.\ {{ ansible_date_time.iso8601 }} changed_when: false @@ -225,22 +223,21 @@ - name: Create Logstash compatible key ansible.builtin.command: > openssl pkcs8 - -inform PEM - -outform PEM - -nocrypt + -in {{ ca_certs_dir }}/{{ inventory_hostname }}.key -topk8 -in {{ ca_client_ca_dir }}/{{ inventory_hostname }}.key -passin pass:{{ ca_keypassphrase | default(omit, true) }} {% if ca_ls7_workaround | bool %}-v1 {{ ca_ls7_workaround_cipher }}{% endif %} - -out {{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key + -out {{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key + -passout pass:{{ ca_keypassphrase | default(omit, true) }} args: - creates: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" + creates: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" - name: Set permissions on Logstash key ansible.builtin.file: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-pkcs8.key" - owner: root - group: root + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-pkcs8.key" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" mode: 0600 ### etcd peer certificate ### @@ -248,34 +245,29 @@ - name: Create etcd peer certificate when: ca_etcd | bool block: - - name: Add IP addresses for etcd to ca_san_etcd ansible.builtin.set_fact: ca_san_etcd: >- - {{ ca_san }}, - IP:127.0.0.1, - {%- for host in groups[ca_etcd_group] -%} - IP:{{ hostvars[host]['ansible_default_ipv4']['address'] }} - {%- if not loop.last -%} - , - {%- endif -%} + {{ ca_subject_alternative_name }}, + IP:127.0.0.1,{%- for host in groups[ca_etcd_group] -%} + IP:{{ hostvars[host]['ansible_default_ipv4']['address'] }}{%- if not loop.last -%},{%- endif -%} {%- endfor -%} when: - ca_etcd | bool - name: Create CSR community.crypto.openssl_csr: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.csr" - privatekey_path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - country_name: "{{ ca_country }}" - organization_name: "{{ ca_organization }}" - common_name: "{{ inventory_hostname }}" + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.csr" + privatekey_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + country_name: "{{ ca_country | default(omit, true) }}" + organization_name: "{{ ca_organization | default(omit, true) }}" + common_name: "{{ ca_common_name }}" subject_alt_name: "{{ ca_san_etcd | regex_replace(' ', '') }}" - extended_key_usage: "{{ extended_key_usage | default(omit, true) }}" + extended_key_usage: "{{ (ca_extended_key_usage | join(',')) if ca_extended_key_usage is defined else omit }}" - name: Pull CSR ansible.builtin.fetch: - src: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.csr" + src: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.csr" dest: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd.csr" flat: true @@ -283,15 +275,15 @@ ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd.csr" dest: "{{ ca_ca_dir }}/{{ inventory_hostname }}-etcd.csr" - owner: root - group: root + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" mode: 0600 delegate_to: "{{ ca_ca_host }}" - name: Check if etcd peer certificate has to be renewed ansible.builtin.import_tasks: renew.yml vars: - crt_path: "{{ ca_ca_dir }}/{{ inventory_hostname }}-etcd.crt" + crt_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt" when: ca_renew | bool - name: (Re)sign CSR with CA key @@ -303,8 +295,7 @@ ownca_privatekey_passphrase: "{{ ca_ca_password }}" ownca_not_after: "{{ ca_valid_time }}" provider: ownca - force: - "{{ not crt_info.valid_at.check_period | default(omit) or + force: "{{ not crt_info.valid_at.check_period | default(omit) or hostvars[ca_ca_host]['ca_ca_renewed'] | default(omit) }}" backup: true delegate_to: "{{ ca_ca_host }}" @@ -316,33 +307,33 @@ flat: true delegate_to: "{{ ca_ca_host }}" - - name: Push certificate to client + - name: Push certificate to etcd peer ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd.crt" - dest: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd.crt" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" + dest: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd.crt" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: "0600" + notify: "Ansible-role-ca : on etcd certificate change" ### etcd server certificate ### - name: Create etcd server certificate when: ca_etcd | bool block: - - name: Create CSR community.crypto.openssl_csr: - path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.csr" - privatekey_path: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}.key" - country_name: "{{ ca_country }}" - organization_name: "{{ ca_organization }}" - common_name: "{{ inventory_hostname }}" + path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.csr" + privatekey_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}.key" + country_name: "{{ ca_country | default(omit, true) }}" + organization_name: "{{ ca_organization | default(omit, true) }}" + common_name: "{{ ca_common_name }}" subject_alt_name: "{{ ca_san_etcd | regex_replace(' ', '') }}" - extended_key_usage: "{{ extended_key_usage | default(omit, true) }}" + extended_key_usage: "{{ (ca_extended_key_usage | join(',')) if ca_extended_key_usage is defined else omit }}" - name: Pull CSR ansible.builtin.fetch: - src: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.csr" + src: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.csr" dest: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd-server.csr" flat: true @@ -350,15 +341,15 @@ ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd-server.csr" dest: "{{ ca_ca_dir }}/{{ inventory_hostname }}-etcd-server.csr" - owner: root - group: root + owner: "{{ ca_ca_dir_owner }}" + group: "{{ ca_ca_dir_group }}" mode: 0600 delegate_to: "{{ ca_ca_host }}" - name: Check if etcd server certificate has to be renewed ansible.builtin.import_tasks: renew.yml vars: - crt_path: "{{ ca_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt" + crt_path: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt" when: ca_renew | bool - name: (Re)sign CSR with CA key @@ -370,8 +361,7 @@ ownca_privatekey_passphrase: "{{ ca_ca_password }}" ownca_not_after: "{{ ca_valid_time }}" provider: ownca - force: - "{{ not crt_info.valid_at.check_period | default(omit) or + force: "{{ not crt_info.valid_at.check_period | default(omit) or hostvars[ca_ca_host]['ca_ca_renewed'] | default(omit) }}" backup: true delegate_to: "{{ ca_ca_host }}" @@ -386,7 +376,8 @@ - name: Push certificate to client ansible.builtin.copy: src: "{{ ca_localdir }}/{{ inventory_hostname }}-etcd-server.crt" - dest: "{{ ca_client_ca_dir }}/{{ inventory_hostname }}-etcd-server.crt" - owner: "{{ ca_client_ca_dir_owner }}" - group: "{{ ca_client_ca_dir_group }}" - mode: "{{ ca_client_ca_dir_mode }}" + dest: "{{ ca_certs_dir }}/{{ inventory_hostname }}-etcd-server.crt" + owner: "{{ ca_certs_dir_owner }}" + group: "{{ ca_certs_dir_group }}" + mode: "0600" + notify: "Ansible-role-ca : on etcd server certificate change" diff --git a/tasks/openssl_detect.yml b/tasks/openssl_detect.yml deleted file mode 100644 index a13794a..0000000 --- a/tasks/openssl_detect.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- - -### -# The following code checks for the OpenSSL version being used -# When signing a CA key, OpenSSL 3 needs paramaters older versions don't -# understand. -# The `version` filter is unkown to Jinja2 so we need a fact that helps -# with changing templates accordingly - -- name: Get OpenSSL version - ansible.builtin.command: openssl version - register: _ca_openssl_version_raw - changed_when: false - -- name: Extract OpenSSL version string - ansible.builtin.set_fact: - _ca_openssl_version: "{{ _ca_openssl_version_raw.stdout.split(' ')[1] }}" - -- name: Set OpenSSL extension parameters (OpenSSL ≥ 3.0) - ansible.builtin.set_fact: - _ca_openssl_ca_ext_opts: "-extensions v3_ca" - when: _ca_openssl_version is version('3.0', '>=') - -- name: Set OpenSSL extension parameters (OpenSSL < 3.0) - ansible.builtin.set_fact: - _ca_openssl_ca_ext_opts: "" - when: _ca_openssl_version is version('3.0', '<') - -- name: Set Variable for Jinja2 templates - ansible.builtin.set_fact: - _ca_ca_openssl_version_3: true - when: _ca_openssl_version is version('3.0', '>=') diff --git a/templates/ca.conf.j2 b/templates/ca.conf.j2 index 97d04f8..015f590 100644 --- a/templates/ca.conf.j2 +++ b/templates/ca.conf.j2 @@ -1,31 +1,39 @@ [req] distinguished_name = req_distinguished_name -req_extensions = v3_req +req_extensions = v3_req_ca +x509_extensions = v3_req_ca prompt = no [req_distinguished_name] +{% if ca_country is defined %} countryName = {{ ca_country }} +{% endif %} +{% if ca_state is defined %} stateOrProvinceName = {{ ca_state }} +{% endif %} +{% if ca_locality is defined %} localityName = {{ ca_locality }} +{% endif %} +{% if ca_postalcode is defined %} postalCode = {{ ca_postalcode }} +{% endif %} +{% if ca_organization is defined %} organizationName = {{ ca_organization }} +{% endif %} +{% if ca_organizationunit is defined %} organizationalUnitName = {{ ca_organizationalunit }} +{% endif %} commonName = {{ ca_common_name }} +{% if ca_email is defined %} emailAddress = {{ ca_email }} - -{% if _ca_ca_openssl_version_3 | bool %} -[v3_ca] -basicConstraints = critical,CA:true,pathlen:0 -keyUsage = critical,keyCertSign,cRLSign -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer - {% endif %} -[v3_req] -keyUsage = keyEncipherment, dataEncipherment -extendedKeyUsage = serverAuth -subjectAltName = @alt_names -[alt_names] -DNS.1 = {{ ca_altname_1 }} -DNS.2 = {{ ca_altname_2 }} +[v3_req_ca] +basicConstraints = critical, CA:true, pathlen:1 +# digitalSignature is needed if CA acts as a OCSP responder +keyUsage = keyCertSign, cRLSign, digitalSignature +# To facilitate certification path construction, this extension MUST +# appear in all conforming CA certificates, that is, all certificates +# including the basic constraints extension +# https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2 +subjectKeyIdentifier = hash