created tasks to handle minimal-ca, self-signed and letsencrypt-certbot certificates

This commit is contained in:
AnsibleGuy 2021-11-04 22:05:17 +01:00
parent 84c2277e80
commit 066d95535e
22 changed files with 861 additions and 41 deletions

114
README.md
View File

@ -1,23 +1,28 @@
# Ansible Role for certificate generation # Certificate Generator Role
**Tested:** **Tested:**
* Debian 11 * Debian 11
## Functionality ## Functionality
* Package installation * **Package installation**
* Ansible dependencies (_minimal_) * Ansible dependencies (_minimal_)
* * Crypto Dependencies
* Configuration
* Two Possible Modes
* Generate Self-Signed certificate * **Configuration**
* Create an internal-ca and generate certificates using it * **Four Possible Modes**:
* Default config: * Generate **Self-Signed** certificate
* Use a **minimal Certificate Authority** to create signed certificates
* Configure **LetsEncrypt-Certbot** to generate publicly valid certificates
* Supported for Nginx and Apache
* Host needs to have a valid public dns record pointed at it
* Needs to be publicly reachable over port 80/tcp
* _Use a proper **Certificate Authority** (_full PKI_) to create **signed certificates**_ => not yet available
* **Default config**:
* Mode => Self-Signed * Mode => Self-Signed
* Default opt-ins:
*
* Default opt-outs:
*
## Info ## Info
@ -28,6 +33,10 @@
* **Note:** Most of this functionality can be opted in or out using the main defaults file and variables! * **Note:** Most of this functionality can be opted in or out using the main defaults file and variables!
* **Note:** The certificate file-name (_name variable as defined or else CommonName_) will be updated:
* spaces are transformed into underlines
* all Characters except "0-9a-zA-Z." are removed
* the file-extension (_crt/chain.crt/key/csr_) will be appended
## Requirements ## Requirements
@ -36,20 +45,89 @@
## Usage ## Usage
Define the config as needed: ### Notes
The **self-signed and minimal-ca** modes will only create a single certificate per run.
Re-runs can save some overhead by using the 'certs' tag.
The **LetsEncrypt** mode will create/remove multiple certificates as defined.
### Config
Example for LetsEncrypt config:
```yaml ```yaml
app: certs:
mode: 'le_certbot'
path: '/etc/apache2/ssl'
letsencrypt:
certs:
myNiceSite:
domains: ['myRandomSite.net', 'ansibleguy.net']
email: 'certs@template.ansibleguy.net'
service: 'apache'
``` ```
Example for Self-Signed config:
```yaml
certs:
mode: 'selfsigned'
path: '/etc/nginx/ssl'
group_key: 'nginx'
owner_cert: 'nginx'
cert:
cn: 'My great certificate!'
org: 'AnsibleGuy'
country: 'AT'
email: 'certs@template.ansibleguy.net'
domains: ['mySoGreat.site', 'ansibleguy.net']
ips: ['192.168.44.2']
pwd: !vault ...
```
Example for minimal-CA config:
```yaml
certs:
mode: 'ca'
path: '/etc/ca/certs'
mode_key: '0400'
cert:
name: 'custom_file_name' # extension will be appended
cn: 'My great certificate!'
org: 'AnsibleGuy'
country: 'AT'
email: 'certs@template.ansibleguy.net'
domains: ['mySoGreat.site', 'ansibleguy.net']
ca:
path: '/etc/ca'
cn: 'SUPER CertificateAuthority'
org: 'AnsibleGuy'
country: 'AT'
email: 'certs@template.ansibleguy.net'
pwd: !vault ...
```
Using the minimal-CA you can create multiple certificates signed by the CA by re-running the role with changed 'cert' settings.
You might want to use 'ansible-vault' to encrypt your passwords:
```bash
ansible-vault encrypt_string
```
### Execution
Run the playbook: Run the playbook:
```bash ```bash
ansible-playbook -K -D -i inventory/hosts.yml playbook.yml ansible-playbook -K -D -i inventory/hosts.yml playbook.yml --ask-vault-pass
``` ```
There are also some useful **tags** available: There are also some useful **tags** available:
* base => only configure basics; sites will not be touched * certs => ignore ca tasks; only generate certs
* sites * selfsigned
* config * config
* certs * certs

View File

