ansible-collection/roles/common/tasks/ca-cert-renew.yml

265 lines
10 KiB
YAML

# Ansible Roles for managing Auengun.net Infrastructure & Testing/Learning.
# Source available at git.auengun.net/homelab/ansible-collection
# Copyright (C) 2023 GregoryDosh
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 GregoryDosh
---
- name: Seed Healtcheck Cron Minute
ansible.builtin.set_fact:
SSH_RENEW_RANDOM_MINUTE: "{{ SSH_RENEW_RANDOM_MINUTE | default(59 | random(seed=inventory_hostname_short)) }}"
- name: Get Healthcheck API Ping URL for Renewal
ansible.builtin.uri:
url: https://healthchecks.auengun.net/api/v2/checks/
method: POST
body_format: json
status_code: [200, 201]
return_content: true
headers:
Content-Type: application/json
X-Api-Key: "{{ HEALTHCHECK_SITE_API_KEY }}"
body: |
{
"name": "{{ STEP_HC_RENEWAL_NAME }} - Post Renewal",
"tz": "{{ HOST_TZ }}",
"timeout": {{ STEP_HC_RENEWAL_TIMEOUT }},
"grace": {{ STEP_HC_RENEWAL_GRACE }},
"tags": "{{ STEP_HC_RENEWAL_TAGS }}",
"unique": [
"name"
]
}
register: _hc_api_response_renewal
delegate_to: localhost
until: _hc_api_response_renewal.status == 200 or _hc_api_response_renewal.status == 201
# 5 minutes at 60 * 5 seconds
retries: 60
delay: 5
- ansible.builtin.set_fact:
_hc_ping_url: "{{ _hc_api_response_renewal.json.ping_url }}"
- name: Get existing ACME cert SANs (if exist)
become: true
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} certificate inspect {{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_CRT }} --format json | jq --sort-keys '.names'
no_log: true
ignore_errors: true
changed_when: false
register: _cert_existing_acme_san
- name: Get existing SSH cert SANs (if exist)
become: true
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ssh inspect {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }} --format json | jq --sort-keys '.Principals'
no_log: true
ignore_errors: true
changed_when: false
register: _cert_existing_ssh_san
- when: _cert_existing_acme_san.stdout
ansible.builtin.set_fact:
_cert_existing_acme_san_json: |
{{ _cert_existing_acme_san.stdout | from_json | sort | to_json(indent=4) }}
- when: _cert_existing_ssh_san.stdout
ansible.builtin.set_fact:
_cert_existing_ssh_san_json: |
{{ _cert_existing_ssh_san.stdout | from_json | sort | to_json(indent=4) }}
- when: not _cert_existing_acme_san.stdout
ansible.builtin.set_fact:
_cert_existing_acme_san_json: ""
- when: not _cert_existing_ssh_san.stdout
ansible.builtin.set_fact:
_cert_existing_ssh_san_json: ""
- ansible.builtin.set_fact:
_cert_acme_existing_san_match: |
{{ (_cert_existing_acme_san_json == _cert_san_json) | bool }}
_cert_ssh_existing_san_match: |
{{ (_cert_existing_ssh_san_json == _cert_san_json) | bool }}
- name: Check if ACME cert needs renewal
become: true
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} certificate needs-renewal {{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_CRT }} > /dev/stderr || true
register: _cert_acme_needs_renewal
changed_when: "'certificate does not need renewal' not in _cert_acme_needs_renewal.stderr"
- name: Check if SSH cert needs renewal
become: true
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ssh needs-renewal {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }} > /dev/stderr || true
register: _cert_ssh_needs_renewal
changed_when: "'certificate does not need renewal' not in _cert_ssh_needs_renewal.stderr"
- name: ACME Cert
when: (_cert_acme_needs_renewal.changed or not _cert_acme_existing_san_match) or (_cert_ssh_needs_renewal.changed or not _cert_ssh_existing_san_match)
become: true
block:
- name: Create ACME cert
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ca certificate \
{{ CERT_SAN[0] }} \
{{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_CRT }} \
{{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_KEY }} \
--ca-url {{ STEP_BOOTSTRAP_URL }} \
--provisioner {{ STEP_CERTS_ACME_CA_PROVISIONER }} \
{% if STEP_WEBROOT_PATH | length > 0 %}
--webroot={{ STEP_WEBROOT_PATH }} \
{% endif %}
{% for san in CERT_SAN %}
--san {{ san }} \
{% endfor %}
--force
register: _create_acme_cert
notify:
- Restart ACME-Renewal-Service
- name: adjust cert permissions
ansible.builtin.file:
path: "{{ item }}"
mode: "0644"
loop:
- "{{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_KEY }}"
- "{{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_CRT }}"
- name: SSH cert
when: _cert_ssh_needs_renewal.changed or not _cert_ssh_existing_san_match
become: true
block:
- name: Create SSH cert
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ssh certificate \
{{ CERT_SAN[0] }} \
{{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_PRIVATE_KEY }} \
--ca-url {{ STEP_BOOTSTRAP_URL }} \
--insecure \
--no-password \
--host \
--x5c-cert {{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_CRT }} \
--x5c-key {{ STEP_CERTS_PATH }}{{ STEP_CERTS_ACME_KEY }} \
{% for san in CERT_SAN %}
--principal {{ san }} \
{% endfor %}
--force
- name: ensure SSH cert permissions
ansible.builtin.file:
path: "{{ item }}"
mode: "0600"
loop:
- "{{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_PRIVATE_KEY }}"
- "{{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }}"
- name: generate ssh roots for validating user certs
ansible.builtin.shell: |
STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ssh config --roots > {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_TRUSTED_USER_CA_KEYS }}
changed_when: false
- name: Step SSH Config
become: true
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
backup: yes
validate: "{{ SSHD_BIN_ABSOLUTE_PATH }} -T -f %s"
marker: "# {mark} Step SSH Configuration https://ca.auengun.net (ANSIBLE MANAGED) -->"
block: |
Match all
TrustedUserCAKeys {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_TRUSTED_USER_CA_KEYS }}
HostKey {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_PRIVATE_KEY }}
HostCertificate {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }}
notify:
- "Restart ssh"
- name: Acme Cert Renewal Service (systemd)
when: ansible_service_mgr == 'systemd'
become: true
block:
- name: create systemd post exec script
ansible.builtin.template:
src: "{{ role_path }}/templates/post-renewal-exec.sh.j2"
dest: "{{ STEP_CONFIG_PATH }}{{ CERT_RENEWAL_SERVICE_NAME }}-post-renewal-exec.sh"
owner: "{{ STEP_USER_NAME }}"
group: "{{ STEP_GROUP_NAME }}"
mode: "0744"
register: _systemd_post_exec_script
- name: call initial script on update/create (or ACME cert renew)
when: _systemd_post_exec_script.changed or _create_acme_cert.changed
ansible.builtin.shell: |
{{ STEP_CONFIG_PATH }}{{ CERT_RENEWAL_SERVICE_NAME }}-post-renewal-exec.sh
- name: create systemd service
ansible.builtin.template:
src: "{{ role_path }}/templates/etc/systemd/system/cert-renew.service.j2"
dest: "/etc/systemd/system/{{ CERT_RENEWAL_SERVICE_NAME }}.service"
owner: "{{ STEP_USER_NAME }}"
group: "{{ STEP_GROUP_NAME }}"
mode: "0644"
register: _cert_renewal_service
notify:
- Restart ACME-Renewal-Service
- name: force systemd to reread configs
when: _cert_renewal_service.changed
ansible.builtin.systemd_service:
daemon_reload: true
- name: enable service
ansible.builtin.systemd_service:
name: "{{ CERT_RENEWAL_SERVICE_NAME }}.service"
state: started
enabled: true
- name: SSH Cert Renewal Service (crontab)
become: true
block:
- ansible.builtin.include_role:
name: auengun.homelab.cron_healthcheck_script
vars:
HEALTHCHECK_NAME: "{{ STEP_HC_RENEWAL_NAME }}"
HEALTHCHECK_TAGS: "{{ STEP_HC_RENEWAL_TAGS }}"
HEALTHCHECK_SCHEDULE_TZ: "{{ HOST_TZ }}"
HEALTHCHECK_CRON_USER: root
HEALTHCHECK_FILE_NAME: hc-renew-ssh
HEALTHCHECK_CRON_HOUR: "*/12" # Every 12 hours
HEALTHCHECK_FILE_CONTENT: |
SSH_RENEW=$(STEPPATH={{ STEP_PATH }} {{ STEP_BIN_ABSOLUTE_PATH }} ssh renew --force {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }} {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_PRIVATE_KEY }} 2>&1)
EXIT_STATUS="$?"
if [ "$EXIT_STATUS" -ne 0 ]; then
$logger "ERROR: unable to renew ssh: $SSH_RENEW"
exit "$EXIT_STATUS"
else
$logger "INFO: Renewed SSH Host Keys: {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_HOST_CERT }} {{ STEP_CERTS_PATH }}{{ STEP_CERTS_SSH_PRIVATE_KEY }}"
fi
SSHD_RESTART=$({{ SSHD_BIN_ABSOLUTE_PATH }} -t && {% if SERVICE_BIN_ABSOLUTE_PATH %}{{ SERVICE_BIN_ABSOLUTE_PATH }} ssh restart{% else %}{{ SYSTEMCTL_BIN_ABSOLUTE_PATH }} {% if (_systemd_version | int) < 229 %}reload-or-try-restart{% else %}try-reload-or-restart{% endif %} sshd{% endif %})
EXIT_STATUS="$?"
if [ "$EXIT_STATUS" -ne 0 ]; then
$logger "ERROR: unable to restart sshd: $SSHD_RESTART"
exit "$EXIT_STATUS"
else
$logger "INFO: Restarted sshd to pick up changes"
fi
- name: call healthcheck on SSH cert renew
when: _cert_ssh_needs_renewal.changed or not _cert_ssh_existing_san_match
become: true
ansible.builtin.shell: hc-renew-ssh