Refactor StrongSwan PKI automation with Ansible crypto modules

- Replace shell-based OpenSSL commands with community.crypto modules
- Remove custom OpenSSL config template and manual file management
- Upgrade Ansible to 11.8.0 in requirements.txt
- Improve idempotency, maintainability, and security of certificate and CRL handling
This commit is contained in:
Jack Ivanov 2025-08-04 15:44:38 -06:00
parent 9e0de205fb
commit 1396827e8a
4 changed files with 104 additions and 402 deletions

View file

@ -24,8 +24,7 @@
- name: Set required ansible version as a fact - name: Set required ansible version as a fact
set_fact: set_fact:
required_ansible_version: "{{ item | regex_replace('^ansible[\\s+]?(?P<op>[=,>,<]+)[\\s+]?(?P<ver>\\d.\\d+(.\\d+)?)$', '{\"op\": \"\\g<op>\",\"ver\"\ required_ansible_version: "{{ item | regex_replace('^ansible\\s*(?P<op>[~>=<]+)\\s*(?P<ver>\\d+\\.\\d+(?:\\.\\d+)?).*$', '{\"op\": \"\\g<op>\", \"ver\": \"\\g<ver>\"}') }}"
: \"\\g<ver>\" }') }}"
when: '"ansible" in item' when: '"ansible" in item'
with_items: "{{ lookup('file', 'requirements.txt').splitlines() }}" with_items: "{{ lookup('file', 'requirements.txt').splitlines() }}"

View file

@ -1,3 +1,3 @@
ansible==9.13.0 ansible==11.8.0
jinja2~=3.1.6 jinja2~=3.1.6
netaddr==1.3.0 netaddr==1.3.0

View file

@ -13,244 +13,94 @@
dest: "{{ ipsec_pki_path }}/{{ item }}" dest: "{{ ipsec_pki_path }}/{{ item }}"
state: directory state: directory
recurse: true recurse: true
mode: "0700"
with_items: with_items:
- ecparams
- certs - certs
- crl
- newcerts
- private - private
- public
- reqs
- name: Ensure the config directories exist - name: Ensure the config directories exist
file: file:
dest: "{{ ipsec_config_path }}/{{ item }}" dest: "{{ ipsec_config_path }}/{{ item }}"
state: directory state: directory
recurse: true recurse: true
mode: "0700"
with_items: with_items:
- apple - apple
- manual - manual
- name: Ensure the files exist - name: Create private key with password protection
file: community.crypto.openssl_privatekey:
dest: "{{ ipsec_pki_path }}/{{ item }}" path: "{{ ipsec_pki_path }}/private/cakey.pem"
state: touch 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: with_items:
- .rnd - "{{ users }}"
- private/.rnd - "{{ IP_subject_alt_name }}"
- 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
register: client_key_jobs register: client_key_jobs
- name: Wait for client key generation to complete - name: Create CSRs
async_status: community.crypto.openssl_csr_pipe:
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"
privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" 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 }}" 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 - name: Copy the p12 certificates
copy: copy:
src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" src: "{{ ipsec_pki_path }}/private/{{ item }}.p12"
@ -258,45 +108,40 @@
with_items: with_items:
- "{{ users }}" - "{{ users }}"
- name: Get active users - name: Add all users to the file
shell: | ansible.builtin.lineinfile:
set -o pipefail path: "{{ ipsec_pki_path }}/all-users"
grep ^V index.txt | line: "{{ item }}"
grep -v "{{ IP_subject_alt_name }}" | create: true
awk '{print $5}' | with_items: "{{ users }}"
sed 's/\/CN=//g' register: users_file
args:
executable: /bin/bash
chdir: "{{ ipsec_pki_path }}"
register: valid_certs
- name: Revoke non-existing users - name: Set all users as a fact
shell: > set_fact:
{{ openssl_bin }} ca -gencrl all_users: "{{ lookup('file', ipsec_pki_path + '/all-users').splitlines() }}"
-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: Generate new CRL file - name: Set revoked certificates as a fact
shell: > set_fact:
{{ openssl_bin }} ca -gencrl revoked_certificates: >-
-config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }}")) [{% set now = '%Y%m%d%H%M%SZ' | strftime(ansible_date_time.epoch | int) -%}
-passin pass:"{{ CA_password }}" {% for user in all_users | difference(users) -%}
-out crl/algo.root.pem {
when: "path": "{{ ipsec_pki_path }}/certs/{{ user }}.crt",
- gencrl is defined "revocation_date": "{{ now }}"
- gencrl.changed }{{ "," if not loop.last else "" }}
args: {% endfor %}]
chdir: "{{ ipsec_pki_path }}"
executable: bash - 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 delegate_to: localhost
become: false become: false
vars: vars:
@ -304,10 +149,7 @@
- name: Copy the CRL to the vpn server - name: Copy the CRL to the vpn server
copy: 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" dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem"
when:
- gencrl is defined
- gencrl.changed
notify: notify:
- rereadcrls - rereadcrls

View file

@ -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