summaryrefslogtreecommitdiff
path: root/roles/x509/static-ca
diff options
context:
space:
mode:
authorChristian Pointner <equinox@spreadspace.org>2023-12-20 00:12:57 +0100
committerChristian Pointner <equinox@spreadspace.org>2023-12-20 00:12:57 +0100
commite004236b4cfa9735cc898ea372dcb99c199dd4b4 (patch)
tree497e1e3dcbb7a223f2e5aaa9b5800319c03dd5d1 /roles/x509/static-ca
parentadd mosquitto role (WIP) (diff)
rename: x509/ownca to x509/static-ca
Diffstat (limited to 'roles/x509/static-ca')
-rw-r--r--roles/x509/static-ca/base/tasks/main.yml5
-rw-r--r--roles/x509/static-ca/cert/finalize/tasks/main.yml2
-rw-r--r--roles/x509/static-ca/cert/meta/main.yml4
-rw-r--r--roles/x509/static-ca/cert/prepare/defaults/main.yml56
-rw-r--r--roles/x509/static-ca/cert/prepare/handlers/main.yml16
-rw-r--r--roles/x509/static-ca/cert/prepare/tasks/main.yml105
-rw-r--r--roles/x509/static-ca/cert/prepare/templates/updated.sh.j215
-rwxr-xr-xroles/x509/static-ca/contrib/gen-ca.py72
8 files changed, 275 insertions, 0 deletions
diff --git a/roles/x509/static-ca/base/tasks/main.yml b/roles/x509/static-ca/base/tasks/main.yml
new file mode 100644
index 00000000..e91eda4a
--- /dev/null
+++ b/roles/x509/static-ca/base/tasks/main.yml
@@ -0,0 +1,5 @@
+---
+- name: install needed packages
+ apt:
+ name: "{{ python_basename }}-cryptography"
+ state: present
diff --git a/roles/x509/static-ca/cert/finalize/tasks/main.yml b/roles/x509/static-ca/cert/finalize/tasks/main.yml
new file mode 100644
index 00000000..c5b6cafe
--- /dev/null
+++ b/roles/x509/static-ca/cert/finalize/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+# nothing to do here
diff --git a/roles/x509/static-ca/cert/meta/main.yml b/roles/x509/static-ca/cert/meta/main.yml
new file mode 100644
index 00000000..bfaf1153
--- /dev/null
+++ b/roles/x509/static-ca/cert/meta/main.yml
@@ -0,0 +1,4 @@
+---
+dependencies:
+ - role: x509/static-ca/cert/prepare
+ - role: x509/static-ca/cert/finalize
diff --git a/roles/x509/static-ca/cert/prepare/defaults/main.yml b/roles/x509/static-ca/cert/prepare/defaults/main.yml
new file mode 100644
index 00000000..5287cc93
--- /dev/null
+++ b/roles/x509/static-ca/cert/prepare/defaults/main.yml
@@ -0,0 +1,56 @@
+---
+static_ca_cert_hostnames: "{{ x509_certificate_hostnames }}"
+static_ca_cert_name: "{{ x509_certificate_name | default(static_ca_cert_hostnames[0]) }}"
+
+static_ca_cert_base_dir: "/etc/ssl"
+
+static_ca_cert_default_renew_margin: "+30d"
+static_ca_cert_config: "{{ x509_certificate_config }}"
+# static_ca_cert_config:
+# path: "{{ static_ca_cert_base_dir }}/{{ static_ca_cert_name }}"
+# mode: "0750"
+# owner: root
+# group: www-data
+# ca:
+# key_content: |
+# -----BEGIN RSA PRIVATE KEY-----
+# ...
+# -----END RSA PRIVATE KEY-----
+# cert_content: |
+# -----BEGIN CERTIFICATE-----
+# ...
+# -----END CERTIFICATE-----
+# key:
+# mode: "0640"
+# owner: root
+# group: www-data
+# type: RSA
+# size: 4096
+# cert:
+# mode: "0644"
+# owner: root
+# group: www-data
+# common_name: foo
+# san_extra:
+# - "IP:192.0.2.1"
+# country_name: "AT"
+# locality_name: "Graz"
+# organization_name: "spreadspace"
+# organizational_unit_name: "ansible"
+# state_or_province_name: "Styria"
+# basic_constraints:
+# - "CA:TRUE"
+# - "pathLenConstraint:0"
+# basic_constraints_critical: no
+# key_usage:
+# - digitalSignature
+# - keyAgreement
+# key_usage_critical: yes
+# extended_key_usage:
+# - serverAuth
+# extended_key_usage_critical: yes
+# create_subject_key_identifier: yes
+# digest: SHA256
+# not_before: +0h
+# not_after: +520w
+# renew_margin: +42d
diff --git a/roles/x509/static-ca/cert/prepare/handlers/main.yml b/roles/x509/static-ca/cert/prepare/handlers/main.yml
new file mode 100644
index 00000000..589d6dde
--- /dev/null
+++ b/roles/x509/static-ca/cert/prepare/handlers/main.yml
@@ -0,0 +1,16 @@
+---
+- name: reload services for x509 certificates
+ loop: "{{ x509_certificate_reload_services | default([]) }}"
+ loop_control:
+ loop_var: x509_certificate_reload_service
+ service:
+ name: "{{ x509_certificate_reload_service }}"
+ state: reloaded
+
+- name: restart services for x509 certificates
+ loop: "{{ x509_certificate_restart_services | default([]) }}"
+ loop_control:
+ loop_var: x509_certificate_restart_service
+ service:
+ name: "{{ x509_certificate_restart_service }}"
+ state: restarted
diff --git a/roles/x509/static-ca/cert/prepare/tasks/main.yml b/roles/x509/static-ca/cert/prepare/tasks/main.yml
new file mode 100644
index 00000000..538bb58d
--- /dev/null
+++ b/roles/x509/static-ca/cert/prepare/tasks/main.yml
@@ -0,0 +1,105 @@
+---
+- name: compute path to static-ca certificate directory
+ set_fact:
+ static_ca_cert_path: "{{ static_ca_cert_config.path | default([static_ca_cert_base_dir, static_ca_cert_name] | path_join) }}"
+
+- name: create directory for static-ca certificate
+ file:
+ path: "{{ static_ca_cert_path }}"
+ state: directory
+ mode: "{{ static_ca_cert_config.mode | default('0700') }}"
+ owner: "{{ static_ca_cert_config.owner | default(omit) }}"
+ group: "{{ static_ca_cert_config.group | default(omit) }}"
+ notify:
+ - reload services for x509 certificates
+ - restart services for x509 certificates
+
+- name: generate key for static-ca certificate
+ openssl_privatekey:
+ path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-key.pem"
+ mode: "{{ static_ca_cert_config.key.mode | default('0600') }}"
+ owner: "{{ static_ca_cert_config.key.owner | default(omit) }}"
+ group: "{{ static_ca_cert_config.key.group | default(omit) }}"
+ type: "{{ static_ca_cert_config.key.type | default(omit) }}"
+ size: "{{ static_ca_cert_config.key.size | default(omit) }}"
+ notify:
+ - reload services for x509 certificates
+ - restart services for x509 certificates
+ register: _static_ca_key_
+
+- name: generate csr for static-ca certificate
+ community.crypto.openssl_csr:
+ path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-csr.pem"
+ mode: "{{ static_ca_cert_config.cert.mode | default('0644') }}"
+ owner: "{{ static_ca_cert_config.cert.owner | default(omit) }}"
+ group: "{{ static_ca_cert_config.cert.group | default(omit) }}"
+ privatekey_path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-key.pem"
+ create_subject_key_identifier: "{{ static_ca_cert_config.cert.create_subject_key_identifier | default(omit) }}"
+ digest: "{{ static_ca_cert_config.cert.digest | default(omit) }}"
+ common_name: "{{ static_ca_cert_config.cert.common_name | default(static_ca_cert_name) }}"
+ subject_alt_name: "{{ ['DNS:'] | product(static_ca_cert_hostnames) | map('join') | union(static_ca_cert_config.cert.san_extra | default([])) | list }}"
+ subject_alt_name_critical: yes
+ use_common_name_for_san: no
+ country_name: "{{ static_ca_cert_config.cert.country_name | default(omit) }}"
+ locality_name: "{{ static_ca_cert_config.cert.locality_name | default(omit) }}"
+ organization_name: "{{ static_ca_cert_config.cert.organization_name | default(omit) }}"
+ organizational_unit_name: "{{ static_ca_cert_config.cert.organizational_unit_name | default(omit) }}"
+ state_or_province_name: "{{ static_ca_cert_config.cert.state_or_province_name | default(omit) }}"
+ basic_constraints: "{{ static_ca_cert_config.cert.basic_constraints | default(omit) }}"
+ basic_constraints_critical: "{{ static_ca_cert_config.cert.basic_constraints_critical | default(omit) }}"
+ key_usage: "{{ static_ca_cert_config.cert.key_usage | default(omit) }}"
+ key_usage_critical: "{{ static_ca_cert_config.cert.key_usage_critical | default(omit) }}"
+ extended_key_usage: "{{ static_ca_cert_config.cert.extended_key_usage | default(omit) }}"
+ extended_key_usage_critical: "{{ static_ca_cert_config.cert.extended_key_usage_critical | default(omit) }}"
+
+- name: check if static-ca certificate already exists
+ stat:
+ path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-crt.pem"
+ register: _static_ca_cert_file_
+
+- name: check validity of existing static-ca certificate
+ when: _static_ca_cert_file_.stat.exists
+ openssl_certificate_info:
+ path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-crt.pem"
+ valid_at:
+ renew_margin: "{{ static_ca_cert_config.cert.renew_margin | default(static_ca_cert_default_renew_margin) }}"
+ register: _static_ca_cert_info_
+
+- name: generate static-ca certificate
+ community.crypto.x509_certificate:
+ path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-crt.pem"
+ mode: "{{ static_ca_cert_config.cert.mode | default('0644') }}"
+ owner: "{{ static_ca_cert_config.cert.owner | default(omit) }}"
+ group: "{{ static_ca_cert_config.cert.group | default(omit) }}"
+ csr_path: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-csr.pem"
+ provider: ownca
+ ownca_content: "{{ static_ca_cert_config.ca.cert_content }}"
+ ownca_privatekey_content: "{{ static_ca_cert_config.ca.key_content }}"
+ ownca_digest: "{{ static_ca_cert_config.cert.digest | default(omit) }}"
+ ownca_not_before: "{{ static_ca_cert_config.cert.not_before | default(omit) }}"
+ ownca_not_after: "{{ static_ca_cert_config.cert.not_after | default(omit) }}"
+ force: "{{ _static_ca_cert_file_.stat.exists and (not _static_ca_cert_info_.valid_at.renew_margin) }}"
+ notify:
+ - reload services for x509 certificates
+ - restart services for x509 certificates
+ register: _static_ca_cert_
+
+- name: export paths to certificate files
+ set_fact:
+ x509_certificate_path_key: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-key.pem"
+ x509_certificate_path_cert: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-crt.pem"
+ x509_certificate_path_chain: ""
+ x509_certificate_path_fullchain: "{{ static_ca_cert_path }}/{{ static_ca_cert_name }}-crt.pem"
+
+- name: generate custom post-renewal script
+ when: x509_certificate_renewal is defined
+ template:
+ src: updated.sh.j2
+ dest: "{{ static_ca_cert_path }}/updated.sh"
+ mode: 0755
+
+- name: call custom post-renewal script
+ when:
+ - x509_certificate_renewal is defined
+ - (_static_ca_key_ is changed) or (_static_ca_cert_ is changed)
+ command: "{{ static_ca_cert_path }}/updated.sh"
diff --git a/roles/x509/static-ca/cert/prepare/templates/updated.sh.j2 b/roles/x509/static-ca/cert/prepare/templates/updated.sh.j2
new file mode 100644
index 00000000..f0757832
--- /dev/null
+++ b/roles/x509/static-ca/cert/prepare/templates/updated.sh.j2
@@ -0,0 +1,15 @@
+#!/bin/sh
+{% if 'install' in x509_certificate_renewal %}
+{% for file in x509_certificate_renewal.install %}
+
+install{% if 'mode' in file %} -m {{ file.mode }}{% endif %}{% if 'owner' in file %} -o {{ file.owner }}{% endif %}{% if 'group' in file %} -g {{ file.group }}{% endif %} /dev/null "{{ file.dest }}.new"
+{% for src in file.src %}
+cat "{{ lookup('vars', 'x509_certificate_path_' + src) }}" >> "{{ file.dest }}.new"
+{% endfor %}
+mv "{{ file.dest }}.new" "{{ file.dest }}"
+{% endfor %}
+{% endif %}
+{% if 'reload' in x509_certificate_renewal %}
+
+{{ x509_certificate_renewal.reload | trim }}
+{% endif %}
diff --git a/roles/x509/static-ca/contrib/gen-ca.py b/roles/x509/static-ca/contrib/gen-ca.py
new file mode 100755
index 00000000..8f99da6c
--- /dev/null
+++ b/roles/x509/static-ca/contrib/gen-ca.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.x509.oid import NameOID
+import datetime
+import argparse
+
+parser = argparse.ArgumentParser("spreadspace ansible CA-generator")
+parser.add_argument("-n", "--variable-name", dest='varname', help="ansible variable name to be used", type=str, required=True)
+parser.add_argument("-CN", "--common-name", dest='CN', help="Common Name field of the CA's subject", type=str, required=True)
+parser.add_argument("-O", "--organization-name", dest='O', help="Organization Name field of the CA's subject", type=str)
+parser.add_argument("-OU", "--organizational-unit", dest='OU', help="Organizational Unit field of the CA's subject", type=str)
+parser.add_argument("-C", "--country-name", dest='C', help="Country Name field of the CA's subject", type=str)
+parser.add_argument("-ST", "--state-or-provice", dest='ST', help="State-or-Province field of the CA's subject", type=str)
+parser.add_argument("-L", "--locality", dest='L', help="Locality Name field of the CA's subject", type=str)
+args = parser.parse_args()
+
+subject_fields = []
+if args.CN:
+ subject_fields.append(x509.NameAttribute(NameOID.COMMON_NAME, args.CN))
+if args.O:
+ subject_fields.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, args.O))
+if args.OU:
+ subject_fields.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, args.OU))
+if args.C:
+ subject_fields.append(x509.NameAttribute(NameOID.COUNTRY_NAME, args.C))
+if args.ST:
+ subject_fields.append(x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, args.ST))
+if args.L:
+ subject_fields.append(x509.NameAttribute(NameOID.LOCALITY_NAME, args.L))
+
+subject = issuer = x509.Name(subject_fields)
+private_key = rsa.generate_private_key(
+ public_exponent=65537,
+ key_size=4096,
+)
+public_key = private_key.public_key()
+
+builder = x509.CertificateBuilder()
+builder = builder.subject_name(subject)
+builder = builder.issuer_name(issuer)
+builder = builder.not_valid_before(datetime.datetime.today() - datetime.timedelta(days=1))
+builder = builder.not_valid_after(datetime.datetime.today() + datetime.timedelta(weeks=2080)) # about 20years
+builder = builder.serial_number(x509.random_serial_number())
+builder = builder.public_key(public_key)
+builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
+certificate = builder.sign(private_key=private_key, algorithm=hashes.SHA256())
+
+
+private_key_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())
+certificate_pem = certificate.public_bytes(serialization.Encoding.PEM)
+
+
+print("## Add this to vault file")
+print("")
+print("vault_%s_key: |" % (args.varname))
+for line in private_key_pem.splitlines():
+ print(" {}".format(line.decode('utf-8')))
+
+print("")
+print("")
+print("")
+print("")
+print("## Add this to vars file")
+print("")
+print("%s_key: \"{{ vault_%s_key }}\"" % (args.varname, args.varname))
+print("%s_cert: |" % (args.varname))
+for line in certificate_pem.splitlines():
+ print(" {}".format(line.decode('utf-8')))