@ -1,8 +1,85 @@
--- ---
# default config => is overwritten by provided config # default config => is overwritten by provided config
default_app: {} default_certs:
mode: 'selfsigned' # selfsigned, ca, ca_min, le_certbot
path: '/etc/certs'
APP_CONFIG: "{{ default_app | combine(app, recursive=true) }}" cert:
name:
key_size: 4096 # 1024, 2048, 4096
key_type: 'RSA'
cipher: 'AES-256-CBC' # see: 'openssl list -cipher-algorithms'
digest: 'sha256'
regenerate: 'partial_idempotence'
pwd:
domains: []
ips: []
default_instance_config: {} # certificate config
cn: 'Ansible Certificate'
org:
ou:
country:
state:
locality:
email: # if using letsencrypt you might pass an email per domain => see letsencrypt-certs
key_usage: 'serverAuth' # serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, ocspSigning
ocsp_staple: false
crl_distribution: []
# - full_name:
# - "URI:https://ca.example.com/revocations.crl"
# crl_issuer:
# - "URI:https://ca.example.com/"
# reasons:
# - key_compromise
# - ca_compromise
# - cessation_of_operation
valid_days: 730
mode_key: '0640'
mode_cert: '0644'
owner_key: 'root'
group_key: 'root'
owner_cert: 'root'
group_cert: 'root'
extension_cert: 'crt'
extension_key: 'key'
extension_csr: 'csr'
letsencrypt:
path: '/etc/letsencrypt'
service: # apache, nginx
renew_timer: 'Mon *-*-* 01:00:00'
verbosity: 'v'
certs: {} # see 'default_le_certbot_cert_config'
renew: false # if a renewal should be started by the role; the renewal service will auto-renew the certificates otherwise
ca:
path: '/etc/certs/ca'
valid_days: 7300
key_size: 8192 # 1024, 2048, 4096, 8192
key_type: 'RSA'
cipher: 'AES-256-CBC' # see: 'openssl list -cipher-algorithms'
digest: 'sha512'
regenerate: 'partial_idempotence'
pwd:
# certificate config
cn: 'CA Certificate'
org:
ou:
country:
state:
locality:
email:
CERT_CONFIG: "{{ default_certs | combine(certs, recursive=true) }}"
default_le_certbot_cert_config:
domains: []
state: 'present'
email:

View File

@ -1,4 +1,5 @@
from re import sub as regex_replace from re import sub as regex_replace
from re import match as regex_match
class FilterModule(object): class FilterModule(object):
@ -6,16 +7,35 @@ class FilterModule(object):
def filters(self): def filters(self):
return { return {
"safe_key": self.safe_key, "safe_key": self.safe_key,
"fallback": self.fallback, "valid_domain": self.valid_domain,
"valid_ip": self.valid_ip,
"check_email": self.check_email,
} }
@staticmethod @staticmethod
def safe_key(key: str) -> str: def safe_key(key: str) -> str:
return regex_replace('[^0-9a-zA-Z]+', '', key.replace(' ', '_')) return regex_replace(r'[^0-9a-zA-Z\.]+', '', key.replace(' ', '_'))
@staticmethod @staticmethod
def fallback(opt1: str, opt2: str) -> str: def valid_domain(domain: str) -> bool:
if opt1 not in [None, '', 'None', 'none', ' ']: expr = r'^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$'
return opt1 return True if regex_match(expr, domain) is not None else False
@staticmethod
def valid_ip(ip: str) -> bool:
expr_ipv4 = r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$'
expr_ipv6 = r'^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$'
if regex_match(expr_ipv4, ip) is not None or regex_match(expr_ipv6, ip) is not None:
return True
return False
@staticmethod
def check_email(certs: dict) -> bool:
for settings in certs.values():
if 'email' not in settings or settings['email'] in ['', ' ', None, 'null', 'None']:
return False
return True
return opt2

View File

@ -1,9 +1,9 @@
--- ---
# ansible-playbook -K -D -i inventory/hosts.yml playbook.yml # ansible-playbook -K -D -i inventory/hosts.yml playbook.yml --ask-vault-pass
- hosts: all # should be limited - hosts: localhost # should be limited
become: true become: true
gather_facts: yes gather_facts: yes
roles: roles:
- ansibleguy.ROLE - ansibleguy.infra_certs

View File

@ -2,5 +2,5 @@
# install: ansible-galaxy install -r requirements.yml # install: ansible-galaxy install -r requirements.yml
collections: [] collections: []
# - name: 'community.general' - name: 'community.crypto'
# source: 'https://galaxy.ansible.com' source: 'https://galaxy.ansible.com'

