mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-04-28 18:30:24 +02:00
Implemented SAN via Letsencrypt and Certbot
This commit is contained in:
parent
0fc9c3e495
commit
04deeef385
@ -75,22 +75,16 @@ randomized_delay_sec: "5min"
|
||||
# Runtime Variables for Process Control
|
||||
activate_all_timers: false # Activates all timers, independend if the handlers had been triggered
|
||||
|
||||
# One Wildcard Certificate for All Subdomains
|
||||
# Enables a single Let's Encrypt wildcard certificate for all subdomains instead of individual certificates.
|
||||
# Default: false (recommended for automatic setup).
|
||||
# Setting this to true requires additional manual configuration.
|
||||
# Using a wildcard certificate can improve performance by reducing TLS handshakes.
|
||||
# To enable, update your inventory file.
|
||||
# For detailed setup instructions, visit:
|
||||
# https://github.com/kevinveenbirkenbach/cymais/tree/master/roles/nginx-docker-cert-deploy
|
||||
enable_wildcard_certificate: false
|
||||
|
||||
# This enables debugging in ansible and in the apps
|
||||
# You SHOULD NOT enable this on production servers
|
||||
enable_debug: false
|
||||
|
||||
# Which ACME method to use: webroot, cloudflare, or hetzner
|
||||
certbot_acme_challenge_method: "webroot"
|
||||
certbot_acme_challenge_method: "cloudflare"
|
||||
certbot_credentials_dir: /etc/certbot
|
||||
certbot_credentials_file: "{{ certbot_credentials_dir }}/{{ certbot_acme_challenge_method }}.ini"
|
||||
# certbot_dns_api_token # Define in inventory file
|
||||
# certbot_dns_api_token # Define in inventory file
|
||||
certbot_dns_propagation_wait_seconds: 40 # How long should the script wait for DNS propagation before continuing
|
||||
certbot_flavor: san # Possible options: san (recommended, with a dns flavor like cloudflare, or hetzner), wildcard(doesn't function with www redirect), deicated
|
||||
certbot_webroot_path: "/var/lib/letsencrypt/" # Path used by Certbot to serve HTTP-01 ACME challenges
|
||||
certbot_cert_path: "/etc/letsencrypt/live" # Path containing active certificate symlinks for domains
|
||||
|
@ -80,5 +80,4 @@ defaults_redirect_domain_mappings:
|
||||
- { source: "wordpress.{{primary_domain}}", target: "{{domains.wordpress[0]}}" }
|
||||
|
||||
# Domains which are deprecated and should be cleaned up
|
||||
deprecated_domains: []
|
||||
|
||||
deprecated_domains: []
|
157
library/find_cert_folder.py
Normal file
157
library/find_cert_folder.py
Normal file
@ -0,0 +1,157 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: find_cert_folder
|
||||
short_description: Find SSL certificate folder covering a given domain
|
||||
description:
|
||||
- Searches through certificates to find a folder that covers the given domain.
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- Domain name to search for in the certificates.
|
||||
required: true
|
||||
type: str
|
||||
certbot_flavor:
|
||||
description:
|
||||
- Certificate type. Either 'san', 'wildcard', or 'dedicated'.
|
||||
required: true
|
||||
type: str
|
||||
cert_base_path:
|
||||
description:
|
||||
- Path where certificates are stored.
|
||||
required: false
|
||||
type: str
|
||||
default: /etc/letsencrypt/live
|
||||
debug:
|
||||
description:
|
||||
- Enable verbose debug output.
|
||||
required: false
|
||||
type: bool
|
||||
default: false
|
||||
author:
|
||||
- Kevin Veen-Birkenbach
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Find cert folder for matomo.cymais.cloud
|
||||
find_cert_folder:
|
||||
domain: "matomo.cymais.cloud"
|
||||
certbot_flavor: "san"
|
||||
cert_base_path: "/etc/letsencrypt/live"
|
||||
register: result
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
folder:
|
||||
description: The name of the folder covering the domain.
|
||||
type: str
|
||||
returned: always
|
||||
'''
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
def run_openssl(cert_path):
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
['openssl', 'x509', '-in', cert_path, '-noout', '-text'],
|
||||
universal_newlines=True
|
||||
)
|
||||
return output
|
||||
except subprocess.CalledProcessError:
|
||||
return ""
|
||||
|
||||
def extract_sans(cert_text):
|
||||
dns_entries = []
|
||||
in_san = False
|
||||
for line in cert_text.splitlines():
|
||||
line = line.strip()
|
||||
if 'X509v3 Subject Alternative Name:' in line:
|
||||
in_san = True
|
||||
continue
|
||||
if in_san:
|
||||
if not line:
|
||||
break
|
||||
dns_entries += [e.strip().replace('DNS:', '') for e in line.split(',') if e.strip()]
|
||||
return dns_entries
|
||||
|
||||
def find_matching_folders(domain, cert_files, flavor, debug):
|
||||
exact_matches = []
|
||||
wildcard_matches = []
|
||||
|
||||
for cert_path in cert_files:
|
||||
cert_text = run_openssl(cert_path)
|
||||
if not cert_text:
|
||||
continue
|
||||
sans = extract_sans(cert_text)
|
||||
if debug:
|
||||
print(f"Checking {cert_path}: {sans}")
|
||||
for entry in sans:
|
||||
if entry == domain:
|
||||
exact_matches.append(os.path.dirname(cert_path))
|
||||
elif entry.startswith('*.'):
|
||||
base = entry[2:]
|
||||
if domain.endswith('.' + base):
|
||||
wildcard_matches.append(os.path.dirname(cert_path))
|
||||
|
||||
if flavor in ('san', 'dedicated'):
|
||||
return exact_matches or wildcard_matches
|
||||
elif flavor == 'wildcard':
|
||||
return wildcard_matches or exact_matches
|
||||
else:
|
||||
return []
|
||||
|
||||
def find_cert_folder(module):
|
||||
domain = module.params['domain']
|
||||
certbot_flavor = module.params['certbot_flavor']
|
||||
cert_base_path = module.params['cert_base_path']
|
||||
debug = module.params['debug']
|
||||
|
||||
cert_files = []
|
||||
for root, dirs, files in os.walk(cert_base_path):
|
||||
if 'cert.pem' in files:
|
||||
cert_files.append(os.path.join(root, 'cert.pem'))
|
||||
|
||||
if debug:
|
||||
print(f"Found {len(cert_files)} cert.pem files under {cert_base_path}")
|
||||
|
||||
preferred = find_matching_folders(domain, cert_files, certbot_flavor, debug)
|
||||
|
||||
if not preferred and certbot_flavor == 'san':
|
||||
if debug:
|
||||
print("Fallback: searching SAN matches without SAN structure parsing")
|
||||
for cert_path in cert_files:
|
||||
cert_text = run_openssl(cert_path)
|
||||
if f"DNS:{domain}" in cert_text:
|
||||
preferred.append(os.path.dirname(cert_path))
|
||||
|
||||
if not preferred:
|
||||
module.fail_json(msg=f"No certificate covering domain {domain} found.")
|
||||
|
||||
preferred = sorted(preferred, key=lambda p: (p.count('-'), len(p)))
|
||||
folder = os.path.basename(preferred[0])
|
||||
|
||||
module.exit_json(folder=folder)
|
||||
|
||||
def main():
|
||||
module_args = dict(
|
||||
domain=dict(type='str', required=True),
|
||||
certbot_flavor=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,
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
find_cert_folder(module)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -8,6 +8,7 @@
|
||||
- name: Retrieve backup-docker-to-local path from pkgmgr
|
||||
command: pkgmgr path backup-docker-to-local
|
||||
register: pkgmgr_output
|
||||
changed_when: false
|
||||
when: run_once_backup_docker_to_local is not defined
|
||||
|
||||
- name: Set fact for backup_docker_to_local_folder
|
||||
|
@ -8,6 +8,7 @@
|
||||
- name: Retrieve backup-docker-to-local path from pkgmgr
|
||||
command: pkgmgr path cleanup-failed-docker-backups
|
||||
register: pkgmgr_output
|
||||
changed_when: false
|
||||
when: run_once_cleanup_failed_docker_backups is not defined
|
||||
|
||||
- name: Set fact for backup_docker_to_local_cleanup_script
|
||||
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
- name: "include docker-compose role"
|
||||
include_role:
|
||||
name: docker-compose
|
||||
|
||||
- name: "include role nginx-domain-setup for {{application_id}}"
|
||||
include_role:
|
||||
name: nginx-domain-setup
|
||||
|
||||
- name: "include docker-compose role"
|
||||
include_role:
|
||||
name: docker-compose
|
||||
|
||||
- name: pull docker repository
|
||||
git:
|
||||
repo: "https://github.com/bigbluebutton/docker.git"
|
||||
|
@ -1,7 +1,6 @@
|
||||
ENABLE_COTURN=true
|
||||
{% set ssl_cert_folder = primary_domain if enable_wildcard_certificate | bool and primary_domain in domain else domain %}
|
||||
COTURN_TLS_CERT_PATH=/etc/letsencrypt/live/{{ssl_cert_folder}}/fullchain.pem
|
||||
COTURN_TLS_KEY_PATH=/etc/letsencrypt/live/{{ssl_cert_folder}}/privkey.pem
|
||||
COTURN_TLS_CERT_PATH={{ certbot_cert_path }}/{{ ssl_cert_folder }}/fullchain.pem
|
||||
COTURN_TLS_KEY_PATH={{ certbot_cert_path }}/{{ ssl_cert_folder }}/privkey.pem
|
||||
ENABLE_GREENLIGHT={{applications.bigbluebutton.enable_greenlight}}
|
||||
|
||||
# Enable Webhooks
|
||||
|
@ -1,9 +1,6 @@
|
||||
application_id: "ldap"
|
||||
ldaps_docker_port: 636
|
||||
ldap_docker_port: 389
|
||||
|
||||
enable_wildcard_certificate: false # Deactivate Wildcard Certificate
|
||||
|
||||
ldif_host_path: "{{docker_compose.directories.volumes}}ldif/"
|
||||
ldif_docker_path: "/tmp/ldif/"
|
||||
ldap.dn.application_roles: "ou=application_roles,{{ldap.dn.root}}"
|
||||
|
@ -5,7 +5,6 @@ database_password: "{{applications.mailu.credentials.database.password
|
||||
database_type: "mariadb"
|
||||
|
||||
cert_mount_directory: "{{docker_compose.directories.volumes}}certs/"
|
||||
enable_wildcard_certificate: false
|
||||
|
||||
# Use dedicated source for oidc if activated
|
||||
# @see https://github.com/heviat/Mailu-OIDC/tree/2024.06
|
||||
|
24
roles/letsencrypt/README.md
Normal file
24
roles/letsencrypt/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Let’s Encrypt SSL for Nginx 🔐
|
||||
|
||||
## Description
|
||||
Automates obtaining, configuring, and renewing Let’s Encrypt SSL certificates for Nginx with Certbot. Keeps your sites secure with minimal fuss! 🌐
|
||||
|
||||
## Overview
|
||||
This Ansible role sets up the necessary Nginx configuration and Certbot integration to:
|
||||
- Redirect HTTP traffic to HTTPS
|
||||
- Serve the ACME challenge for certificate issuance
|
||||
- Apply strong SSL/TLS defaults
|
||||
- Schedule automatic renewals
|
||||
|
||||
It’s idempotent: configuration and certificate tasks only run when needed. ✅
|
||||
|
||||
## Purpose
|
||||
Ensure all your Nginx-hosted sites use free, trusted SSL certificates from Let’s Encrypt—all managed automatically via Ansible. 🎯
|
||||
|
||||
## Features
|
||||
- **Automatic Certificate Issuance**: Uses Certbot’s webroot plugin to request and install certificates. 📜
|
||||
- **Nginx Redirect**: Creates a temporary HTTP → HTTPS redirect block. ↪️
|
||||
- **ACME‐Challenge Handling**: Configures `/.well-known/acme-challenge/` for Certbot validation. 🔍
|
||||
- **Secure SSL Defaults**: Includes modern cipher suites, HSTS, OCSP stapling, and session settings. 🔒
|
||||
- **Auto‐Renewal**: Leverages system scheduling (cron or systemd timer) to renew certs before expiration. 🔄
|
||||
- **One‐Time Setup**: Tasks guarded by a “run once” fact to avoid re-applying unchanged templates. 🏃♂️
|
@ -1,2 +1,26 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "An Ansible role to automate Let’s Encrypt SSL certificate issuance and renewal for Nginx"
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- letsencrypt
|
||||
- nginx
|
||||
- ssl
|
||||
- certificate
|
||||
- security
|
||||
repository: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
dependencies:
|
||||
- nginx-certbot
|
||||
- nginx-certbot
|
||||
|
@ -9,7 +9,7 @@ server
|
||||
#letsencrypt
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
root /var/lib/letsencrypt/;
|
||||
root {{ certbot_webroot_path }};
|
||||
default_type "text/plain";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
{% set ssl_cert_folder = primary_domain if enable_wildcard_certificate | bool and primary_domain in domain else domain %}
|
||||
ssl_certificate /etc/letsencrypt/live/{{ ssl_cert_folder }}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/{{ ssl_cert_folder }}/privkey.pem;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/{{ ssl_cert_folder }}/chain.pem;
|
||||
ssl_certificate {{ certbot_cert_path }}/{{ ssl_cert_folder }}/fullchain.pem;
|
||||
ssl_certificate_key {{ certbot_cert_path }}/{{ ssl_cert_folder }}/privkey.pem;
|
||||
ssl_trusted_certificate {{ certbot_cert_path }}/{{ ssl_cert_folder }}/chain.pem;
|
@ -1,19 +0,0 @@
|
||||
# Configuration Options 📋
|
||||
|
||||
## One Wildcard Certificate for All Subdomains
|
||||
|
||||
By default, each subdomain gets its own certificate. You can **enable a wildcard certificate** by setting:
|
||||
|
||||
```yaml
|
||||
enable_wildcard_certificate: true
|
||||
```
|
||||
|
||||
## Pros & Cons of a Wildcard Certificate
|
||||
### Pros
|
||||
- ✅ **Improves performance** by reducing TLS handshakes.
|
||||
- ✅ **Simplifies certificate management** (one cert for all subdomains).
|
||||
### Cons
|
||||
- ⚠ **Requires manual DNS challenge setup** for Let's Encrypt.
|
||||
- ⚠ **Needs additional configuration for automation** (see below).
|
||||
|
||||
If enabled, update your inventory file and follow the **[manual wildcard certificate setup](SETUP.md)**.
|
@ -2,21 +2,21 @@
|
||||
|
||||
# Check if the necessary parameters are provided
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <domain> <docker_compose_instance_directory>"
|
||||
echo "Usage: $0 <ssl_cert_folder> <docker_compose_instance_directory>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Assign parameters
|
||||
domain="$1"
|
||||
ssl_cert_folder="$1"
|
||||
docker_compose_instance_directory="$2"
|
||||
docker_compose_cert_directory="$docker_compose_instance_directory/volumes/certs"
|
||||
|
||||
# Copy certificates
|
||||
cp -RvL "/etc/letsencrypt/live/$domain/"* "$docker_compose_cert_directory" || exit 1
|
||||
cp -RvL "/etc/letsencrypt/live/$ssl_cert_folder/"* "$docker_compose_cert_directory" || exit 1
|
||||
|
||||
# This code is optimized for mailu
|
||||
cp -v "/etc/letsencrypt/live/$domain/privkey.pem" "$docker_compose_cert_directory/key.pem" || exit 1
|
||||
cp -v "/etc/letsencrypt/live/$domain/fullchain.pem" "$docker_compose_cert_directory/cert.pem" || exit 1
|
||||
cp -v "/etc/letsencrypt/live/$ssl_cert_folder/privkey.pem" "$docker_compose_cert_directory/key.pem" || exit 1
|
||||
cp -v "/etc/letsencrypt/live/$ssl_cert_folder/fullchain.pem" "$docker_compose_cert_directory/cert.pem" || exit 1
|
||||
|
||||
# Set correct reading rights
|
||||
chmod a+r -v "$docker_compose_cert_directory/"*
|
||||
|
@ -4,4 +4,4 @@ OnFailure=systemd-notifier.cymais@%n.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/bash {{path_administrator_scripts}}/nginx-docker-cert-deploy.sh {{domain}} {{docker_compose.directories.instance}}
|
||||
ExecStart=/usr/bin/bash {{path_administrator_scripts}}/nginx-docker-cert-deploy.sh {{ssl_cert_folder}} {{docker_compose.directories.instance}}
|
||||
|
@ -23,7 +23,12 @@
|
||||
when:
|
||||
- mode_cleanup | bool
|
||||
- run_once_nginx_domains_cleanup is not defined
|
||||
ignore_errors: true
|
||||
register: certbot_revoke_result
|
||||
failed_when: >
|
||||
certbot_revoke_result.rc != 0 and
|
||||
'No certificate found with name' not in certbot_revoke_result.stderr
|
||||
changed_when: >
|
||||
certbot_revoke_result.rc == 0
|
||||
|
||||
# The deleting just works for the base domain
|
||||
- name: "Delete Certbot certificate for {{ item }}"
|
||||
@ -36,7 +41,12 @@
|
||||
when:
|
||||
- mode_cleanup | bool
|
||||
- run_once_nginx_domains_cleanup is not defined
|
||||
ignore_errors: true
|
||||
register: certbot_delete_result
|
||||
failed_when: >
|
||||
certbot_delete_result.rc != 0 and
|
||||
'No certificate found with name' not in certbot_delete_result.stderr
|
||||
changed_when: >
|
||||
certbot_delete_result.rc == 0
|
||||
|
||||
- name: run the nginx_domains_cleanup role once
|
||||
set_fact:
|
||||
|
23
roles/nginx-https-get-cert/tasks/flavors/dedicated.yml
Normal file
23
roles/nginx-https-get-cert/tasks/flavors/dedicated.yml
Normal file
@ -0,0 +1,23 @@
|
||||
- name: "receive certificate for {{ domain }}"
|
||||
command: >-
|
||||
certbot certonly
|
||||
--agree-tos
|
||||
--email {{ users.administrator.email }}
|
||||
--non-interactive
|
||||
{% if certbot_acme_challenge_method != "webroot" %}
|
||||
--dns-{{ certbot_acme_challenge_method }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-credentials {{ certbot_credentials_file }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-propagation-seconds {{ certbot_dns_propagation_wait_seconds }}
|
||||
{% else %}
|
||||
--webroot
|
||||
-w {{ certbot_webroot_path }}
|
||||
{% endif %}
|
||||
{% if wildcard_domain is defined and ( wildcard_domain | bool ) %}
|
||||
-d {{ primary_domain }}
|
||||
-d *.{{ primary_domain }}
|
||||
{% else %}
|
||||
-d {{ domain }}
|
||||
{% endif %}
|
||||
{{ '--test-cert' if mode_test | bool else '' }}
|
||||
register: certbot_result
|
||||
changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout"
|
27
roles/nginx-https-get-cert/tasks/flavors/san.yml
Normal file
27
roles/nginx-https-get-cert/tasks/flavors/san.yml
Normal file
@ -0,0 +1,27 @@
|
||||
- name: Install certbundle
|
||||
include_role:
|
||||
name: pkgmgr-install
|
||||
vars:
|
||||
package_name: certbundle
|
||||
when: run_once_san_certs is not defined
|
||||
|
||||
- name: Generate SAN certificate with certbundle
|
||||
command: >-
|
||||
certbundle
|
||||
--domains "{{ all_domains | join(',') }}"
|
||||
--certbot-email "{{ users.administrator.email }}"
|
||||
--certbot-acme-challenge-method "{{ certbot_acme_challenge_method }}"
|
||||
{% if certbot_acme_challenge_method != 'webroot' %}
|
||||
--certbot-credentials-file "{{ certbot_credentials_file }}"
|
||||
--certbot-dns-propagation-seconds "{{ certbot_dns_propagation_wait_seconds }}"
|
||||
{% else %}
|
||||
--certbot-webroot-path "{{ certbot_webroot_path }}"
|
||||
{% endif %}
|
||||
{{ '--mode-test' if mode_test | bool else '' }}
|
||||
register: certbundle_result
|
||||
when: run_once_san_certs is not defined
|
||||
|
||||
- name: run the san tasks once
|
||||
set_fact:
|
||||
run_once_san_certs: true
|
||||
when: run_once_san_certs is not defined
|
19
roles/nginx-https-get-cert/tasks/flavors/wildcard.yml
Normal file
19
roles/nginx-https-get-cert/tasks/flavors/wildcard.yml
Normal file
@ -0,0 +1,19 @@
|
||||
- name: "Load wildcard certificate for domain"
|
||||
include_tasks: "dedicated.yml"
|
||||
vars:
|
||||
wildcard_domain: true
|
||||
when:
|
||||
- domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain)
|
||||
- run_once_receive_certificate is not defined
|
||||
|
||||
- name: "Load dedicated certificate for domain"
|
||||
include_tasks: "dedicated.yml"
|
||||
vars:
|
||||
wildcard_domain: false
|
||||
when:
|
||||
- not (domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain))
|
||||
|
||||
- name: run the receive_certificate tasks once
|
||||
set_fact:
|
||||
run_once_receive_certificate: true
|
||||
when: run_once_receive_certificate is not defined
|
@ -1,55 +1,5 @@
|
||||
- name: "receive dedicated certificate for {{ domain }}"
|
||||
command: >-
|
||||
certbot certonly
|
||||
--agree-tos
|
||||
--email {{ users.administrator.email }}
|
||||
--non-interactive
|
||||
{% if certbot_acme_challenge_method != "webroot" %}
|
||||
--dns-{{ certbot_acme_challenge_method }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-credentials {{ certbot_credentials_file }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-propagation-seconds 60
|
||||
{% else %}
|
||||
--webroot
|
||||
-w /var/lib/letsencrypt/
|
||||
{% endif %}
|
||||
-d {{ domain }}
|
||||
{{ '--test-cert' if mode_test | bool else '' }}
|
||||
register: certbot_result
|
||||
changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout"
|
||||
when:
|
||||
- not enable_wildcard_certificate | bool or not (domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain))
|
||||
# Wildcard certificate should not be used
|
||||
# OR: The domain is not a first-level subdomain of the primary domain
|
||||
|
||||
# The following should not work, checkout the Setup.md instructions.
|
||||
# @see https://chatgpt.com/share/67efa9f0-1cdc-800f-8bce-62b00fc3e6a2
|
||||
- name: "receive wildcard certificate *{{ primary_domain }} for {{domain}}"
|
||||
command: >-
|
||||
certbot certonly
|
||||
--agree-tos
|
||||
--email {{ users.administrator.email }}
|
||||
--non-interactive
|
||||
{% if certbot_acme_challenge_method != "webroot" %}
|
||||
--dns-{{ certbot_acme_challenge_method }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-credentials {{ certbot_credentials_file }}
|
||||
--dns-{{ certbot_acme_challenge_method }}-propagation-seconds 60
|
||||
{% else %}
|
||||
--webroot
|
||||
-w /var/lib/letsencrypt/
|
||||
{% endif %}
|
||||
-d {{ primary_domain }}
|
||||
-d *.{{ primary_domain }}
|
||||
{{ '--test-cert' if mode_test | bool else '' }}
|
||||
register: certbot_result
|
||||
changed_when: "'Certificate not yet due for renewal' not in certbot_result.stdout"
|
||||
when:
|
||||
- enable_wildcard_certificate | bool
|
||||
# Wildcard certificate is enabled
|
||||
- domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain)
|
||||
# AND: The domain is a direct first-level subdomain of the primary domain
|
||||
- run_once_receive_certificate is not defined
|
||||
# Ensure this task runs only once for the wildcard certificate
|
||||
ignore_errors: true
|
||||
- name: "Include flavor"
|
||||
include_tasks: "{{ role_path }}/tasks/flavors/{{ certbot_flavor }}.yml"
|
||||
|
||||
- name: "Cleanup dedicated cert for {{ domain }}"
|
||||
command: >-
|
||||
@ -57,7 +7,7 @@
|
||||
when:
|
||||
- mode_cleanup | bool
|
||||
# Cleanup mode is enabled
|
||||
- enable_wildcard_certificate | bool
|
||||
- certbot_flavor != 'dedicated'
|
||||
# Wildcard certificate is enabled
|
||||
- domain.split('.') | length == (primary_domain.split('.') | length + 1) and domain.endswith(primary_domain)
|
||||
# AND: The domain is a direct first-level subdomain of the primary domain
|
||||
@ -67,7 +17,15 @@
|
||||
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: run the receive_certificate tasks once
|
||||
- name: Find SSL cert folder for domain
|
||||
find_cert_folder:
|
||||
domain: "{{ domain }}"
|
||||
certbot_flavor: "{{ certbot_flavor }}"
|
||||
cert_base_path: "{{ certbot_cert_path }}"
|
||||
debug: "{{ enable_debug | default(false) }}"
|
||||
register: cert_folder_result
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
|
||||
- name: Set fact
|
||||
set_fact:
|
||||
run_once_receive_certificate: true
|
||||
when: run_once_receive_certificate is not defined
|
||||
ssl_cert_folder: "{{ cert_folder_result.folder }}"
|
@ -1,22 +1,24 @@
|
||||
# nginx-redirect-www
|
||||
# Nginx WWW Redirect 🌐
|
||||
|
||||
## Description
|
||||
Automates the creation of Nginx server blocks that redirect all `www.` subdomains to their non-www equivalents. Simple, idempotent, and SEO-friendly! 🚀
|
||||
|
||||
## Overview
|
||||
The `nginx-redirect-www` role is designed to automate the process of setting up redirects from `www.domain.tld` to `domain.tld` for all domains and subdomains configured within the `{{nginx.directories.http.servers}}` directory. This role dynamically identifies configuration files following the pattern `*domain.tld.conf` and creates corresponding redirection rules.
|
||||
This role will:
|
||||
- **Discover** existing `*.conf` vhosts in your Nginx servers directory
|
||||
- **Filter** domains with or without your `primary_domain`
|
||||
- **Generate** redirect rules via the `nginx-redirect-domain` role
|
||||
- **Optionally** include a wildcard redirect template (experimental) ⭐️
|
||||
- **Clean up** leftover configs when running in cleanup mode 🧹
|
||||
|
||||
## Role Description
|
||||
This role performs several key tasks:
|
||||
1. **Find Configuration Files**: Locates all `.conf` files in the `{{nginx.directories.http.servers}}` directory that match the `*.*.conf` pattern, ensuring that only domain and subdomain configurations are selected.
|
||||
|
||||
2. **Filter Domain Names**: Processes each configuration file, extracting the domain names and removing both the `.conf` extension and the `{{nginx.directories.http.servers}}` path.
|
||||
All tasks are guarded by “run once” facts and `mode_cleanup` flags to avoid unintended re-runs or stale files.
|
||||
|
||||
3. **Prepare Redirect Domain Mappings**: Transforms the filtered domain names into a source-target mapping format, where `source` is `www.domain.tld` and `target` is `domain.tld`.
|
||||
## Purpose
|
||||
Ensure that any request to `www.example.com` automatically and permanently redirects to `https://example.com`, improving user experience, SEO, and certificate management. 🎯
|
||||
|
||||
4. **Include nginx-redirect-domain Role**: Applies the redirection configuration using the `nginx-redirect-domain` role with the dynamically generated domain mappings.
|
||||
|
||||
## Notes
|
||||
- This role is designed to work in environments where domain and subdomain configurations follow the naming pattern `*domain.tld.conf`.
|
||||
- It automatically excludes any configurations that begin with `www.`, preventing duplicate redirects.
|
||||
|
||||
---
|
||||
|
||||
This `nginx-redirect-www` role was crafted by [Kevin Veen-Birkenbach](https://www.veen.world) with insights and guidance provided by ChatGPT, an advanced AI language model from OpenAI. The development process, including the discussions with ChatGPT that shaped this role, can be [here](https://chat.openai.com/share/a68e3574-f543-467d-aea7-0895f0e00bbb) explored in detail.
|
||||
## Features
|
||||
- **Auto-Discovery**: Scans your Nginx `servers` directory for `.conf` files. 🔍
|
||||
- **Dynamic Redirects**: Builds `source: "www.domain"` → `target: "domain"` mappings on the fly. 🔧
|
||||
- **Wildcard Redirect**: Includes a templated wildcard server block for `www.*` domains (toggleable). ✨
|
||||
- **Cleanup Mode**: Removes the wildcard config file when `certbot_flavor` is set to `dedicated` and `mode_cleanup` is enabled. 🗑️
|
||||
- **Debug Output**: Optional `enable_debug` gives detailed variable dumps for troubleshooting. 🐛
|
||||
|
@ -1,2 +1,26 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "An Ansible role to redirect www subdomains to non-www domains in Nginx"
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- redirect
|
||||
- www
|
||||
- wildcard
|
||||
- seo
|
||||
repository: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
dependencies:
|
||||
- nginx
|
||||
- nginx
|
@ -1,87 +1,23 @@
|
||||
---
|
||||
- name: Find all .conf
|
||||
ansible.builtin.find:
|
||||
paths: "{{nginx.directories.http.servers}}"
|
||||
patterns: '*.*.conf'
|
||||
register: conf_files
|
||||
|
||||
# Filter all domains
|
||||
|
||||
- name: Filter domain names and remove .conf extension and path
|
||||
# 1. Filter all domains with the “www.” prefix
|
||||
- name: Filter www-prefixed domains from all_domains
|
||||
set_fact:
|
||||
filtered_domains: "{{ conf_files.files | map(attribute='path') | map('regex_search', domain_regex) | select('string') | map('regex_replace', path_regex, '') | map('regex_replace', '.conf$', '') | list }}"
|
||||
vars:
|
||||
domain_regex: "^{{nginx.directories.http.servers}}(?!www\\.)[^/]+\\.conf$"
|
||||
path_regex: "^{{nginx.directories.http.servers}}"
|
||||
www_domains: "{{ all_domains | select('match', '^www\\.') | list }}"
|
||||
|
||||
# Routine for domains with primary domain included
|
||||
|
||||
- name: Set filtered_domains_with_primary_domain
|
||||
# 2. Build redirect mappings (www.domain → domain)
|
||||
- name: Build redirect mappings for www domains
|
||||
set_fact:
|
||||
filtered_domains_with_primary_domain: "{{ filtered_domains | select('search', primary_domain + '$') | list }}"
|
||||
domain_mappings: >-
|
||||
{{ www_domains
|
||||
| map('regex_replace', '^www\\.(.+)$', '{ source: \"www.\\1\", target: \"\\1\" }')
|
||||
| map('from_yaml')
|
||||
| list
|
||||
}}
|
||||
|
||||
- name: Include nginx-redirect-domain role with dynamic domain mappings for domains with {{primary_domain}} included
|
||||
# 3. Include the nginx-redirect-domain role to apply these mappings
|
||||
- name: Include nginx-redirect-domain role for www-to-bare redirects
|
||||
include_role:
|
||||
name: nginx-redirect-domain
|
||||
vars:
|
||||
domain_mappings: "{{ filtered_domains_with_primary_domain | map('regex_replace', '^(.*)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}"
|
||||
when: not enable_wildcard_certificate | bool
|
||||
|
||||
- name: Include wildcard www. redirect for domains with {{primary_domain}} included
|
||||
vars:
|
||||
domain: "{{primary_domain}}"
|
||||
template:
|
||||
src: www.wildcard.conf.j2
|
||||
dest: "{{nginx_www_wildcard_configuration}}"
|
||||
notify: restart nginx
|
||||
when: enable_wildcard_certificate | bool
|
||||
|
||||
# Routine for domains without the primary domain included
|
||||
|
||||
- name: Set filtered_domains_without_primary_domain
|
||||
set_fact:
|
||||
filtered_domains_without_primary_domain: "{{ filtered_domains | reject('search', primary_domain + '$') | list }}"
|
||||
|
||||
- name: "Relevant variables for role: {{ role_path | basename }}"
|
||||
debug:
|
||||
msg:
|
||||
filtered_domains_with_primary_domain: "{{filtered_domains_with_primary_domain}}"
|
||||
filtered_domains: "{{filtered_domains}}"
|
||||
filtered_domains_without_primary_domain: "{{filtered_domains_without_primary_domain}}"
|
||||
when: enable_debug | bool
|
||||
|
||||
- name: Include nginx-redirect-domain role with dynamic domain mappings for domains without primary domain
|
||||
include_role:
|
||||
name: nginx-redirect-domain
|
||||
vars:
|
||||
domain_mappings: "{{ filtered_domains_without_primary_domain | map('regex_replace', '^(.*)$', '{ source: \"www.\\1\", target: \"\\1\" }') | map('from_yaml') | list }}"
|
||||
|
||||
|
||||
# Cleanup
|
||||
# Deactivated due to complexity
|
||||
#- name: Cleanup dedicated nginx configurations for www redirect configuration
|
||||
# file:
|
||||
# path: "{{ nginx.directories.http.servers }}{{ item.source }}.conf"
|
||||
# state: absent
|
||||
# # Filter: Only first-level subdomains of primary_domain
|
||||
# # Exclude the primary domain itself
|
||||
# # Transform for www redirection
|
||||
# loop: "{{ filtered_domains_with_primary_domain
|
||||
# | select('regex_search', '^[^.]+\\.' ~ primary_domain ~ '$')
|
||||
# | reject('equalto', primary_domain)
|
||||
# | map('regex_replace', '^(.*)$', '{ source: \"www.\\1\", target: \"\\1\" }')
|
||||
# | map('from_yaml')
|
||||
# | list }}"
|
||||
# notify: restart nginx
|
||||
# when:
|
||||
# - enable_wildcard_certificate | bool # Wildcard certificate must be enabled
|
||||
# - mode_cleanup | bool # Cleanup mode must be enabled
|
||||
|
||||
- name: Cleanup {{nginx_www_wildcard_configuration}}
|
||||
file:
|
||||
path: "{{nginx_www_wildcard_configuration}}"
|
||||
state: absent
|
||||
notify: restart nginx
|
||||
when:
|
||||
- not enable_wildcard_certificate | bool
|
||||
- mode_cleanup | bool
|
||||
domain_mappings: "{{ domain_mappings }}"
|
||||
when: certbot_flavor == 'dedicated'
|
||||
|
@ -1,6 +0,0 @@
|
||||
server {
|
||||
server_name ~^www\.(?<domain>.+)$;
|
||||
{% include 'roles/letsencrypt/templates/ssl_header.j2' %}
|
||||
|
||||
return 301 https://$domain$request_uri;
|
||||
}
|
@ -1 +0,0 @@
|
||||
nginx_www_wildcard_configuration: "{{nginx.directories.http.global}}www.wildcard.conf"
|
@ -9,7 +9,10 @@
|
||||
source ~/.venvs/pkgmgr/bin/activate
|
||||
pkgmgr update {{ package_name }} --dependencies --clone-mode https
|
||||
notify: "{{ package_notify | default(omit) }}"
|
||||
|
||||
register: pkgmgr_update_result
|
||||
changed_when: "'No command defined and neither main.sh nor main.py found' not in pkgmgr_update_result.stdout"
|
||||
failed_when: pkgmgr_update_result.rc != 0 and 'No command defined and neither main.sh nor main.py found' not in pkgmgr_update_result.stdout
|
||||
|
||||
- name: mark pkgmgr update as done
|
||||
set_fact:
|
||||
run_once_pkgmgr_update: true
|
||||
|
@ -62,15 +62,32 @@
|
||||
set_fact:
|
||||
service_provider: "{{ defaults_service_provider | combine(service_provider | default({}, true), recursive=True) }}"
|
||||
|
||||
- name: Collect all domains (domains, redirect sources + www)
|
||||
set_fact:
|
||||
all_domains: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
domains.values() | flatten
|
||||
+ (redirect_domain_mappings | map(attribute='source') | list)
|
||||
)
|
||||
+ (
|
||||
domains.values() | flatten
|
||||
+ (redirect_domain_mappings | map(attribute='source') | list)
|
||||
) | map('regex_replace', '^(.*)$', 'www.\\1') | list
|
||||
) | unique | sort
|
||||
}}
|
||||
|
||||
- name: "Merged Variables"
|
||||
# Add new merged variables here
|
||||
debug:
|
||||
msg:
|
||||
domains: "{{domains}}"
|
||||
applications: "{{applications}}"
|
||||
oidc: "{{oidc}}"
|
||||
service_provider: "{{service_provider}}"
|
||||
users: "{{users}}"
|
||||
domains: "{{ domains }}"
|
||||
applications: "{{ applications }}"
|
||||
oidc: "{{ oidc }}"
|
||||
service_provider: "{{ service_provider }}"
|
||||
users: "{{ users }}"
|
||||
all_domains: "{{ all_domains }}"
|
||||
when: enable_debug | bool
|
||||
|
||||
- name: init root user
|
||||
|
Loading…
x
Reference in New Issue
Block a user