v0.01
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode
|
||||
48
defaults/main.yml
Normal file
48
defaults/main.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# Certbot
|
||||
#
|
||||
# Ansible Role
|
||||
# Linux-Server-Admin.com
|
||||
|
||||
#
|
||||
# Default Variables for Certbot
|
||||
#
|
||||
|
||||
certbot_admin_email: root@localhost
|
||||
certbot_python: python3-certbot-nginx # python3-certbot-apache
|
||||
certbot_debug: false
|
||||
|
||||
certbot_vhosts: []
|
||||
# Example:
|
||||
# certbot_vhosts:
|
||||
# name: "domain.com"
|
||||
# documentroot: "/var/www/"
|
||||
# alias:
|
||||
# - ""
|
||||
|
||||
# Install certbot packages
|
||||
# certbot_install: true
|
||||
|
||||
# certbot_temp_dir: "/tmp/letsencrypt/"
|
||||
|
||||
#
|
||||
# Optional vars
|
||||
#
|
||||
|
||||
# Webserver
|
||||
# certbot_webserver: not defined by default # apache2 (or httpd for CentOS) or nginx
|
||||
# certbot_webserver_plugin_install: true
|
||||
|
||||
# FreeIPA Integration
|
||||
# certbot_freeipa: false
|
||||
# FreeIPA Domain for WebGUI, default is inventory_hostname
|
||||
# certbot_ipa_domain: "{{inventory_hostname}}"
|
||||
|
||||
|
||||
# Readme & Host Documentation
|
||||
# certbot_readme: false
|
||||
# certbot_readme_mode: "0640"
|
||||
# certbot_readme_path: "/etc/ansible/readme/"
|
||||
|
||||
# Certbot set facts
|
||||
# certbot_facts: false
|
||||
54
tasks/facts.yml
Normal file
54
tasks/facts.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
# Certbot
|
||||
#
|
||||
# Linux-Server-Admin.com Ansible Role for cert management with Certbot
|
||||
#
|
||||
# Fact Tasks
|
||||
#
|
||||
|
||||
- name: Install python3 and pip (inkl. venv)
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-psutil
|
||||
- python3-venv
|
||||
state: latest
|
||||
update_cache: true
|
||||
become: true
|
||||
|
||||
- name: Create python venv for facts
|
||||
ansible.builtin.command:
|
||||
cmd: python3 -m venv /opt/ansible-facts-venv
|
||||
become: true
|
||||
args:
|
||||
creates: /opt/ansible-facts-venv
|
||||
|
||||
- name: Install pyyaml in venv
|
||||
ansible.builtin.command:
|
||||
cmd: /opt/ansible-facts-venv/bin/pip install pyyaml
|
||||
become: true
|
||||
|
||||
- name: "Create certbot parse facts script"
|
||||
ansible.builtin.template:
|
||||
src: "certbot-certificates.py.j2"
|
||||
dest: "/usr/local/bin/ansible_certbot_parse_facts.py"
|
||||
mode: +x
|
||||
become: true
|
||||
|
||||
- name: "Create directory for ansible system facts"
|
||||
ansible.builtin.file:
|
||||
state: directory
|
||||
recurse: true
|
||||
path: /etc/ansible/facts.d
|
||||
become: true
|
||||
|
||||
- name: "Set certbot fact file"
|
||||
ansible.builtin.template:
|
||||
src: "certbot.fact.j2"
|
||||
dest: "/etc/ansible/facts.d/certbot.json"
|
||||
become: true
|
||||
|
||||
- name: Run certbot parse script in venv
|
||||
ansible.builtin.shell: certbot certificates | /opt/ansible-facts-venv/bin/python3 /usr/local/bin/ansible_certbot_parse_facts.py
|
||||
become: true
|
||||
32
tasks/install.yml
Normal file
32
tasks/install.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# Certbot
|
||||
#
|
||||
# Linux-Server-Admin.com Ansible Role for cert management with Certbot
|
||||
#
|
||||
# Install Tasks
|
||||
#
|
||||
|
||||
- name: Install EPEL Release
|
||||
ansible.builtin.package:
|
||||
name: "epel-release"
|
||||
state: latest
|
||||
update_cache: true
|
||||
when: ansible_facts["os_family"] == "RedHat"
|
||||
become: true
|
||||
|
||||
- name: Install Certbot
|
||||
ansible.builtin.package:
|
||||
name: "certbot"
|
||||
state: latest
|
||||
update_cache: true
|
||||
become: true
|
||||
|
||||
- name: Install Certbot's Nginx/Apache package
|
||||
ansible.builtin.package:
|
||||
name: "{{ certbot_python }}"
|
||||
state: latest
|
||||
when:
|
||||
- not certbot_freeipa | default(false) | bool
|
||||
- certbot_webserver is defined
|
||||
- certbot_webserver_plugin_install | default(true) | bool
|
||||
become: true
|
||||
80
tasks/main.yml
Normal file
80
tasks/main.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
# Certbot
|
||||
#
|
||||
# Linux-Server-Admin.com Ansible Role for cert management with Certbot
|
||||
#
|
||||
# Main Tasks
|
||||
#
|
||||
|
||||
- name: "Check if certbot_debug is defined and true and if set debug_nolog to false for all sensitive tasks"
|
||||
set_fact:
|
||||
debug_nolog: false
|
||||
when: certbot_debug is defined and certbot_debug is true
|
||||
|
||||
- name: "Install Certbot"
|
||||
include_tasks: install.yml
|
||||
when: certbot_install | default(true) | bool
|
||||
|
||||
- shell: "certbot --version"
|
||||
register: __certbot_version
|
||||
|
||||
- debug:
|
||||
var: __certbot_version
|
||||
when: certbot_debug is defined and certbot_debug is true
|
||||
|
||||
- name: Check Webserver
|
||||
debug:
|
||||
msg: "Selected Webserver: {{ certbot_webserver }}"
|
||||
when: certbot_webserver is defined and certbot_debug is defined and certbot_debug is true
|
||||
|
||||
- name: "Check if certificate already exists"
|
||||
ansible.builtin.stat:
|
||||
path: /etc/letsencrypt/live/{{ item.name }}/cert.pem
|
||||
register: certbot_vhosts_host
|
||||
with_items: "{{ certbot_vhosts }}"
|
||||
become: true
|
||||
|
||||
- name: "Generate certificate scripts"
|
||||
ansible.builtin.template:
|
||||
src: "generate-cert.sh.j2"
|
||||
dest: "/usr/local/bin/certbot-{{ item.item.name }}.sh"
|
||||
mode: +x
|
||||
with_items: "{{ certbot_vhosts_host.results }}"
|
||||
become: true
|
||||
# no_log: debug_nolog | default(true) | bool
|
||||
|
||||
- name: "Exec cert script"
|
||||
ansible.builtin.shell: '/usr/local/bin/certbot-{{ item.item.name }}.sh'
|
||||
with_items: "{{ certbot_vhosts_host.results }}"
|
||||
become: true
|
||||
# no_log: debug_nolog | default(true) | bool
|
||||
|
||||
# list all installed certificates
|
||||
- name: "List all installed certificates"
|
||||
ansible.builtin.command:
|
||||
cmd: "certbot certificates"
|
||||
register: __certbot_certificates
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
become: true
|
||||
# when: certbot_debug is defined and certbot_debug is true
|
||||
|
||||
- debug:
|
||||
var: __certbot_certificates.stdout_lines
|
||||
when: certbot_debug is defined and certbot_debug is true
|
||||
|
||||
- name: "Generate LetsEncrypt FreeIPA Integration script"
|
||||
ansible.builtin.template:
|
||||
src: "letsencrypt-freeipa.sh.j2"
|
||||
dest: "/usr/local/bin/letsencrypt-freeipa.sh"
|
||||
mode: +x
|
||||
when: certbot_freeipa | default(false) | bool
|
||||
become: true
|
||||
|
||||
- name: "Setup Certbot facts"
|
||||
include_tasks: facts.yml
|
||||
when: certbot_facts | default(false) | bool
|
||||
|
||||
- name: "Setup Certbot readme"
|
||||
include_tasks: readme.yml
|
||||
when: certbot_readme | default(false) | bool
|
||||
21
tasks/readme.yml
Normal file
21
tasks/readme.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
# Certbot
|
||||
#
|
||||
# Linux-Server-Admin.com Ansible Role for cert management with Certbot
|
||||
#
|
||||
# Readme Tasks
|
||||
#
|
||||
|
||||
- name: "Create Readme Directory"
|
||||
ansible.builtin.file:
|
||||
path: "{{ certbot_readme_path | default('/etc/ansible/readme/') }}"
|
||||
state: directory
|
||||
mode: "{{ certbot_readme_mode | default('0640') }}"
|
||||
become: true
|
||||
|
||||
- name: "Update Readme"
|
||||
ansible.builtin.template:
|
||||
src: "certbot.md.j2"
|
||||
dest: "{{ certbot_readme_path | default('/etc/ansible/readme/') }}certbot.md"
|
||||
mode: "{{ certbot_readme_mode | default('0640') }}"
|
||||
become: true
|
||||
43
templates/certbot-certificates.py.j2
Normal file
43
templates/certbot-certificates.py.j2
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# filepath: /usr/local/bin/certbot_parse_facts.py
|
||||
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
def parse_certbot_output(lines):
|
||||
certs = []
|
||||
cert = {}
|
||||
for line in lines:
|
||||
if line.startswith(" Certificate Name:"):
|
||||
if cert:
|
||||
certs.append(cert)
|
||||
cert = {}
|
||||
cert["name"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Serial Number:"):
|
||||
cert["serial"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Key Type:"):
|
||||
cert["key_type"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Domains:"):
|
||||
cert["domains"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Expiry Date:"):
|
||||
cert["expiry"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Certificate Path:"):
|
||||
cert["cert_path"] = line.split(":", 1)[1].strip()
|
||||
elif line.strip().startswith("Private Key Path:"):
|
||||
cert["key_path"] = line.split(":", 1)[1].strip()
|
||||
if cert:
|
||||
certs.append(cert)
|
||||
return {"certificates": certs}
|
||||
|
||||
def sort_certificates_by_name(facts):
|
||||
facts["certificates"].sort(key=lambda c: c.get("name", "").lower())
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Read lines from stdin
|
||||
lines = [line.rstrip("\n") for line in sys.stdin]
|
||||
facts = parse_certbot_output(lines)
|
||||
# Write facts but sorted by certificate name
|
||||
sort_certificates_by_name(facts)
|
||||
# Output to YAML file for Ansible facts
|
||||
with open("/etc/ansible/facts.d/certbot.certificates.yml", "w") as f:
|
||||
yaml.dump(facts, f, default_flow_style=False)
|
||||
35
templates/certbot.certificates.fact.j2
Normal file
35
templates/certbot.certificates.fact.j2
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"certificates": [
|
||||
{% set cert = {} %}
|
||||
{% for line in certbot_certificates %}
|
||||
{% if line.startswith(' Certificate Name:') %}
|
||||
{% if cert %}
|
||||
{{ cert | to_nice_json }},{% set cert = {} %}
|
||||
{% endif %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['name'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Serial Number:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['serial'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Key Type:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['key_type'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Domains:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['domains'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Expiry Date:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['expiry'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Certificate Path:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['cert_path'] = line.split(':', 1)[1].strip() %}
|
||||
{% elif line.startswith(' Private Key Path:') %}
|
||||
{% set cert = cert.copy() %}
|
||||
{% set cert['key_path'] = line.split(':', 1)[1].strip() %}
|
||||
{% endif %}
|
||||
{% if loop.last and cert %}
|
||||
{{ cert | to_nice_json }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
}
|
||||
4
templates/certbot.fact.j2
Normal file
4
templates/certbot.fact.j2
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"installed":"true",
|
||||
"version":"{{ __certbot_version | regex_replace('^.*certbot ([\\w\\.\\-]+).*?$', '\\1') }}"
|
||||
}
|
||||
43
templates/certbot.md.j2
Normal file
43
templates/certbot.md.j2
Normal file
@@ -0,0 +1,43 @@
|
||||
{{ certbot_message | default (admin_message ) | comment }}
|
||||
|
||||
# Certbot
|
||||
|
||||
Certbot {{ __certbot_version | regex_replace('^.*certbot ([\\w\\.\\-]+).*?$', '\\1') }} is installed on host {{ inventory_hostname }}
|
||||
|
||||
For all certs helper scripts (/usr/local/bin/certbot-[domainname].sh) and a cronjob (to update the certs) are installed.
|
||||
|
||||
{% if certbot_webserver is defined %}
|
||||
The Webserver "{{ certbot_webserver }}" is defined as the default one which interacts with the certbot plugin.
|
||||
{% endif %}
|
||||
|
||||
|
||||
## Certs
|
||||
|
||||
{{ __certbot_certificates.stdout }}
|
||||
|
||||
|
||||
## Facts
|
||||
|
||||
{% if certbot_facts | default(false) %}
|
||||
|
||||
Facts are saved on the host in these files:
|
||||
|
||||
- /etc/ansible/facts.d/certbot.json
|
||||
- /etc/ansible/facts.d/certbot.certificates.yml
|
||||
|
||||
|
||||
### Update facts without Ansible
|
||||
|
||||
To update the facts run:
|
||||
certbot certificates | /opt/ansible-facts-venv/bin/python3 /usr/local/bin/ansible_certbot_parse_facts.py
|
||||
|
||||
{% else %}
|
||||
Facts are not saved on the host. To enable this set certbot_facts to true in your inventory.
|
||||
{% endif %}
|
||||
|
||||
---
|
||||
|
||||
Any Questions?
|
||||
|
||||
**Linux-Server-Admin.com**
|
||||
|
||||
56
templates/generate-cert.sh.j2
Normal file
56
templates/generate-cert.sh.j2
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Create Lets Encrypt Cert for {{ item.item.name }}
|
||||
# If the Cert is already created, it will just perform a quiet "certbot renew".
|
||||
#
|
||||
# Linux-Server-Admin.com
|
||||
#
|
||||
{{ certbot_message | default (admin_message ) | comment }}
|
||||
#
|
||||
#
|
||||
# /usr/local/bin/certbot-{{ item.item.name }}.sh
|
||||
#
|
||||
|
||||
CERT="/etc/letsencrypt/live/{{ item.item.name }}/"
|
||||
|
||||
if [ ! -d "$CERT" ]; then
|
||||
{% if certbot_webserver is defined and certbot_webserver_plugin_install | default(true) | bool %}
|
||||
|
||||
echo "### Start Creating Cert {{ item.item.name }} or renew it";
|
||||
|
||||
certbot certonly --{{ certbot_webserver }} --noninteractive --agree-tos --expand \
|
||||
--email {{ certbot_admin_email | default('root@localhost') }} \
|
||||
-d {{ item.item.name }} {% if item.item.alias is defined %}\
|
||||
{% for altname in item.item.alias %} -d {{ altname }} {% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% if certbot_freeipa %}
|
||||
systemctl stop httpd
|
||||
{% endif %}
|
||||
|
||||
echo "### Start Creating Cert {{ item.item.name }} or renew it";
|
||||
|
||||
certbot certonly --standalone --noninteractive --agree-tos --expand \
|
||||
--email {{ certbot_admin_email | default('root@localhost') }} \
|
||||
-d {{ item.item.name }} {% if item.item.alias is defined %}\
|
||||
{% for altname in item.item.alias %} -d {{ altname }} {% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if certbot_freeipa %}
|
||||
systemctl start httpd
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
else
|
||||
{% if certbot_webserver is defined %}
|
||||
certbot renew --quiet --{{ certbot_webserver }}
|
||||
{% else %}
|
||||
certbot renew --quiet
|
||||
{% endif %}
|
||||
fi
|
||||
|
||||
exit
|
||||
120
templates/letsencrypt-freeipa.sh.j2
Normal file
120
templates/letsencrypt-freeipa.sh.j2
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# LetsEncrypt Integration for FreeIPA with Backup and Recovery
|
||||
#
|
||||
# Linux-Server-Admin.com
|
||||
#
|
||||
{{ certbot_message | default (admin_message ) | comment }}
|
||||
#
|
||||
# This script obtains a Let’s Encrypt certificate for FreeIPA and integrates it.
|
||||
# It also creates backups and provides instructions for recovery.
|
||||
#
|
||||
# USAGE:
|
||||
# sudo ./letsencrypt-freeipa.sh
|
||||
#
|
||||
# REQUIREMENTS:
|
||||
# - FreeIPA server with ipa-cacert-manage, ipa-certupdate, ipa-server-certinstall, and ipactl available.
|
||||
# - Certbot certificates must already be present in /etc/letsencrypt/live/$IPADOMAIN/.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
### VARIABLES ###
|
||||
IPADOMAIN="{{ certbot_ipa_domain | default(inventory_hostname) }}"
|
||||
TEMP_DIR="{{ certbot_temp_dir | default('/tmp/letsencrypt/') }}"
|
||||
ADMIN_EMAIL="{{ certbot_admin_email | default('root@localhost') }}"
|
||||
|
||||
CERTS_URL="https://letsencrypt.org/certs/"
|
||||
CERTS_BASE=("isrgrootx1.pem" "isrg-root-x2.pem")
|
||||
|
||||
CERTS_EXTRA_URL="https://letsencrypt.org/certs/2024/"
|
||||
CERTS_EXTRA=("e5.pem" "e6.pem" "r10.pem" "r11.pem")
|
||||
|
||||
CERTDIR="/etc/letsencrypt/live/$IPADOMAIN/"
|
||||
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
BACKUP_DIR="/var/lib/ipa-backups/${TIMESTAMP}"
|
||||
|
||||
### FUNCTIONS ###
|
||||
# If something fails after we start modifying certs, you may need to restore.
|
||||
restore_instructions() {
|
||||
echo "### ERROR DETECTED ###"
|
||||
echo "To restore from backups:"
|
||||
echo "- If only certs and private directories changed, restore from backups:"
|
||||
echo " cp -r /var/lib/ipa/certs.bak.${TIMESTAMP} /var/lib/ipa/certs"
|
||||
echo " cp -r /var/lib/ipa/private.bak.${TIMESTAMP} /var/lib/ipa/private"
|
||||
echo " ipa-certupdate"
|
||||
echo " ipactl restart"
|
||||
echo ""
|
||||
echo "- If the FreeIPA state is more severely disrupted, use ipa-restore:"
|
||||
echo " ipa-restore --from-backup /var/lib/ipa/backup/${TIMESTAMP}"
|
||||
echo "You will need to confirm the restore when prompted."
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap restore_instructions ERR
|
||||
|
||||
### MAIN SCRIPT ###
|
||||
|
||||
echo "### Creating working directory"
|
||||
mkdir -p "${TEMP_DIR}"
|
||||
cd "${TEMP_DIR}"
|
||||
|
||||
echo "### Creating timestamped backups for IPA certs and private keys"
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# Back up existing certs and keys
|
||||
cp -r /var/lib/ipa/certs "/var/lib/ipa/certs.bak.${TIMESTAMP}"
|
||||
cp -r /var/lib/ipa/private "/var/lib/ipa/private.bak.${TIMESTAMP}"
|
||||
|
||||
# Optional: Create a full FreeIPA backup for complete rollback if needed.
|
||||
# Note: This can be commented out if you do not want a full backup.
|
||||
echo "### Performing a full FreeIPA backup"
|
||||
ipa-backup
|
||||
|
||||
echo "### Downloading Let’s Encrypt root certificates"
|
||||
for CERT_BASE in "${CERTS_BASE[@]}"; do
|
||||
curl -fSLo "${TEMP_DIR}${CERT_BASE}" "${CERTS_URL}${CERT_BASE}"
|
||||
done
|
||||
|
||||
echo "### Downloading additional Let’s Encrypt certificates"
|
||||
for CERT_EXTRA in "${CERTS_EXTRA[@]}"; do
|
||||
curl -fSLo "${TEMP_DIR}${CERT_EXTRA}" "${CERTS_EXTRA_URL}${CERT_EXTRA}"
|
||||
done
|
||||
|
||||
echo "### Installing Root Certificates into IPA CA Store"
|
||||
for CERT_BASE in "${CERTS_BASE[@]}"; do
|
||||
ipa-cacert-manage install "${TEMP_DIR}${CERT_BASE}"
|
||||
done
|
||||
|
||||
echo "### Installing Additional Certificates into IPA CA Store"
|
||||
for CERT_EXTRA in "${CERTS_EXTRA[@]}"; do
|
||||
ipa-cacert-manage install "${TEMP_DIR}${CERT_EXTRA}"
|
||||
done
|
||||
|
||||
echo "### Updating CA certificates in IPA"
|
||||
ipa-certupdate
|
||||
|
||||
echo "### Installing Let’s Encrypt server certificates"
|
||||
# Ensure that fullchain.pem and privkey.pem exist
|
||||
if [[ ! -f "${CERTDIR}fullchain.pem" || ! -f "${CERTDIR}privkey.pem" ]]; then
|
||||
echo "ERROR: The Let's Encrypt certificates are not present in ${CERTDIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ipa-server-certinstall -w -d \
|
||||
"${CERTDIR}privkey.pem" \
|
||||
"${CERTDIR}fullchain.pem" \
|
||||
--pin=''
|
||||
|
||||
echo "### Restarting IPA services"
|
||||
ipactl restart
|
||||
|
||||
echo "### Cleanup"
|
||||
rm -rf "${TEMP_DIR}"
|
||||
|
||||
echo "### Done!"
|
||||
echo "The Let’s Encrypt certificates have been installed successfully."
|
||||
echo "If you need to restore at any point, follow the instructions in the error handler above."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user