9
tasks/debian/ca_full.yml Normal file
View File

@ -0,0 +1,9 @@
---
# creating ca with full pki
# to be continued (;
- name: Certificates | Debian | Internal | CA | Not yet implemented
ansible.builtin.debug:
msg: "The certificate mode 'ca_full' is not yet implemented!"
tags: ca

View File

@ -0,0 +1,29 @@
---
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Install package
ansible.builtin.package:
name: ['python3-certbot-apache']
state: present
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Checking sites
ansible.builtin.shell: 'ls /etc/apache2/sites-enabled/'
changed_when: false
register: enabled_apache_sites
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Deploying temporary apache site
ansible.builtin.template:
src: 'templates/etc/apache2/sites-enabled/le_dummy.conf.j2'
dest: '/etc/apache2/sites-enabled/tmp_le_dummy.conf'
owner: 'root'
group: 'root'
mode: 0644
register: tmp_site_enable
when: enabled_apache_sites.stdout == ''
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Reloading apache
ansible.builtin.systemd:
name: 'apache2.service'
state: reloaded
when:
- enabled_apache_sites.stdout == ''
- tmp_site_enable.changed

View File

@ -0,0 +1,13 @@
---
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Disable temporary site
ansible.builtin.file:
state: absent
path: '/etc/apache2/sites-enabled/tmp_le_dummy.conf'
register: tmp_site_disable
- name: Certificates | Debian | LetsEncrypt Certbot | Apache | Reloading apache
ansible.builtin.systemd:
name: 'apache2.service'
state: reloaded
when: tmp_site_disable.changed

View File

@ -0,0 +1,23 @@
---
- name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Deploying temporary apache site
ansible.builtin.template:
src: 'templates/etc/apache2/sites-available/le_dummy.conf.j2'
dest: '/etc/apache2/sites-available/tmp_le_dummy.conf'
owner: 'root'
group: 'root'
mode: 0644
- name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Enable apache site
ansible.builtin.file:
state: link
src: '/etc/apache2/sites-available/tmp_le_dummy.conf'
dest: '/etc/apache2/sites-enabled/tmp_le_dummy.conf'
owner: 'root'
group: 'root'
mode: 0644
- name: Apache | Debian | LetsEncrypt Certbot | Dependencies | Reload apache
ansible.builtin.systemd:
name: 'apache2.service'
state: reloaded

View File

@ -0,0 +1,58 @@
---
# todo: check domains registered in current certificate (certbot certificates) and remove it if there are more than configured before re-configuring it
- name: "Certificates | Debian | LetsEncrypt Certbot | {{ le_name }} | Creating directory"
ansible.builtin.file:
path: "{{ le_path }}"
state: directory
owner: 'root'
group: 'root'
mode: 0755
- name: "Certificates | Debian | LetsEncrypt Certbot | {{ le_name }} | Command to be executed"
ansible.builtin.debug:
msg: "certbot certonly --non-interactive --agree-tos --no-redirect
--{{ CERT_CONFIG.letsencrypt.service }} --cert-name {{ le_name }}
-{{ CERT_CONFIG.letsencrypt.verbosity }}
--rsa-key-size {{ le_cert.key_size | default(CERT_CONFIG.cert.key_size, true) }}
--config-dir {{ CERT_CONFIG.letsencrypt.path }}
{% for domain in le_cert.domains %}{% if domain | valid_domain %}--domain {{ domain }} {% endif %}{% endfor %}
{% if le_cert.email is not none %}--email {{ le_cert.email }} {% elif CERT_CONFIG.cert.email | default(none, true) is not none %}--email {{ CERT_CONFIG.cert.email }} {% endif %}"
when: existing_certs_raw.stdout.find(name) == -1
- name: "Certificates | Debian | LetsEncrypt Certbot | {{ le_name }} | Starting certbot"
ansible.builtin.command: "certbot certonly --non-interactive --agree-tos --no-redirect
--{{ CERT_CONFIG.letsencrypt.service }} --cert-name {{ le_name }}
-{{ CERT_CONFIG.letsencrypt.verbosity }}
--rsa-key-size {{ le_cert.key_size | default(CERT_CONFIG.cert.key_size, true) }}
--config-dir {{ CERT_CONFIG.letsencrypt.path }}
{% for domain in le_cert.domains %}{% if domain | valid_domain %}--domain {{ domain }} {% endif %}{% endfor %}
{% if le_cert.email is not none %}--email {{ le_cert.email }} {% elif CERT_CONFIG.cert.email | default(none, true) is not none %}--email {{ CERT_CONFIG.cert.email }} {% endif %}"
when: existing_certs_raw.stdout.find(name) == -1
- name: "Certificates | Debian | LetsEncrypt Certbot | {{ le_name }} | Linking cert"
ansible.builtin.file:
state: link
src: "{{ item.src }}"
dest: "{{ item.dst }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
follow: true
force: true
loop:
- {'dst': "{{ CERT_CONFIG.path }}/{{ le_name }}.{{ CERT_CONFIG.extension_cert }}", 'src': "{{ le_path }}/cert.pem"}
- {'dst': "{{ CERT_CONFIG.path }}/{{ le_name }}.chain.{{ CERT_CONFIG.extension_cert }}", 'src': "{{ le_path }}/chain.pem"}
- {'dst': "{{ CERT_CONFIG.path }}/{{ le_name }}.fullchain.{{ CERT_CONFIG.extension_cert }}", 'src': "{{ le_path }}/fullchain.pem"}
- name: "Certificates | Debian | LetsEncrypt Certbot | {{ le_name }} | Linking key"
ansible.builtin.file:
state: link
src: "{{ le_path }}/privkey.pem"
dest: "{{ CERT_CONFIG.path }}/{{ le_name }}.{{ CERT_CONFIG.extension_key }}"
mode: "{{ CERT_CONFIG.mode_key }}"
owner: "{{ CERT_CONFIG.owner_key }}"
group: "{{ CERT_CONFIG.group_key }}"
follow: true
force: true

