refactor(cert-deploy): make cert deploy script TLS-source agnostic

- Remove cert_folder_find module (no longer needed)
- Update deploy script to accept a cert source directory (not LETSENCRYPT_LIVE_PATH)
- Derive cert source dir via cert_plan in systemd unit
- Keep Mailu key.pem/cert.pem mapping with LE/selfsigned fallback
- Minor shell hardening (set -eu) and wording fixes

https://chatgpt.com/share/697cb143-e68c-800f-9823-c78a8d833d4e
This commit is contained in:
kevinveenbirkenbach committed 2026-01-30 14:25:19 +01:00
1 parent 1b4bbb24a2
commit 16023dbc9b
4 files changed
+68 -96

No files matched your search

-29
View File
@@ -1,29 +0,0 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.cert_utils import CertUtils
def main():
module_args = dict(
domain=dict(type="str", required=True),
cert_base_path=dict(
type="str", required=False, default="/etc/letsencrypt/live"
),
debug=dict(type="bool", required=False, default=False),
)
module = AnsibleModule(argument_spec=module_args)
domain = module.params["domain"]
cert_base_path = module.params["cert_base_path"]
debug = module.params["debug"]
folder = CertUtils.find_cert_for_domain(domain, cert_base_path, debug)
if folder is None:
module.fail_json(msg=f"No certificate covering domain {domain} found.")
else:
module.exit_json(folder=folder)
if __name__ == "__main__":
main()
+58 -45
View File
@@ -1,29 +1,45 @@
#!/bin/sh
set -eu
# Check if the necessary parameters are provided
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <ssl_cert_folder> <docker_compose_instance_directory> <letsencrypt_live_path>"
exit 1
# Usage: script.sh <ssl_cert_source_dir> <docker_compose_instance_directory>
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <ssl_cert_source_dir> <docker_compose_instance_directory>" >&2
exit 1
fi
# Assign parameters
ssl_cert_folder="$1"
ssl_cert_source_dir="$1"
docker_compose_instance_directory="$2"
letsencrypt_live_path="$3"
# Keep your existing target layout (minimal change)
docker_compose_cert_directory="${docker_compose_instance_directory}volumes/certs"
if [ ! -d "$ssl_cert_source_dir" ]; then
echo "ERROR: ssl_cert_source_dir does not exist or is not a directory: $ssl_cert_source_dir" >&2
exit 1
fi
# Ensure the target cert directory exists
if [ ! -d "$docker_compose_cert_directory" ]; then
echo "Creating certs directory: $docker_compose_cert_directory"
mkdir -p "$docker_compose_cert_directory" || exit 1
echo "Creating certs directory: $docker_compose_cert_directory"
mkdir -p "$docker_compose_cert_directory"
fi
# Copy all certificates (generic)
cp -RvL "${letsencrypt_live_path}/${ssl_cert_folder}/"* "$docker_compose_cert_directory" || exit 1
echo "Copying certificates from: $ssl_cert_source_dir -> $docker_compose_cert_directory"
cp -RvL "${ssl_cert_source_dir}/"* "$docker_compose_cert_directory"
# Mailu optimization: explicit key/cert mapping
cp -v "${letsencrypt_live_path}/${ssl_cert_folder}/privkey.pem" "${docker_compose_cert_directory}/key.pem" || exit 1
cp -v "${letsencrypt_live_path}/${ssl_cert_folder}/fullchain.pem" "${docker_compose_cert_directory}/cert.pem" || exit 1
# Mailu optimization: create key.pem/cert.pem from whatever exists
# Prefer LE naming if present
if [ -f "${ssl_cert_source_dir}/privkey.pem" ] && [ -f "${ssl_cert_source_dir}/fullchain.pem" ]; then
cp -v "${ssl_cert_source_dir}/privkey.pem" "${docker_compose_cert_directory}/key.pem"
cp -v "${ssl_cert_source_dir}/fullchain.pem" "${docker_compose_cert_directory}/cert.pem"
elif [ -f "${ssl_cert_source_dir}/key.pem" ] && [ -f "${ssl_cert_source_dir}/cert.pem" ]; then
cp -v "${ssl_cert_source_dir}/key.pem" "${docker_compose_cert_directory}/key.pem"
cp -v "${ssl_cert_source_dir}/cert.pem" "${docker_compose_cert_directory}/cert.pem"
else
echo "ERROR: Could not determine key/cert mapping for Mailu." >&2
echo "Looked for: privkey.pem+fullchain.pem OR key.pem+cert.pem in: $ssl_cert_source_dir" >&2
exit 1
fi
# Set correct reading rights
chmod a+r -v "${docker_compose_cert_directory}/"* || exit 1
@@ -36,45 +52,42 @@ failed_services=""
# Reload Nginx in all containers within the Docker Compose setup
cd "$docker_compose_instance_directory" || exit 1
echo "Wait for 5 minutes to prevent interuption of setup procedures"
echo "Wait for 5 minutes to prevent interruption of setup procedures"
sleep 300
# Iterate over all services
for service in $(docker compose ps --services); do
echo "Checking service: $service"
echo "Checking service: $service"
# Check if Nginx exists in the container
if docker compose exec -T "$service" which nginx > /dev/null 2>&1; then
echo "Testing Nginx config for service: $service"
if ! docker compose exec -T "$service" nginx -t; then
echo "Nginx config test FAILED for service: $service" >&2
nginx_reload_failed=true
failed_services="$failed_services $service"
continue
fi
echo "Reloading Nginx for service: $service"
if docker compose exec -T "$service" nginx -s reload; then
nginx_reload_successful=true
echo "Successfully reloaded Nginx for service: $service"
else
echo "Failed to reload Nginx for service: $service" >&2
nginx_reload_failed=true
failed_services="$failed_services $service"
fi
else
echo "Nginx not found in service: $service, skipping."
if docker compose exec -T "$service" which nginx > /dev/null 2>&1; then
echo "Testing Nginx config for service: $service"
if ! docker compose exec -T "$service" nginx -t; then
echo "Nginx config test FAILED for service: $service" >&2
nginx_reload_failed=true
failed_services="$failed_services $service"
continue
fi
echo "Reloading Nginx for service: $service"
if docker compose exec -T "$service" nginx -s reload; then
nginx_reload_successful=true
echo "Successfully reloaded Nginx for service: $service"
else
echo "Failed to reload Nginx for service: $service" >&2
nginx_reload_failed=true
failed_services="$failed_services $service"
fi
else
echo "Nginx not found in service: $service, skipping."
fi
done
# Optional auto-healing: restart only the services whose reload failed
if [ "$nginx_reload_failed" = true ]; then
echo "At least one Nginx reload failed. Affected services:${failed_services}"
echo "Restarting affected services to apply the new certificates..."
# shellcheck disable=SC2086
(sleep 120 && docker compose restart $failed_services) || (sleep 120 && docker compose restart) || exit 1
echo "At least one Nginx reload failed. Affected services:${failed_services}"
echo "Restarting affected services to apply the new certificates..."
# shellcheck disable=SC2086
(sleep 120 && docker compose restart $failed_services) || (sleep 120 && docker compose restart) || exit 1
elif [ "$nginx_reload_successful" = true ]; then
echo "At least one Nginx reload was successful. No restart needed."
echo "At least one Nginx reload was successful. No restart needed."
else
echo "No Nginx instances found in any service. Nothing to reload."
echo "No Nginx instances found in any service. Nothing to reload."
fi
@@ -1,7 +1,9 @@
[Unit]
Description=Let's Encrypt deploy to {{ docker_compose.directories.instance }}
Description=TLS cert deploy to {{ docker_compose.directories.instance }}
OnFailure={{ SYS_SERVICE_ON_FAILURE_COMPOSE }}
[Service]
Type=oneshot
ExecStart={{ system_service_script_exec }} {{ ssl_cert_folder }} {{ docker_compose.directories.instance }} {{ LETSENCRYPT_LIVE_PATH }}
ExecStart={{ system_service_script_exec }} \
{{ lookup('cert_plan', domain, want='files.cert') | dirname }} \
{{ docker_compose.directories.instance }}
+6 -20
View File
@@ -1,3 +1,4 @@
---
- block:
- name: Include dependency 'sys-svc-webserver-https'
include_role:
@@ -12,34 +13,19 @@
#- name: "Cleanup dedicated cert for '{{ domain }}'"
# command: >-
# certbot delete --cert-name {{ domain }} --non-interactive
# when:
# when:
# - MODE_CLEANUP | bool
# # Cleanup mode is enabled
# - CERTBOT_FLAVOR != 'dedicated'
# # Wildcard certificate is enabled
# - domain.split('.') | length == (DOMAIN_PRIMARY.split('.') | length + 1) and domain.endswith(DOMAIN_PRIMARY)
# # AND: The domain is a direct first-level subdomain of the primary domain
# - domain != DOMAIN_PRIMARY
# - domain != DOMAIN_PRIMARY
# # The domain is not the primary domain
# register: certbot_result
# failed_when: certbot_result.rc != 0 and ("No certificate found with name" not in certbot_result.stderr)
# changed_when: certbot_result.rc == 0 and ("No certificate found with name" not in certbot_result.stderr)
- name: "Find SSL cert folder for '{{ domain }}'"
cert_folder_find:
domain: "{{ domain }}"
cert_base_path: "{{ LETSENCRYPT_LIVE_PATH }}"
debug: "{{ MODE_DEBUG | bool }}"
register: cert_folder_result
delegate_to: "{{ inventory_hostname }}"
changed_when: false
- name: "Set ssl_cert_folder fact to '{{ cert_folder_result.folder }}'"
set_fact:
ssl_cert_folder: "{{ cert_folder_result.folder }}"
changed_when: false
- name: "Ensure ssl_cert_folder is set for domain {{ domain }}"
fail:
msg: "No certificate folder found for domain {{ domain }}"
when: ssl_cert_folder is undefined or ssl_cert_folder is none
# NOTE (minimal invasive change):
# ssl_cert_folder is now derived from tls_resolve so it works for both
# letsencrypt and self_signed.