diff --git a/main.yml b/main.yml index 16415ec3..42e15965 100644 --- a/main.yml +++ b/main.yml @@ -24,8 +24,7 @@ - name: Set required ansible version as a fact set_fact: - required_ansible_version: "{{ item | regex_replace('^ansible[\\s+]?(?P[=,>,<]+)[\\s+]?(?P\\d.\\d+(.\\d+)?)$', '{\"op\": \"\\g\",\"ver\"\ - : \"\\g\" }') }}" + required_ansible_version: "{{ item | regex_replace('^ansible\\s*(?P[~>=<]+)\\s*(?P\\d+\\.\\d+(?:\\.\\d+)?).*$', '{\"op\": \"\\g\", \"ver\": \"\\g\"}') }}" when: '"ansible" in item' with_items: "{{ lookup('file', 'requirements.txt').splitlines() }}" diff --git a/requirements.txt b/requirements.txt index a7428528..abfe8dc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -ansible==9.13.0 +ansible==11.8.0 jinja2~=3.1.6 netaddr==1.3.0 diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index 0894517c..76d6fbcb 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -13,244 +13,94 @@ dest: "{{ ipsec_pki_path }}/{{ item }}" state: directory recurse: true - mode: "0700" with_items: - - ecparams - certs - - crl - - newcerts - private - - public - - reqs - name: Ensure the config directories exist file: dest: "{{ ipsec_config_path }}/{{ item }}" state: directory recurse: true - mode: "0700" with_items: - apple - manual - - name: Ensure the files exist - file: - dest: "{{ ipsec_pki_path }}/{{ item }}" - state: touch + - name: Create private key with password protection + community.crypto.openssl_privatekey: + path: "{{ ipsec_pki_path }}/private/cakey.pem" + passphrase: "{{ CA_password }}" + type: ECC + curve: secp384r1 + mode: "0600" + + - name: Create certificate signing request (CSR) for CA certificate + community.crypto.openssl_csr_pipe: + privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" + privatekey_passphrase: "{{ CA_password }}" + common_name: "{{ IP_subject_alt_name }}" + use_common_name_for_san: true + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: true + key_usage: + - keyCertSign + key_usage_critical: true + register: ca_csr + + - name: Create self-signed CA certificate from CSR + community.crypto.x509_certificate: + path: "{{ ipsec_pki_path }}/cacert.pem" + csr_content: "{{ ca_csr.csr }}" + privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" + privatekey_passphrase: "{{ CA_password }}" + provider: selfsigned + + - name: Create private keys + community.crypto.openssl_privatekey: + path: "{{ ipsec_pki_path }}/private/{{ item }}.key" + type: ECC + curve: secp384r1 + mode: "0600" with_items: - - .rnd - - private/.rnd - - index.txt - - index.txt.attr - - serial - - - name: Generate the openssl server configs - template: - src: openssl.cnf.j2 - dest: "{{ ipsec_pki_path }}/openssl.cnf" - - - name: Build the CA pair - shell: > - umask 077; - {{ openssl_bin }} ecparam -name secp384r1 -out ecparams/secp384r1.pem && - {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) - -keyout private/cakey.pem - -out cacert.pem -x509 -days 3650 - -batch - -passout pass:"{{ CA_password }}" && - touch {{ IP_subject_alt_name }}_ca_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: "{{ IP_subject_alt_name }}_ca_generated" - executable: bash - - - name: Copy the CA certificate - copy: - src: "{{ ipsec_pki_path }}/cacert.pem" - dest: "{{ ipsec_config_path }}/manual/cacert.pem" - - - name: Generate the serial number - shell: echo 01 > serial && touch serial_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: serial_generated - - - name: Build the server pair - shell: > - umask 077; - {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) - -keyout private/{{ IP_subject_alt_name }}.key - -out reqs/{{ IP_subject_alt_name }}.req -nodes - -passin pass:"{{ CA_password }}" - -subj "/CN={{ IP_subject_alt_name }}" -batch && - {{ openssl_bin }} ca -utf8 - -in reqs/{{ IP_subject_alt_name }}.req - -out certs/{{ IP_subject_alt_name }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) - -days 3650 -batch - -passin pass:"{{ CA_password }}" - -subj "/CN={{ IP_subject_alt_name }}" && - touch certs/{{ IP_subject_alt_name }}_crt_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: certs/{{ IP_subject_alt_name }}_crt_generated - executable: bash - - - name: Generate client private keys and certificate requests (parallel) - shell: > - umask 077; - {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) - -keyout private/{{ item }}.key - -out reqs/{{ item }}.req -nodes - -passin pass:"{{ CA_password }}" - -subj "/CN={{ item }}" -batch && - touch reqs/{{ item }}_req_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: reqs/{{ item }}_req_generated - executable: bash - with_items: "{{ users }}" - async: 60 - poll: 0 + - "{{ users }}" + - "{{ IP_subject_alt_name }}" register: client_key_jobs - - name: Wait for client key generation to complete - async_status: - jid: "{{ item.ansible_job_id }}" - with_items: "{{ client_key_jobs.results }}" - register: client_key_results - until: client_key_results.finished - retries: 30 - delay: 2 - failed_when: > - client_key_results.failed or - (client_key_results.finished and client_key_results.rc != 0) - - - name: Display crypto generation failure details (if any) - debug: - msg: | - StrongSwan client key generation failed for user: {{ item.item }} - Error details: {{ item.stderr | default('No stderr available') }} - Return code: {{ item.rc | default('Unknown') }} - Command: {{ item.cmd | default('Unknown command') }} - - Common causes: - - Insufficient entropy (try installing haveged or rng-tools) - - Disk space issues in /opt/algo - - Permission problems with OpenSSL CA database - - OpenSSL configuration errors - - Troubleshooting: - - Check disk space: df -h /opt/algo - - Check entropy: cat /proc/sys/kernel/random/entropy_avail - - Verify OpenSSL config: openssl version -a - when: item.rc is defined and item.rc != 0 - with_items: "{{ client_key_results.results | default([]) }}" - failed_when: false - - - name: Sign client certificates (sequential - CA database locking required) - shell: > - umask 077; - {{ openssl_bin }} ca -utf8 - -in reqs/{{ item }}.req - -out certs/{{ item }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) - -days 3650 -batch - -passin pass:"{{ CA_password }}" - -subj "/CN={{ item }}" && - touch certs/{{ item }}_crt_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: certs/{{ item }}_crt_generated - executable: bash - with_items: "{{ users }}" - - - name: Build the tests pair - shell: > - umask 077; - {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:google-algo-test-pair.com")) - -keyout private/google-algo-test-pair.com.key - -out reqs/google-algo-test-pair.com.req -nodes - -passin pass:"{{ CA_password }}" - -subj "/CN=google-algo-test-pair.com" -batch && - {{ openssl_bin }} ca -utf8 - -in reqs/google-algo-test-pair.com.req - -out certs/google-algo-test-pair.com.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:google-algo-test-pair.com")) - -days 3650 -batch - -passin pass:"{{ CA_password }}" - -subj "/CN=google-algo-test-pair.com" && - touch certs/google-algo-test-pair.com_crt_generated - args: - chdir: "{{ ipsec_pki_path }}" - creates: certs/google-algo-test-pair.com_crt_generated - executable: bash - when: tests|default(false)|bool - - - name: Build openssh public keys - openssl_publickey: - path: "{{ ipsec_pki_path }}/public/{{ item }}.pub" + - name: Create CSRs + community.crypto.openssl_csr_pipe: privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" - format: OpenSSH + subject_alt_name: "{{ subjectAltName | split(',') if item == IP_subject_alt_name else [subjectAltName_USER] }}" + common_name: "{{ item }}" + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + register: client_csr_jobs + + - name: Sign clients certificates with our CA + community.crypto.x509_certificate: + csr_content: "{{ item.csr }}" + path: "{{ ipsec_pki_path }}/certs/{{ item.item }}.crt" + provider: ownca + ownca_path: "{{ ipsec_pki_path }}/cacert.pem" + ownca_privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" + ownca_privatekey_passphrase: "{{ CA_password }}" + ownca_not_after: +3650d + ownca_not_before: "-1d" + with_items: "{{ client_csr_jobs.results }}" + register: client_sign_results + + - name: Generate p12 files + community.crypto.openssl_pkcs12: + path: "{{ ipsec_pki_path }}/private/{{ item }}.p12" + friendly_name: "{{ item }}" + privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" + certificate_path: "{{ ipsec_pki_path }}/certs/{{ item }}.crt" + passphrase: "{{ p12_export_password }}" + mode: "0600" + encryption_level: "compatibility2022" with_items: "{{ users }}" - - name: Get OpenSSL version - shell: | - set -o pipefail - {{ openssl_bin }} version | - cut -f 2 -d ' ' - args: - executable: bash - register: ssl_version - run_once: true - - - name: Set OpenSSL version fact - set_fact: - openssl_version: "{{ ssl_version.stdout }}" - - - name: Build the client's p12 - shell: > - umask 077; - {{ openssl_bin }} pkcs12 - {{ (openssl_version is version('3', '>=')) | ternary('-legacy', '') }} - -in certs/{{ item }}.crt - -inkey private/{{ item }}.key - -export - -name {{ item }} - -out private/{{ item }}.p12 - -passout pass:"{{ p12_export_password }}" - args: - chdir: "{{ ipsec_pki_path }}" - executable: bash - with_items: "{{ users }}" - register: p12 - - - name: Build the client's p12 with the CA cert included - shell: > - umask 077; - {{ openssl_bin }} pkcs12 - {{ (openssl_version is version('3', '>=')) | ternary('-legacy', '') }} - -in certs/{{ item }}.crt - -inkey private/{{ item }}.key - -export - -name {{ item }} - -out private/{{ item }}_ca.p12 - -certfile cacert.pem - -passout pass:"{{ p12_export_password }}" - args: - chdir: "{{ ipsec_pki_path }}" - executable: bash - with_items: "{{ users }}" - register: p12 - - name: Copy the p12 certificates copy: src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" @@ -258,45 +108,40 @@ with_items: - "{{ users }}" - - name: Get active users - shell: | - set -o pipefail - grep ^V index.txt | - grep -v "{{ IP_subject_alt_name }}" | - awk '{print $5}' | - sed 's/\/CN=//g' - args: - executable: /bin/bash - chdir: "{{ ipsec_pki_path }}" - register: valid_certs + - name: Add all users to the file + ansible.builtin.lineinfile: + path: "{{ ipsec_pki_path }}/all-users" + line: "{{ item }}" + create: true + with_items: "{{ users }}" + register: users_file - - name: Revoke non-existing users - shell: > - {{ openssl_bin }} ca -gencrl - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) - -passin pass:"{{ CA_password }}" - -revoke certs/{{ item }}.crt - -out crl/{{ item }}.crt - register: gencrl - args: - chdir: "{{ ipsec_pki_path }}" - creates: crl/{{ item }}.crt - executable: bash - when: item.split('@')[0] not in users - with_items: "{{ valid_certs.stdout_lines }}" + - name: Set all users as a fact + set_fact: + all_users: "{{ lookup('file', ipsec_pki_path + '/all-users').splitlines() }}" - - name: Generate new CRL file - shell: > - {{ openssl_bin }} ca -gencrl - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }}")) - -passin pass:"{{ CA_password }}" - -out crl/algo.root.pem - when: - - gencrl is defined - - gencrl.changed - args: - chdir: "{{ ipsec_pki_path }}" - executable: bash + - name: Set revoked certificates as a fact + set_fact: + revoked_certificates: >- + [{% set now = '%Y%m%d%H%M%SZ' | strftime(ansible_date_time.epoch | int) -%} + {% for user in all_users | difference(users) -%} + { + "path": "{{ ipsec_pki_path }}/certs/{{ user }}.crt", + "revocation_date": "{{ now }}" + }{{ "," if not loop.last else "" }} + {% endfor %}] + + - name: Generate a CRL + community.crypto.x509_crl: + path: "{{ ipsec_pki_path }}/crl.pem" + privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" + privatekey_passphrase: "{{ CA_password }}" + last_update: "{{ '%Y%m%d%H%M%SZ' | strftime(ansible_date_time.epoch | int) }}" + next_update: "{{ '%Y%m%d%H%M%SZ' | strftime((ansible_date_time.epoch | int) + (10 * 365 * 24 * 60 * 60)) }}" + crl_mode: generate + issuer: + CN: "{{ IP_subject_alt_name }}" + revoked_certificates: "{{ revoked_certificates }}" delegate_to: localhost become: false vars: @@ -304,10 +149,7 @@ - name: Copy the CRL to the vpn server copy: - src: "{{ ipsec_pki_path }}/crl/algo.root.pem" + src: "{{ ipsec_pki_path }}/crl.pem" dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem" - when: - - gencrl is defined - - gencrl.changed notify: - rereadcrls diff --git a/roles/strongswan/templates/openssl.cnf.j2 b/roles/strongswan/templates/openssl.cnf.j2 deleted file mode 100644 index bd199b3a..00000000 --- a/roles/strongswan/templates/openssl.cnf.j2 +++ /dev/null @@ -1,139 +0,0 @@ -# For use with Easy-RSA 3.0 and OpenSSL 1.0.* - -RANDFILE = .rnd - -#################################################################### -[ ca ] -default_ca = CA_default # The default ca section - -#################################################################### -[ CA_default ] - -dir = . # Where everything is kept -certs = $dir # Where the issued certs are kept -crl_dir = $dir # Where the issued crl are kept -database = $dir/index.txt # database index file. -new_certs_dir = $dir/certs # default place for new certs. - -certificate = $dir/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number -crl = $dir/crl.pem # The current CRL -private_key = $dir/private/cakey.pem # The private key -RANDFILE = $dir/private/.rand # private random number file - -x509_extensions = basic_exts # The extensions to add to the cert - -# This allows a V2 CRL. Ancient browsers don't like it, but anything Easy-RSA -# is designed for will. In return, we get the Issuer attached to CRLs. -crl_extensions = crl_ext - -default_days = 3650 # how long to certify for -default_crl_days= 3650 # how long before next CRL -default_md = sha256 # use public key default MD -preserve = no # keep passed DN ordering - -# A few difference way of specifying how similar the request should look -# For type CA, the listed attributes must be the same, and the optional -# and supplied fields are just that :-) -policy = policy_anything - -# For the 'anything' policy, which defines allowed DN fields -[ policy_anything ] -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -name = optional -emailAddress = optional - -#################################################################### -# Easy-RSA request handling -# We key off $DN_MODE to determine how to format the DN -[ req ] -default_bits = 2048 -default_keyfile = privkey.pem -default_md = sha256 -distinguished_name = cn_only -x509_extensions = easyrsa_ca # The extensions to add to the self signed cert - -# A placeholder to handle the $EXTRA_EXTS feature: -#%EXTRA_EXTS% # Do NOT remove or change this line as $EXTRA_EXTS support requires it - -#################################################################### -# Easy-RSA DN (Subject) handling - -# Easy-RSA DN for cn_only support: -[ cn_only ] -commonName = Common Name (eg: your user, host, or server name) -commonName_max = 64 -commonName_default = {{ IP_subject_alt_name }} - -# Easy-RSA DN for org support: -[ org ] -countryName = Country Name (2 letter code) -countryName_default = US -countryName_min = 2 -countryName_max = 2 - -stateOrProvinceName = State or Province Name (full name) -stateOrProvinceName_default = California - -localityName = Locality Name (eg, city) -localityName_default = San Francisco - -0.organizationName = Organization Name (eg, company) -0.organizationName_default = Copyleft Certificate Co - -organizationalUnitName = Organizational Unit Name (eg, section) -organizationalUnitName_default = My Organizational Unit - -commonName = Common Name (eg: your user, host, or server name) -commonName_max = 64 -commonName_default = {{ IP_subject_alt_name }} - -emailAddress = Email Address -emailAddress_default = me@example.net -emailAddress_max = 64 - -#################################################################### -# Easy-RSA cert extension handling - -# This section is effectively unused as the main script sets extensions -# dynamically. This core section is left to support the odd usecase where -# a user calls openssl directly. -[ basic_exts ] -basicConstraints = CA:FALSE -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer:always - -extendedKeyUsage = serverAuth,clientAuth,1.3.6.1.5.5.7.3.17 -keyUsage = digitalSignature, keyEncipherment - -# The Easy-RSA CA extensions -[ easyrsa_ca ] - -# PKIX recommendations: - -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid:always,issuer:always - -basicConstraints = critical,CA:true,pathlen:0 -nameConstraints = {{ nameConstraints }} - - -# Limit key usage to CA tasks. If you really want to use the generated pair as -# a self-signed cert, comment this out. -keyUsage = cRLSign, keyCertSign - -# nsCertType omitted by default. Let's try to let the deprecated stuff die. -# nsCertType = sslCA - -# CRL extensions. -[ crl_ext ] - -# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. - -# issuerAltName=issuer:copy -authorityKeyIdentifier=keyid:always,issuer:always