View File

@ -0,0 +1,86 @@
---
- name: Certificates | Debian | LetsEncrypt Certbot | Checking config
ansible.builtin.fail:
msg: "The required configuration was not provided!
Needed: 'certs.letsencrypt.certs', 'certs.letsencrypt.service',
'certs.letsencrypt.email or certs.letsencrypt.email.certs.email'"
when: >
CERT_CONFIG.letsencrypt.certs | length == 0 or
CERT_CONFIG.letsencrypt.service is none | default(none, true) or
(CERT_CONFIG.letsencrypt.email | default(none, true) is none and not CERT_CONFIG.letsencrypt.certs|check_email)
- name: Certificates | Debian | LetsEncrypt Certbot | Checking service
ansible.builtin.fail:
msg: "You need to supply a supported LetsEncrypt Certbot service to use! (apache/nginx)"
when: "CERT_CONFIG.letsencrypt.service | default(none, true) is none or CERT_CONFIG.letsencrypt.service not in ['apache', 'nginx']"
- name: Certificates | Debian | LetsEncrypt Certbot | Configure for Apache2
ansible.builtin.import_tasks: apache.yml
when: CERT_CONFIG.letsencrypt.service == 'apache'
- name: Certificates | Debian | LetsEncrypt Certbot | Configure for Nginx
ansible.builtin.import_tasks: nginx.yml
when: CERT_CONFIG.letsencrypt.service == 'nginx'
- name: Certificates | Debian | LetsEncrypt Certbot | Pulling existing certs
ansible.builtin.shell: 'certbot certificates'
register: existing_certs_raw
changed_when: false
- name: Certificates | Debian | LetsEncrypt Certbot | Adding certificates
ansible.builtin.include_tasks: cert.yml
when:
- le_cert.domains | length > 0
- le_cert.state == 'present'
vars:
le_cert: "{{ default_le_certbot_cert_config | combine(cert_item.value, recursive=true) }}"
le_name: "{{ cert_item.key | safe_key }}"
le_path: "{{ CERT_CONFIG.letsencrypt.path }}/live/{{ name }}"
loop_control:
loop_var: cert_item
with_dict: "{{ CERT_CONFIG.letsencrypt.certs }}"
- name: Certificates | Debian | LetsEncrypt Certbot | Removing certificates
ansible.builtin.command: "certbot revoke --cert-name {{ le_name }} && certbot delete --cert-name {{ le_name }}"
when:
- le_cert.state != 'present'
- existing_certs_raw.stdout.find(le_name) != -1
vars:
le_cert: "{{ default_le_certbot_cert_config | combine(cert_item.value, recursive=true) }}"
le_name: "{{ cert_item.key | safe_key }}"
loop_control:
loop_var: cert_item
with_dict: "{{ CERT_CONFIG.letsencrypt.certs }}"
- name: Certificates | Debian | LetsEncrypt Certbot | Cleanup for Apache2
ansible.builtin.import_tasks: apache_cleanup.yml
when: CERT_CONFIG.letsencrypt.service == 'apache'
- name: Certificates | Debian | LetsEncrypt Certbot | Cleanup for Nginx
ansible.builtin.import_tasks: nginx_cleanup.yml
when: CERT_CONFIG.letsencrypt.service == 'nginx'
- name: Certificates | Debian | LetsEncrypt Certbot | Adding service for certbot renewal
ansible.builtin.template:
src: "templates/etc/systemd/system/{{ item }}.j2"
dest: "/etc/systemd/system/{{ item }}"
owner: 'root'
group: 'root'
mode: 0644
with_items:
- 'ansibleguy.infra_certs.LetsEncryptCertbot.service'
- 'ansibleguy.infra_certs.LetsEncryptCertbot.timer'
- name: Certificates | Debian | LetsEncrypt Certbot | Enabling cert-renewal timer
ansible.builtin.systemd:
daemon_reload: yes
name: 'ansibleguy.infra_certs.LetsEncryptCertbot.timer'
enabled: yes
state: started
- name: Certificates | Debian | LetsEncrypt Certbot | Running renewal
ansible.builtin.command: 'certbot renew --force-renewal'
when: CERT_CONFIG.letsencrypt.renew
ignore_errors: true

View File

@ -0,0 +1,29 @@
---
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Install package
ansible.builtin.package:
name: ['python3-certbot-nginx']
state: present
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Checking sites
ansible.builtin.shell: 'ls /etc/nginx/sites-enabled/'
changed_when: false
register: enabled_nginx_sites
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Deploying temporary apache site
ansible.builtin.template:
src: 'templates/etc/nginx/sites-enabled/le_dummy.j2'
dest: '/etc/nginx/sites-enabled/tmp_le_dummy'
owner: 'root'
group: 'root'
mode: 0644
register: tmp_site_enable
when: enabled_nginx_sites.stdout == ''
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Reloading apache
ansible.builtin.systemd:
name: 'nginx.service'
state: reloaded
when:
- enabled_nginx_sites.stdout == ''
- tmp_site_enable.changed

View File

@ -0,0 +1,13 @@
---
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Disable temporary site
ansible.builtin.file:
state: absent
path: '/etc/nginx/sites-enabled/tmp_le_dummy'
register: tmp_site_disable
- name: Certificates | Debian | LetsEncrypt Certbot | Nginx | Reloading apache
ansible.builtin.systemd:
name: 'nginx.service'
state: reloaded
when: tmp_site_disable.changed

View File

@ -1,6 +0,0 @@
---
- name: ROLE | Debian | Task
ansible.builtin.apt:
pkg: "{{ something }}"
tags: [base]

View File

@ -0,0 +1,106 @@
---
# creating a minimal ca
- name: Certificates | Internal | Minimal CA | Creating ca directory
ansible.builtin.file:
path: "{{ CERT_CONFIG.ca.path }}"
state: directory
- name: Certificates | Internal | Minimal CA | Generate ca private key (encrypted key)
community.crypto.openssl_privatekey:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
passphrase: "{{ CERT_CONFIG.ca.pwd }}"
cipher: "{{ CERT_CONFIG.ca.cipher }}"
size: "{{ CERT_CONFIG.ca.key_size }}"
type: "{{ CERT_CONFIG.ca.key_type }}"
regenerate: "{{ CERT_CONFIG.ca.regenerate }}"
mode: "{{ CERT_CONFIG.mode_key }}"
owner: "{{ CERT_CONFIG.owner_key }}"
group: "{{ CERT_CONFIG.group_key }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is not none
- name: Certificates | Internal | Minimal CA | Generate ca private key (plain key)
community.crypto.openssl_privatekey:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
size: "{{ CERT_CONFIG.ca.key_size }}"
type: "{{ CERT_CONFIG.ca.key_type }}"
regenerate: "{{ CERT_CONFIG.ca.regenerate }}"
mode: "{{ CERT_CONFIG.mode_key }}"
owner: "{{ CERT_CONFIG.owner_key }}"
group: "{{ CERT_CONFIG.group_key }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is none
# NOTE: for details see https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html
- name: Certificates | Internal | Minimal CA | Generating ca signing-request (encrypted key)
community.crypto.openssl_csr:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_csr }}"
privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase: "{{ CERT_CONFIG.ca.pwd }}"
basic_constraints: ['CA:TRUE', 'pathlen:2']
basic_constraints_critical: true
key_usage: ['cRLSign', 'digitalSignature', 'keyCertSign']
key_usage_critical: true
digest: "{{ CERT_CONFIG.ca.digest }}"
common_name: "{{ CERT_CONFIG.ca.cn }}"
organization_name: "{{ CERT_CONFIG.ca.org }}"
country_name: "{{ CERT_CONFIG.ca.country }}"
state_or_province_name: "{{ CERT_CONFIG.ca.state }}"
locality_name: "{{ CERT_CONFIG.ca.locality }}"
email_address: "{{ CERT_CONFIG.ca.email }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is not none
- name: Certificates | Internal | Minimal CA | Generating ca signing-request (plain key)
community.crypto.openssl_csr:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_csr }}"
privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
basic_constraints: ['CA:TRUE', 'pathlen:2']
basic_constraints_critical: true
key_usage: ['cRLSign', 'digitalSignature', 'keyCertSign']
key_usage_critical: true
digest: "{{ CERT_CONFIG.ca.digest }}"
common_name: "{{ CERT_CONFIG.ca.cn }}"
organization_name: "{{ CERT_CONFIG.ca.org }}"
country_name: "{{ CERT_CONFIG.ca.country }}"
state_or_province_name: "{{ CERT_CONFIG.ca.state }}"
locality_name: "{{ CERT_CONFIG.ca.locality }}"
email_address: "{{ CERT_CONFIG.ca.email }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is none
- name: Certificates | Internal | Minimal CA | Generating ca certificate (encrypted key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
csr_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_csr }}"
privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase: "{{ CERT_CONFIG.ca.pwd }}"
provider: selfsigned
valid_in: "{{ CERT_CONFIG.ca.valid_days }}d"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is not none
- name: Certificates | Internal | Minimal CA | Generating ca certificate (plain key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
csr_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_csr }}"
provider: selfsigned
valid_in: "{{ CERT_CONFIG.ca.valid_days }}d"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.ca.pwd | default(none, true) is none

202
tasks/internal/cert.yml Normal file
View File

@ -0,0 +1,202 @@
---
- name: Certificates | Internal | Cert | Generate private key (encrypted)
community.crypto.openssl_privatekey:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
cipher: "{{ CERT_CONFIG.cert.cipher }}"
size: "{{ CERT_CONFIG.cert.key_size }}"
type: "{{ CERT_CONFIG.cert.key_type }}"
passphrase: "{{ CERT_CONFIG.cert.pwd }}"
regenerate: "{{ CERT_CONFIG.cert.regenerate }}"
mode: "{{ CERT_CONFIG.mode_key }}"
owner: "{{ CERT_CONFIG.owner_key }}"
group: "{{ CERT_CONFIG.group_key }}"
no_log: true
when: CERT_CONFIG.cert.pwd | default(none, true) is not none
- name: Certificates | Internal | Cert | Generate private key (plain)
community.crypto.openssl_privatekey:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
size: "{{ CERT_CONFIG.cert.key_size }}"
type: "{{ CERT_CONFIG.cert.key_type }}"
regenerate: "{{ CERT_CONFIG.cert.regenerate }}"
mode: "{{ CERT_CONFIG.mode_key }}"
owner: "{{ CERT_CONFIG.owner_key }}"
group: "{{ CERT_CONFIG.group_key }}"
no_log: true
when: CERT_CONFIG.cert.pwd | default(none, true) is none
- name: Certificates | Internal | Cert | Setting SAN
ansible.builtin.set_fact:
cert_san: "{% for domain in CERT_CONFIG.cert.domains %}
{% if domain | valid_domain %}DNS:{{ domain }}{% if not loop.last %},{% endif %}{% endif %}
{% endfor %}
{% for ip in CERT_CONFIG.cert.ips %}
{% if ip | valid_ip %},IP:{{ ip }}{% endif %}
{% endfor %}"
- name: Certificates | Internal | Cert | Generating signing-request (encrypted key)
community.crypto.openssl_csr:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase : "{{ CERT_CONFIG.cert.pwd }}"
digest: "{{ CERT_CONFIG.cert.digest }}"
common_name: "{{ CERT_CONFIG.cert.cn }}"
organization_name: "{{ CERT_CONFIG.cert.org }}"
country_name: "{{ CERT_CONFIG.cert.country }}"
state_or_province_name: "{{ CERT_CONFIG.cert.state }}"
locality_name: "{{ CERT_CONFIG.cert.locality }}"
email_address: "{{ CERT_CONFIG.cert.email }}"
extended_key_usage: "{{ CERT_CONFIG.cert.key_usage }}"
ocsp_must_staple: "{{ CERT_CONFIG.cert.ocsp_staple }}"
crl_distribution_points: "{{ CERT_CONFIG.cert.crl_distribution }}"
subject_alt_name: "{{ cert_san | replace(' ', '') | default('DNS:localhost', true) }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.cert.pwd | default(none, true) is not none
- name: Certificates | Internal | Cert | Generating signing-request (plain key)
community.crypto.openssl_csr:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
digest: "{{ CERT_CONFIG.cert.digest }}"
common_name: "{{ CERT_CONFIG.cert.cn }}"
organization_name: "{{ CERT_CONFIG.cert.org }}"
country_name: "{{ CERT_CONFIG.cert.country }}"
state_or_province_name: "{{ CERT_CONFIG.cert.state }}"
locality_name: "{{ CERT_CONFIG.cert.locality }}"
email_address: "{{ CERT_CONFIG.cert.email }}"
extended_key_usage: "{{ CERT_CONFIG.cert.key_usage }}"
ocsp_must_staple: "{{ CERT_CONFIG.cert.ocsp_staple }}"
crl_distribution_points: "{{ CERT_CONFIG.cert.crl_distribution }}"
subject_alt_name: "{{ cert_san | replace(' ', '') | default('DNS:localhost', true) }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when: CERT_CONFIG.cert.pwd | default(none, true) is none
- name: Certificates | Internal | Cert | Self-Signed | Generating certificate (encrypted key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase: "{{ CERT_CONFIG.cert.pwd }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
provider: selfsigned
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.cert.pwd | default(none, true) is not none
- CERT_CONFIG.mode == 'selfsigned'
- name: Certificates | Internal | Cert | Self-Signed | Generating certificate (plain key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
provider: selfsigned
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.cert.pwd | default(none, true) is none
- CERT_CONFIG.mode == 'selfsigned'
- name: Certificates | Internal | Cert | CA-Signed | Generating certificate (encrypted key; encrypted ca-key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase: "{{ CERT_CONFIG.cert.pwd }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
provider: ownca
ownca_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
ownca_privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
ownca_privatekey_passphrase: "{{ CERT_CONFIG.ca.pwd }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.ca.pwd | default(none, true) is not none
- CERT_CONFIG.cert.pwd | default(none, true) is not none
- CERT_CONFIG.mode == 'ca'
- name: Certificates | Internal | Cert | CA-Signed | Generating certificate (plain key; encrypted ca-key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
provider: ownca
ownca_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
ownca_privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
ownca_privatekey_passphrase: "{{ CERT_CONFIG.ca.pwd }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.ca.pwd | default(none, true) is not none
- CERT_CONFIG.cert.pwd | default(none, true) is none
- CERT_CONFIG.mode == 'ca'
- name: Certificates | Internal | Cert | CA-Signed | Generating certificate (encrypted key; plain ca-key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
privatekey_passphrase: "{{ CERT_CONFIG.cert.pwd }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
provider: ownca
ownca_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
ownca_privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.ca.pwd | default(none, true) is none
- CERT_CONFIG.cert.pwd | default(none, true) is not none
- CERT_CONFIG.mode == 'ca'
- name: Certificates | Internal | Cert | CA-Signed | Generating certificate (plain key; plain ca-key)
community.crypto.x509_certificate:
path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}"
privatekey_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_key }}"
csr_path: "{{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_csr }}"
valid_in: "{{ CERT_CONFIG.cert.valid_days }}d"
provider: ownca
ownca_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }}"
ownca_privatekey_path: "{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_key }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
no_log: true
when:
- CERT_CONFIG.ca.pwd | default(none, true) is none
- CERT_CONFIG.cert.pwd | default(none, true) is none
- CERT_CONFIG.mode == 'ca'
- name: Certificates | Internal | Cert | CA-Signed | Creating chained certificate
ansible.builtin.shell: "cat {{ CERT_CONFIG.path }}/{{ name }}.{{ CERT_CONFIG.extension_cert }}
{{ CERT_CONFIG.ca.path }}/ca.{{ CERT_CONFIG.extension_cert }} >
{{ CERT_CONFIG.path }}/{{ name }}.chain.{{ CERT_CONFIG.extension_cert }}"
args:
creates: "{{ CERT_CONFIG.path }}/{{ name }}.chain.{{ CERT_CONFIG.extension_cert }}"
when: CERT_CONFIG.mode == 'ca'
- name: Certificates | Internal | Cert | CA-Signed | Setting privileges on chained certificate
ansible.builtin.file:
path: "{{ CERT_CONFIG.path }}/{{ name }}.chain.{{ CERT_CONFIG.extension_cert }}"
mode: "{{ CERT_CONFIG.mode_cert }}"
owner: "{{ CERT_CONFIG.owner_cert }}"
group: "{{ CERT_CONFIG.group_cert }}"
when: CERT_CONFIG.mode == 'ca'

22
tasks/internal/main.yml Normal file
View File

@ -0,0 +1,22 @@
---
- name: Certificates | Internal | Installing dependencies
ansible.builtin.package:
pkg: ['python3-cryptography']
tags: [certs, ca]
- name: Certificates | Internal | Creating cert directory
ansible.builtin.file:
path: "{{ CERT_CONFIG.path }}"
state: directory
tags: [certs, ca]
- name: Certificates | Internal | Minimal CA
ansible.builtin.import_tasks: ca_minimal.yml
when: CERT_CONFIG.mode == 'ca'
tags: [ca]
- name: Certificates | Internal | Cert
ansible.builtin.import_tasks: cert.yml
when: "CERT_CONFIG.mode in ['ca', 'selfsigned']"
tags: [certs]

View File

@ -1,5 +1,25 @@
--- ---
- name: ROLE | Processing debian config - name: Certificates | Checking config
ansible.builtin.import_tasks: debian/main.yml ansible.builtin.fail:
when: "ansible_distribution|lower in ['debian', 'ubuntu']" msg: "The required configuration was not provided!
Needed: 'certs'"
when: certs is undefined
- name: Certificates | Setting name
ansible.builtin.set_fact:
name: "{% if CERT_CONFIG.cert.name is not none %}{{ CERT_CONFIG.cert.name | safe_key }}{% else %}{{ CERT_CONFIG.cert.cn | safe_key }}{% endif %}"
- name: Certificates | Internal signed
ansible.builtin.include_tasks: internal/main.yml
when: "CERT_CONFIG.mode in ['ca_full', 'ca', 'selfsigned']"
- name: Certificates | Internal | CA
ansible.builtin.include_tasks: debian/ca_full.yml
when: CERT_CONFIG.mode == 'ca_full'
- name: Certificates | Debian | Letsencrypt
ansible.builtin.include_tasks: debian/letsencrypt/main.yml
when:
- CERT_CONFIG.mode == 'le_certbot'
- "ansible_distribution|lower in ['debian', 'ubuntu']"

View File

@ -0,0 +1,9 @@
# {{ ansible_managed }}
# ansibleguy.infra_certs - dummy site used for letsencrypt certbot
<VirtualHost *:80>
ServerName dummy.letsencrypt.localhost
ServerAdmin webmaster@localhost
ErrorLog {{ APACHE_CONFIG.log.path }}/error.log
CustomLog {{ APACHE_CONFIG.log.path }}/access.log combined
</VirtualHost>

View File

@ -0,0 +1,9 @@
# {{ ansible_managed }}
# ansibleguy.infra_certs - dummy site used for letsencrypt certbot
server {
listen 80;
server_name dummy.letsencrypt.localhost;
error_log {{ NGINX_CONFIG.log.path }}/error.log;
access_log {{ NGINX_CONFIG.log.path }}/access.log;
}

View File

@ -0,0 +1,10 @@
# {{ ansible_managed }}
# ansibleguy.infra_certs
[Unit]
Description=Service to renew LetsEncrypt Certificates using certbot
[Service]
Type=oneshot
ExecStart=certbot renew -{{ CERT_CONFIG.letsencrypt.verbosity }} --non-interactive --agree-tos --renew-with-new-domains
SuccessExitStatus=0

View File

@ -0,0 +1,13 @@
# {{ ansible_managed }}
# ansibleguy.infra_certs
[Unit]
Description=Timer to renew LetsEncrypt Certificates using certbot
[Timer]
OnCalendar={{ CERT_CONFIG.letsencrypt.renew_timer }}
Persistent=false
WakeSystem=false
[Install]
WantedBy=multi-user.target