1628 lines
59 KiB
Bash
1628 lines
59 KiB
Bash
#!/bin/sh
|
||
#
|
||
###############################################################################
|
||
#
|
||
# dyntls.sh
|
||
#
|
||
# Automated Let's Encrypt Certificate Management Script
|
||
#
|
||
# This script automates issuing, updating, and managing Let's Encrypt certificates
|
||
# using a local PKI structure, integrated service mappings, and secure defaults.
|
||
# It supports service reloads and PKI operations via 'systemd' and 'openssl', reads
|
||
# configuration from a vars file, and enforces strong file permissions—suitable
|
||
# for production and testing environments.
|
||
#
|
||
# Features:
|
||
# - Automated certificate issuance and renewal (incl. SAN, wildcard)
|
||
# - Full PKI structure with configurable paths
|
||
# - Service mapping for cert/key deployment and reloads
|
||
# - Support for pre/post command hooks
|
||
# - Configurable ACME client program via vars
|
||
# - POSIX-compatible operation for cron and systemd integration
|
||
#
|
||
# Authors: Stephan Düsterhaupt <me@jabber.stephanduesterhaupt.de>
|
||
# Ivo Noack aka Insonic <me@jabber.ivonoack.de>
|
||
#
|
||
# Copyright (c) 2018-2026 CB-601 - the open tec Elevator <mail@opensource-technology.de>
|
||
# License: MIT
|
||
#
|
||
# Project Home: https://dev.town-square.de/cb601/dyntls
|
||
#
|
||
###############################################################################
|
||
# MIT License
|
||
#
|
||
# Copyright (c) 2025 CB-601 - the open tec Elevator <mail@opensource-technology.de>
|
||
#
|
||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
# of this software and associated documentation files (the "Software"), to deal
|
||
# in the Software without restriction, including without limitation the rights
|
||
# to use, copy, modify, merge, publish, distribute, sublicense and/or sell
|
||
# copies of the Software and to permit persons to whom the Software is
|
||
# furnished to do so, subject to the following conditions:
|
||
#
|
||
# The above copyright notice and this permission notice shall be included in all
|
||
# copies or substantial portions of the Software.
|
||
#
|
||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
# SOFTWARE.
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Show usage/help of the script and commands
|
||
#
|
||
# Prints general usage information and available commands to stdout.
|
||
# Intended to guide user on how to run the script and list supported operations.
|
||
#
|
||
# No input parameters.
|
||
# Outputs text to stdout.
|
||
_usage() {
|
||
_log "Enter the function '_usage()'..." 1
|
||
|
||
# command help:
|
||
|
||
# init-pki
|
||
# update [ cmd-opts ]
|
||
# gen-req <filename_base> [ cmd-opts ]
|
||
printf '%s\n' \
|
||
"dynTLS usage and overview" \
|
||
"" \
|
||
"USAGE: dyntls [options] COMMAND [command-options]" \
|
||
"" \
|
||
"A list of commands is shown below. To get detailed usage and help for a command, run:" \
|
||
" dyntls help COMMAND" \
|
||
"" \
|
||
"For a listing of options that can be supplied before the command, use:" \
|
||
" dyntls help options" \
|
||
"" \
|
||
"Commands:" \
|
||
" init-pki" \
|
||
" add-cert [ cmd-opts ]" \
|
||
" check-cert [ cmd-opts ]" \
|
||
" remove-cert [ cmd-opts ]" \
|
||
" update-cert [ cmd-opts ]" \
|
||
""
|
||
|
||
# Verzeichnisse prüfen
|
||
err_source="Not defined: vars autodetect failed and no value provided"
|
||
work_dir=${DYNTLS:-$err_source}
|
||
pki_dir=${DYNTLS_PKI:-$err_source}
|
||
|
||
printf '%s\n' \
|
||
"DIRECTORY STATUS (commands would take effect on these locations)" \
|
||
" DYNTLS: $work_dir" \
|
||
" PKI: $pki_dir" \
|
||
""
|
||
|
||
_log "Leave the function '_usage()'..." 1
|
||
|
||
return 0
|
||
} #=> usage()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Show help for a specific command or options
|
||
#
|
||
# When called with no arguments, calls _usage().
|
||
# Otherwise prints detailed help text for a given command.
|
||
#
|
||
# @param1: Command name or special keywords like "options".
|
||
# Outputs help text to stdout.
|
||
_cmd_help() {
|
||
text=""
|
||
opts=""
|
||
|
||
case "$1" in
|
||
# init-pki|clean-all)
|
||
#text="
|
||
init-pki)
|
||
text="init-pki [ cmd-opts]\n Removes & re-initializes the PKI dir for a clean PKI"
|
||
;;
|
||
add-hostname)
|
||
text="add-hostname [ cmd-opts]\n Add a hostname to a DNS zone"
|
||
;;
|
||
check-hostname)
|
||
text="check-hostname [ cmd-opts]\n Check a hostname of a DNS zone"
|
||
;;
|
||
remove-hostname)
|
||
text="remove-hostname [ cmd-opts]\n Remove a hostname from a DNS zone"
|
||
;;
|
||
update)
|
||
text="update [ cmd-opts]\n Update a DNS zone"
|
||
;;
|
||
options)
|
||
_opt_usage
|
||
return 0
|
||
;;
|
||
"")
|
||
_usage
|
||
return 0
|
||
;;
|
||
*)
|
||
text="Unknown command: '$1' (try without command for a list of commands)"
|
||
;;
|
||
esac
|
||
|
||
# display the help text
|
||
[ -n "$text" ] && printf '%b\n' "$text"
|
||
[ -n "$opts" ] && {
|
||
printf '\n%s\n' "cmd-opts is an optional set of command options from this list:"
|
||
printf '%s\n' "$opts"
|
||
}
|
||
|
||
return 0
|
||
} #=> _cmd_help()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Print global option flags and usage
|
||
#
|
||
# Prints description of global command line options recognized by the script.
|
||
#
|
||
# No input parameters.
|
||
# Outputs text to stdout.
|
||
_opt_usage() {
|
||
printf '%s\n' \
|
||
"dynTLS Global Option Flags" \
|
||
"" \
|
||
"The following options may be provided before the command. Options specified" \
|
||
"at runtime override env-vars and any 'vars' file in use. Unless noted," \
|
||
"non-empty values to options are mandatory." \
|
||
"" \
|
||
"General options:" \
|
||
" --vars=FILE define a specific 'vars' file to use for dynTLS config" \
|
||
"" \
|
||
"Add hostname options (create an account in the database):" \
|
||
" --hostname=ARG set a hostname" \
|
||
"" \
|
||
"Update options:" \
|
||
" --zone=ARG set DNS zone ARG for update (example365.tld)" \
|
||
""
|
||
|
||
return 0
|
||
} #=> _opt_usage()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Exit script with fatal error message
|
||
#
|
||
# Prints an error message to stderr, logs it if applicable, then exits script with given error code.
|
||
#
|
||
# @param1: Error message string to print.
|
||
# @param2: Exit code integer (default 1).
|
||
# No output (exits).
|
||
_die() {
|
||
msg=$1
|
||
code=${2:-1}
|
||
|
||
# Console + stderr Meldung
|
||
printf '\n%s\n%s\n' "dynTLS error:" "$msg" >&2
|
||
|
||
# Logging, falls gewünscht
|
||
#[ "$code" -ne 0 ] && _log "$msg" 4
|
||
|
||
exit "$code"
|
||
} #=> _die()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Print informational notice to stdout
|
||
#
|
||
# Prints a message unless batch mode is enabled.
|
||
#
|
||
# @param1: Message string to print.
|
||
# Outputs message to stdout conditionally.
|
||
notice() {
|
||
# gibt nur etwas aus, wenn nicht im Batchmodus
|
||
if [ -z "$DYNTLS_BATCH" ]; then
|
||
printf '\n%s\n' "$1"
|
||
fi
|
||
|
||
return 0
|
||
} #=> notice()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Create a secure temporary file/directory
|
||
#
|
||
# Wrapper for mktemp to create temporary files or directories in the session temp directory.
|
||
# Ensures that DYNTLS_TEMP_DIR_session is initialized and usable.
|
||
#
|
||
# No parameters.
|
||
# Outputs path of created temporary file/directory to stdout.
|
||
# Returns 0 on success or 1 on failure.
|
||
dyntls_mktemp() {
|
||
[ -n "$DYNTLS_TEMP_DIR_session" ] || _die "DYNTLS_TEMP_DIR_session not initialized!" 1
|
||
[ -d "$DYNTLS_TEMP_DIR_session" ] || mkdir -p "$DYNTLS_TEMP_DIR_session" \
|
||
|| _die "Could not create temporary directory '$DYNTLS_TEMP_DIR_session'" 1
|
||
|
||
template="$DYNTLS_TEMP_DIR_session/tmp.XXXXXX"
|
||
tempfile=$(mktemp "$template") || return 1
|
||
|
||
# Workaround für win32 mktemp
|
||
if [ "$template" = "$tempfile" ]; then
|
||
tempfile=$(mktemp -du "$tempfile") || return 1
|
||
: >"$tempfile" || return 1
|
||
fi
|
||
|
||
printf '%s\n' "$tempfile"
|
||
|
||
return 0
|
||
} #=> dyntls_mktemp
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Clean up temporary files and restore terminal state on exit
|
||
#
|
||
# Removes temporary session directory if it exists and resets terminal echo.
|
||
#
|
||
# No parameters or output.
|
||
cleanup() {
|
||
# Entferne temporäres Sitzungsverzeichnis falls vorhanden
|
||
[ -n "$DYNTLS_TEMP_DIR_session" ] && rm -rf "$DYNTLS_TEMP_DIR_session"
|
||
|
||
# Terminal-Zustand wiederherstellen
|
||
(stty echo 2>/dev/null) || { (set -o echo 2>/dev/null) && set -o echo; }
|
||
|
||
printf '\n'
|
||
|
||
return 0
|
||
} #=> cleanup()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Verify essential DYNTLS environment variables are defined
|
||
#
|
||
# Checks the presence of critical env vars for PKI and HTTP directories.
|
||
# Exits fatally if any required variable is undefined.
|
||
#
|
||
# No input parameters.
|
||
# No output.
|
||
_vars_source_check() {
|
||
_log "Enter the function '_vars_source_check()'..." 1
|
||
|
||
[ -n "$DYNTLS_PKI" ] || _die "DYNTLS_PKI env-var undefined" 1
|
||
[ -n "$DYNTLS_PKI_HTTP_DIR" ] || _die "DYNTLS_PKI_HTTP_DIR env-var undefined" 1
|
||
[ -n "$DYNTLS_PKI_HTTP_CERT_DIR" ] || _die "DYNTLS_PKI_HTTP_CERT_DIR env-var undefined" 1
|
||
[ -n "$DYNTLS_PKI_HTTP_KEY_DIR" ] || _die "DYNTLS_PKI_HTTP_KEY_DIR env-var undefined" 1
|
||
[ -n "$DYNTLS_HTTPD_DEFAULT_DIR" ] || _die "DYNTLS_HTTPD_DEFAULT_DIR env-var undefined" 1
|
||
[ -n "$DYNTLS_ENCRYPT_TOKEN_DIR" ] || _die "DYNTLS_ENCRYPT_TOKEN_DIR env-var undefined" 1
|
||
|
||
_log "Leave the function '_vars_source_check()'..." 1
|
||
|
||
return 0
|
||
} #=> _vars_source_check()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Verify PKI directory structure and required files exist
|
||
#
|
||
# Checks existence of necessary PKI directories and server base key.
|
||
# Calls _vars_source_check internally.
|
||
# Exits fatally if any required directory or file is missing.
|
||
#
|
||
# No input parameters.
|
||
# No output.
|
||
_verify_pki_init() {
|
||
_log "Enter the function '_verify_pki_init()'..." 1
|
||
help_note="Run dyntls without commands for usage and command help."
|
||
|
||
_vars_source_check
|
||
|
||
[ -d "$DYNTLS_PKI" ] || _die "DYNTLS_PKI does not exist. $help_note" 1
|
||
[ -d "$DYNTLS_PKI_HTTP_DIR" ] || _die "DYNTLS_PKI_HTTP_DIR missing. $help_note" 1
|
||
[ -d "$DYNTLS_PKI_HTTP_CERT_DIR" ] || _die "DYNTLS_PKI_HTTP_CERT_DIR missing. $help_note" 1
|
||
[ -d "$DYNTLS_PKI_HTTP_KEY_DIR" ] || _die "DYNTLS_PKI_HTTP_KEY_DIR missing. $help_note" 1
|
||
[ -d "$DYNTLS_HTTPD_DEFAULT_DIR" ] || _die "DYNTLS_HTTPD_DEFAULT_DIR missing. $help_note" 1
|
||
[ -d "$DYNTLS_ENCRYPT_TOKEN_DIR" ] || _die "DYNTLS_ENCRYPT_TOKEN_DIR missing. $help_note" 1
|
||
[ -f "$DYNTLS_PKI_SERVER_BASEKEY" ] || _die "DYNTLS_PKI_SERVER_BASEKEY missing. $help_note" 1
|
||
|
||
_log "Leave the function '_verify_pki_init()'..." 1
|
||
|
||
return 0
|
||
} #=> _verify_pki_init()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Initialize PKI directory structure and keys
|
||
#
|
||
# Creates PKI cert and key directories if missing.
|
||
# Generates base server key if missing and key linking is enabled.
|
||
# Sets ownership and permissions on directories and files.
|
||
#
|
||
# No input parameters.
|
||
# Outputs informational messages and errors.
|
||
_init_pki() {
|
||
_log "Enter the function '_init_pki()'..." 1
|
||
|
||
[ -d "$DYNTLS_PKI_HTTP_CERT_DIR" ] || mkdir -p "$DYNTLS_PKI_HTTP_CERT_DIR" \
|
||
|| _die "Failed to create $DYNTLS_PKI_HTTP_CERT_DIR" 1
|
||
[ -d "$DYNTLS_PKI_HTTP_KEY_DIR" ] || mkdir -p "$DYNTLS_PKI_HTTP_KEY_DIR" \
|
||
|| _die "Failed to create $DYNTLS_PKI_HTTP_KEY_DIR" 1
|
||
|
||
if [ ! -f "$DYNTLS_PKI_SERVER_BASEKEY" ] && [ "$DYNTLS_PKI_KEY_LNS" -ne 0 ]; then
|
||
_log "Create base server key, length $DYNTLS_PKI_KEY_SIZE bit" 1
|
||
#MyLEError=$(eval "$DYNTLS_OPENSSL genrsa -out $DYNTLS_PKI_SERVER_BASEKEY $DYNTLS_PKI_KEY_SIZE 2>&1")
|
||
"$DYNTLS_OPENSSL" genrsa -out "$DYNTLS_PKI_SERVER_BASEKEY" "$DYNTLS_PKI_KEY_SIZE" 2>/dev/null \
|
||
|| _die "Failed to generate base server key" 1
|
||
fi
|
||
|
||
find "$DYNTLS_HTTPD_DEFAULT_DIR" -type d -exec chown "$DYNTLS_HTTPD_DEFAULT_OWNER" {} \; -exec chmod 755 {} \;
|
||
find "$DYNTLS_PKI_HTTP_CERT_DIR" -type f -exec chmod 644 {} \;
|
||
find "$DYNTLS_PKI_HTTP_KEY_DIR" -type f -exec chmod 440 {} \;
|
||
|
||
[ -d "$DYNTLS_ENCRYPT_TOKEN_DIR" ] || mkdir -p "$DYNTLS_ENCRYPT_TOKEN_DIR" \
|
||
|| _die "Failed to create $DYNTLS_ENCRYPT_TOKEN_DIR" 1
|
||
|
||
notice "init-pki complete; you may now create a certificate.
|
||
Your PKI dirs are: $DYNTLS_PKI_HTTP_CERT_DIR, $DYNTLS_PKI_HTTP_KEY_DIR, $DYNTLS_ENCRYPT_TOKEN_DIR"
|
||
|
||
_log "Leave the function '_init_pki()'..." 1
|
||
|
||
return 0
|
||
} #=> _init_pki()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Initialize log directory for dynTLS logs
|
||
#
|
||
# Checks for the log directory existence; creates it if missing.
|
||
# Exits fatally on failure.
|
||
#
|
||
# No input parameters.
|
||
# Outputs informational messages and errors.
|
||
_init_log() {
|
||
if [ ! -d "$DYNTLS_LOG_DIR" ]; then
|
||
printf "The log directory '%s' doesn't exist. Try to create it...\n" "$DYNTLS_LOG_DIR"
|
||
if mkdir -p "$DYNTLS_LOG_DIR" 2>/dev/null; then
|
||
printf "Log directory '%s' created.\n" "$DYNTLS_LOG_DIR"
|
||
else
|
||
_die "Could not create log directory '$DYNTLS_LOG_DIR'" 1
|
||
fi
|
||
fi
|
||
|
||
return 0
|
||
} #=> _init_log
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Load and set configuration variables for dynTLS
|
||
#
|
||
# Loads configuration variables from a 'vars' file if defined, falling back to defaults.
|
||
# Supports multiple possible locations for the vars file, with overriding behavior.
|
||
# Initializes many DYNTLS_* environment variables with defaults if not already set.
|
||
# This ensures consistent environment setup for the remainder of the script.
|
||
#
|
||
# No input parameters.
|
||
# Sets and exports multiple DYNTLS_* environment variables.
|
||
# Outputs informational notice if vars file was loaded.
|
||
_vars_setup() {
|
||
prog_file=$0
|
||
prog_dir=$(dirname "$(readlink -f "$prog_file" 2>/dev/null || printf '%s' "$prog_file")")
|
||
vars=""
|
||
|
||
[ -f "$DYNTLS_VARS_FILE" ] && vars=$DYNTLS_VARS_FILE
|
||
[ -z "$vars" ] && [ -n "$DYNTLS" ] && [ -f "$DYNTLS/vars" ] && vars=$DYNTLS/vars
|
||
[ -z "$vars" ] && [ -f "$prog_dir/vars" ] && vars=$prog_dir/vars
|
||
|
||
if [ -z "$DYNTLS_NO_VARS" ] && [ -n "$vars" ]; then
|
||
DYNTLS_CALLER=1
|
||
. "$vars"
|
||
notice "Note: using dynTLS configuration from: $vars"
|
||
fi
|
||
|
||
# Set defaults, preferring existing env-vars if present
|
||
set_var DYNTLS "$prog_dir"
|
||
set_var DYNTLS_LE_PROGRAM "letsencrypt_master.sh"
|
||
set_var DYNTLS_OPENSSL openssl
|
||
set_var DYNTLS_TMP "$DYNTLS/tmp"
|
||
#set_var DYNTLS_DN cn_only
|
||
#set_var DYNTLS_REQ_COUNTRY "US"
|
||
#set_var DYNTLS_REQ_PROVINCE "California"
|
||
#set_var DYNTLS_REQ_CITY "San Francisco"
|
||
#set_var DYNTLS_REQ_ORG "Copyleft Certificate Co"
|
||
#set_var DYNTLS_REQ_EMAIL me@example.net
|
||
#set_var DYNTLS_REQ_OU "My Organizational Unit"
|
||
set_var DYNTLS_PKI "/etc/pki"
|
||
set_var DYNTLS_PKI_KEY_ALGO rsa
|
||
set_var DYNTLS_PKI_KEY_SIZE 2048
|
||
set_var DYNTLS_PKI_KEY_LNS 0
|
||
set_var DYNTLS_PKI_KEY_CURVE secp384r1
|
||
set_var DYNTLS_PKI_HTTP_DIR "$DYNTLS_PKI/httpd"
|
||
set_var DYNTLS_PKI_HTTP_CERT_DIR "$DYNTLS_PKI_HTTP_DIR/certs"
|
||
set_var DYNTLS_PKI_HTTP_CERT_BACKUP_DIR "$DYNTLS_PKI_HTTP_CERT_DIR/backup"
|
||
set_var DYNTLS_PKI_HTTP_KEY_DIR "$DYNTLS_PKI_HTTP_DIR/private"
|
||
set_var DYNTLS_PKI_CERT_SUFFIX "cert.pem"
|
||
set_var DYNTLS_PKI_FULLCHAIN_SUFFIX "fullchain.pem"
|
||
set_var DYNTLS_PKI_KEY_SUFFIX "key.pem"
|
||
set_var DYNTLS_PKI_LECA_CHAIN_FILE "LE_CA.chain.pem"
|
||
set_var DYNTLS_PKI_LECA_CHAIN "$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_PKI_LECA_CHAIN_FILE"
|
||
set_var DYNTLS_PKI_LECA_R12_CHAIN_FILE "LE_CA-R12.chain.pem"
|
||
set_var DYNTLS_PKI_LECA_R12_CHAIN "$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_PKI_LECA_R12_CHAIN_FILE"
|
||
set_var DYNTLS_PKI_LECA_R13_CHAIN_FILE "LE_CA-R13.chain.pem"
|
||
set_var DYNTLS_PKI_LECA_R13_CHAIN "$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_PKI_LECA_R13_CHAIN_FILE"
|
||
set_var DYNTLS_PKI_CERT_EXPIRE 30 # Let's Encrypt default: 30 days
|
||
set_var DYNTLS_PKI_KEY_FORCE_RENEW 0
|
||
set_var DYNTLS_TEMP_DIR "$DYNTLS_TMP"
|
||
set_var DYNTLS_BACKUP_EXPIRATION 0
|
||
|
||
set_var DYNTLS_LOG_DIR "/var/log/dyntls"
|
||
set_var DYNTLS_LOG_FILE "$DYNTLS_LOG_DIR/dyntls.log"
|
||
set_var DYNTLS_LOG_LEVEL "3"
|
||
|
||
set_var DYNTLS_ENCRYPT_ACCOUNTKEY "$DYNTLS/private/letsencrypt_account.key"
|
||
set_var DYNTLS_PKI_SERVER_BASEKEY_FILE "base.$DYNTLS_PKI_KEY_SUFFIX"
|
||
set_var DYNTLS_PKI_SERVER_BASEKEY "$DYNTLS_PKI_HTTP_KEY_DIR/$DYNTLS_PKI_SERVER_BASEKEY_FILE"
|
||
set_var DYNTLS_HTTPD_DEFAULT_DIR "/var/www/public_html/default"
|
||
set_var DYNTLS_ENCRYPT_TOKEN_DIR "$DYNTLS_HTTPD_DEFAULT_DIR/.well-known/acme-challenge"
|
||
set_var DYNTLS_DNS_OPTIONS ""
|
||
|
||
set_var DYNTLS_DNS_SERVER "root-dns.example365.tld"
|
||
set_var DYNTLS_DNS_TSIG "tsig.key"
|
||
set_var DYNTLS_DNS_ZONE ""
|
||
|
||
set_var DYNTLS_HTTPD_DEFAULT_OWNER "apache."
|
||
|
||
set_var DYNTLS_RELOAD_WEBSERVER "false"
|
||
set_var DYNTLS_SEND_MAIL "false"
|
||
|
||
set_list DYNTLS_DOMAIN_LIST "example365.tld:sub1.example365.tld:sub2.example365.tld" "1"
|
||
set_list DYNTLS_DOMAINSERVICE_LIST "mail02.example365.tld:postfix:root.root:444:postfix:1:restart:Postfix" "1"
|
||
|
||
set_var DYNTLS_PRODUCTIVE 0
|
||
|
||
set_list DYNTLS_CMD_PRE_LIST ""
|
||
set_list DYNTLS_CMD_POST_LIST ""
|
||
|
||
|
||
# Assign value to $DYNTLS_TEMP_DIR_session and work around Windows mktemp bug when parent dir is missing
|
||
if [ -z "$DYNTLS_TEMP_DIR_session" ]; then
|
||
if [ -d "$DYNTLS_TEMP_DIR" ]; then
|
||
DYNTLS_TEMP_DIR_session="$(mktemp -du "$DYNTLS_TEMP_DIR/dyn-tls-$$.XXXXXX")"
|
||
else
|
||
# If the directory does not exist then we have not run init-pki
|
||
mkdir -p "$DYNTLS_TEMP_DIR" || _die "Cannot create $DYNTLS_TEMP_DIR (permission?)" "1"
|
||
DYNTLS_TEMP_DIR_session="$(mktemp -du "$DYNTLS_TEMP_DIR/dyn-tls-$$.XXXXXX")"
|
||
rm -rf "$DYNTLS_TEMP_DIR"
|
||
fi
|
||
fi
|
||
|
||
return 0
|
||
} # _vars_setup()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Sets environment variables with defaults if not defined
|
||
#
|
||
# @param1: Name of environment variable to set.
|
||
# @param2...: Default value(s) to assign if variable is unset or empty.
|
||
# Exports the variable and sets value accordingly.
|
||
#
|
||
# No output.
|
||
set_var() {
|
||
var=$1
|
||
shift
|
||
value=$*
|
||
|
||
eval "${var}=\${$var:-$value}"
|
||
|
||
export "$var"
|
||
} #=> set_var()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Set a list-style environment variable with concatenation
|
||
#
|
||
# Adds new values to an existing colon-separated list variable or sets it if empty.
|
||
# @param1: Name of list variable.
|
||
# @param2: New value(s) to add.
|
||
# @param3: Default flag (optional).
|
||
# Exports updated list variable.
|
||
#
|
||
# No output.
|
||
set_list() {
|
||
list=$1
|
||
value=$2
|
||
default=$3
|
||
|
||
current=$(eval "printf '%s' \${$list}")
|
||
|
||
if [ -n "$current" ]; then
|
||
[ -z "$default" ] && eval "$list=\"$current&$value\""
|
||
else
|
||
eval "$list=\"$value\""
|
||
fi
|
||
|
||
export "$list"
|
||
} #=> set_list()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Log messages with severity and optionally print to console/stderr
|
||
#
|
||
# 0 -> OFF (silent mode)
|
||
# 1 -> DEBUG
|
||
# 2 -> INFO
|
||
# 3 -> WARN
|
||
# 4 -> ERROR
|
||
# 5 -> CRITICAL
|
||
#
|
||
# @param1: Message string to log.
|
||
# @param2: Severity level (0-5) where higher is more severe.
|
||
# Writes message with timestamp to log file.
|
||
# Prints to stderr if level >= 2.
|
||
_log() {
|
||
msg=$1
|
||
level=$2
|
||
|
||
# Optional domainset prefix to group log lines belonging to the same domain set
|
||
ds_prefix=""
|
||
if [ -n "$DYNTLS_DOMAINSET_ID" ]; then
|
||
ds_prefix="[DS:${DYNTLS_DOMAINSET_ID}]"
|
||
fi
|
||
|
||
# Map numeric levels to human-readable tags
|
||
# 0 -> OFF (silent mode)
|
||
# 1 -> DEBUG
|
||
# 2 -> INFO
|
||
# 3 -> WARN
|
||
# 4 -> ERROR
|
||
# 5 -> CRITICAL
|
||
set -- "" "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL"
|
||
shift # now: $1=DEBUG, $2=INFO, ...
|
||
|
||
# Determine the level name only for levels >= 1
|
||
level_name=$([ "$level" -ge 1 ] && eval "printf '%s' \${$level}")
|
||
|
||
# Write to log file if level is high enough and message is not empty
|
||
# Desired total width for "[LEVEL]" including brackets, e.g. 9 chars
|
||
# Example:
|
||
# "[INFO]" -> len 6 -> 3 spaces
|
||
# "[DEBUG]" -> len 7 -> 2 spaces
|
||
level_tag="[$level_name]"
|
||
level_tag_len=${#level_tag}
|
||
pad_width=9
|
||
pad_count=$(( pad_width - level_tag_len ))
|
||
[ "$pad_count" -lt 1 ] && pad_count=1
|
||
pad=$(printf '%*s' "$pad_count" '')
|
||
|
||
if [ "$level" -ge "$DYNTLS_LOG_LEVEL" ] && [ -n "$msg" ]; then
|
||
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||
# Example:
|
||
# 2025-12-06 11:21:00 dyntls [DS:251206-1a2b3c4d]: [INFO] Some message
|
||
if [ -n "$ds_prefix" ]; then
|
||
# 2025-12-06 11:21:00 dyntls [DS:...]: [INFO] Text
|
||
printf '%s dyntls %s: %s%s%s\n' \
|
||
"$ts" "$ds_prefix" "$level_tag" "$pad" "$msg" >>"$DYNTLS_LOG_FILE"
|
||
else
|
||
# 2025-12-06 11:21:00 dyntls [INFO] Text
|
||
printf '%s dyntls %s%s%s\n' \
|
||
"$ts" "$level_tag" "$pad" "$msg" >>"$DYNTLS_LOG_FILE"
|
||
fi
|
||
fi
|
||
|
||
# For levels >= 2 also print to stderr (including the domainset prefix)
|
||
if [ "$level" -ge 2 ]; then
|
||
printf '%s\n' "$msg" >&2
|
||
fi
|
||
|
||
return 0
|
||
} #=> _log()
|
||
|
||
|
||
# SDuesterhaupt: 2025-12-06 - Initialize per-domainset logging ID
|
||
_init_domainset_id() {
|
||
# 6-digit date stamp in the format YYMMDD
|
||
ds_date=$(date +%y%m%d)
|
||
|
||
# Build a hash source string from:
|
||
# - the given domain set identifier (e.g. CN or full DYNTLS_DOMAINS)
|
||
# - current UNIX timestamp
|
||
# - current process ID
|
||
# - a small chunk of random data from /dev/urandom (if available)
|
||
hash_src="$1:$(date +%s):$$:$(od -An -N4 -tx4 /dev/urandom 2>/dev/null)"
|
||
|
||
# Create an 8-character hexadecimal hash from the source string
|
||
# Fallback to 'cksum' if 'sha256sum' is not available
|
||
if command -v sha256sum >/dev/null 2>&1; then
|
||
ds_hash=$(printf '%s\n' "$hash_src" | sha256sum 2>/dev/null | cut -c1-8)
|
||
else
|
||
ds_hash=$(printf '%s\n' "$hash_src" | cksum 2>/dev/null | awk '{print $1}' | cut -c1-8)
|
||
fi
|
||
|
||
# Compose the final domainset ID: YYMMDD-<8-hex-chars>
|
||
DYNTLS_DOMAINSET_ID="${ds_date}-${ds_hash}"
|
||
|
||
# Export the ID so that the logger and all sub-functions can use it
|
||
export DYNTLS_DOMAINSET_ID
|
||
} #=> _init_domainset_id()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Validate hostname or extract parts
|
||
#
|
||
# Checks or extracts components of a hostname.
|
||
# @param1: Mode ("check" or "get").
|
||
# @param2: Type ("tld", "sld", "fqdn", "hostname", "wildcard", "domain", "subdomain").
|
||
# Returns 0 if check passes or prints extracted value if get mode.
|
||
# Exits fatally on invalid mode or type.
|
||
#
|
||
# Uses DYNTLS_MEMBER_HOSTNAME for input hostname.
|
||
_Hostname()
|
||
{
|
||
_log "Enter the function '_Hostname()'..." 1
|
||
|
||
MyMethod=$1
|
||
MyDomainPart=$2
|
||
MyReturnFlag=0
|
||
|
||
_log "Method '$MyMethod' ..." 1
|
||
|
||
case "$MyMethod" in
|
||
check)
|
||
_log "Check type of hostname '$DYNTLS_MEMBER_HOSTNAME' ..." 1
|
||
case "$MyDomainPart" in
|
||
tld)
|
||
# Top Level Domain:
|
||
# Must be alphabetic only, length 2–5
|
||
if printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | grep -Eq '^[A-Za-z]{2,5}$'; then
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is a valid TLD." 1
|
||
MyReturnFlag=0
|
||
else
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is not a TLD." 3
|
||
MyReturnFlag=1
|
||
fi
|
||
;;
|
||
sld)
|
||
# Second Level Domain:
|
||
# e.g. example.com
|
||
if printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | grep -Eq '^[A-Za-z0-9_-]+\.[A-Za-z]{2,5}$'; then
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is a valid SLD." 1
|
||
MyReturnFlag=0
|
||
else
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is not a valid SLD." 3
|
||
MyReturnFlag=1
|
||
fi
|
||
;;
|
||
fqdn)
|
||
# Fully Qualified Domain Name:
|
||
# at least 3 parts, e.g. sub.example.com
|
||
if printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | grep -Eq '^([A-Za-z0-9_-]+\.){2,}[A-Za-z]{2,5}$'; then
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is a valid FQDN." 1
|
||
MyReturnFlag=0
|
||
else
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is not a valid FQDN." 3
|
||
MyReturnFlag=1
|
||
fi
|
||
;;
|
||
hostname)
|
||
# Generic hostnames with minimum pattern sld.tld
|
||
if printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | \
|
||
grep -Eq '^(([A-Za-z0-9][A-Za-z0-9_-]*[A-Za-z0-9]|[A-Za-z0-9])\.)+[A-Za-z]{2,5}$'; then
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is a valid hostname." 1
|
||
MyReturnFlag=0
|
||
else
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is not a valid hostname." 3
|
||
MyReturnFlag=1
|
||
fi
|
||
;;
|
||
wildcard)
|
||
# Wildcard domains must start with "*." followed by a valid hostname
|
||
if printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | \
|
||
grep -Eq '^\*\.[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.[A-Za-z]{2,5}$'; then
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is a valid wildcard domain." 1
|
||
MyReturnFlag=0
|
||
else
|
||
_log "Expression '$DYNTLS_MEMBER_HOSTNAME' is not a valid wildcard domain." 3
|
||
MyReturnFlag=1
|
||
fi
|
||
;;
|
||
*)
|
||
_die "Invalid action '$MyDomainPart' in function _Hostname()." 1
|
||
;;
|
||
esac
|
||
;;
|
||
get)
|
||
_log "Extract part of hostname '$DYNTLS_MEMBER_HOSTNAME' ..." 2
|
||
case "$MyDomainPart" in
|
||
domain)
|
||
# Extract base domain (SLD.TLD) from an FQDN
|
||
# e.g. "sub.example.com" -> "example.com"
|
||
MyDomain=$(printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | sed -E 's/^.*\.([^.]+\.[A-Za-z]{2,5})$/\1/')
|
||
printf '%s\n' "$MyDomain"
|
||
;;
|
||
subdomain)
|
||
# Extract leftmost subdomain from an FQDN
|
||
# e.g. "sub.example.com" -> "sub"
|
||
MySubdomain=$(printf '%s\n' "$DYNTLS_MEMBER_HOSTNAME" | sed -E 's/^([^.]+)\..*$/\1/')
|
||
printf '%s\n' "$MySubdomain"
|
||
;;
|
||
*)
|
||
_die "Invalid action '$MyDomainPart' in function _Hostname()." 1
|
||
;;
|
||
esac
|
||
;;
|
||
*)
|
||
_die "Invalid method '$MyMethod' in function _Hostname()." 1
|
||
;;
|
||
esac
|
||
|
||
_log "Leave the function '_Hostname()'..." 1
|
||
|
||
return $MyReturnFlag
|
||
} #=> _Hostname()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Build Subject Alternative Name list from DYNTLS_DOMAINS
|
||
#
|
||
# Parses DYNTLS_DOMAINS colon-separated list, validates each domain,
|
||
# and populates DYNTLS_SAN_LIST with valid hostnames and wildcards.
|
||
#
|
||
# No input parameters.
|
||
# Sets and exports DYNTLS_SAN_LIST.
|
||
_SetupDomainSANList()
|
||
{
|
||
_log "Enter the function '_SetupDomainSANList()'..." 1
|
||
_log "DYNTLS_MEMBER_HOSTNAME: $DYNTLS_MEMBER_HOSTNAME" 1
|
||
_log "DYNTLS_DOMAINS: $DYNTLS_DOMAINS" 1
|
||
|
||
# Split DYNTLS_DOMAINS on ":" into an array
|
||
#DomainAry=$(printf '%s\n' "$DYNTLS_DOMAINS" | sed 's/:/ /g')
|
||
# Copy DYNTLS_DOMAINS to helper variable so the original is untouched
|
||
DomainAry="$DYNTLS_DOMAINS"
|
||
_log "DomainAry: $DomainAry" 1
|
||
|
||
# Initialize positional parameters safely
|
||
# Temporarily set IFS to ':' and use set -- to create positional parameters (POSIX compatible)
|
||
old_ifs="$IFS"
|
||
IFS=":"
|
||
set -- $DomainAry
|
||
IFS="$old_ifs"
|
||
|
||
# The CN (Common Name) is always the first and added to the SAN list first
|
||
CN="$DYNTLS_MEMBER_HOSTNAME"
|
||
export DYNTLS_SAN_LIST="$CN"
|
||
|
||
_log "CN domain is '$CN'. Begin checking for additional SANs..." 1
|
||
|
||
# Iterate over all additional SAN domains (starting from the second parameter)
|
||
shift
|
||
for dom in "$@"; do
|
||
_log "Checking additional SAN entry: $dom" 1
|
||
export DYNTLS_MEMBER_HOSTNAME="$dom"
|
||
|
||
# Is the entry a valid hostname?
|
||
if _Hostname "check" "hostname"; then
|
||
DYNTLS_SAN_LIST="$DYNTLS_SAN_LIST $dom"
|
||
continue
|
||
fi
|
||
|
||
# Is it a valid wildcard?
|
||
if _Hostname "check" "wildcard"; then
|
||
DYNTLS_SAN_LIST="$DYNTLS_SAN_LIST $dom"
|
||
else
|
||
_log "Domain '$dom' is not a valid hostname or wildcard. Skipped." 3
|
||
fi
|
||
done
|
||
|
||
# Always restore the original CN value after the loop
|
||
export DYNTLS_MEMBER_HOSTNAME="$CN"
|
||
|
||
_log "Resulting DYNTLS_SAN_LIST: $DYNTLS_SAN_LIST" 1
|
||
_log "Leave the function '_SetupDomainSANList()'..." 1
|
||
|
||
return 0
|
||
} #=> _SetupDomainSANList()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Deploy issued certificates to configured services
|
||
#
|
||
# Copies or links certificate and key files to service PKI directories,
|
||
# changes ownership and permissions, and reloads or restarts services as configured.
|
||
#
|
||
# Uses DYNTLS_DOMAINSERVICE_LIST and DYNTLS_MEMBER_HOSTNAME globals.
|
||
# Returns 0 on success or 1 if errors occurred.
|
||
_ProvideCertDomainService() {
|
||
_log "Enter the function '_ProvideCertDomainService()'..." 1
|
||
_log "DYNTLS_MEMBER_HOSTNAME: $DYNTLS_MEMBER_HOSTNAME" 1
|
||
|
||
MyIsError=0
|
||
|
||
# Split list by '&' into individual domain-service mappings
|
||
IFS=$'\n' DomainServiceAry=$(echo "$DYNTLS_DOMAINSERVICE_LIST" | sed 's/&/\n/g')
|
||
for ServiceEntry in $DomainServiceAry; do
|
||
|
||
# Split one mapping into fields
|
||
IFS=":" set -- $ServiceEntry
|
||
Domain="$1"; PkiDir="$2"; UserGroup="$3"; FilePerm="$4"
|
||
Service="$5"; ServiceOwner="$6"; RestartFlag="$7"; RestartMode="$8"; DisplayName="$9"
|
||
|
||
# Decide whether PkiDir is relative (default) or absolute (contains '/')
|
||
case "$PkiDir" in
|
||
*/*)
|
||
# Absolute path (or in general a path containing '/')
|
||
EffectivePkiDir="$PkiDir"
|
||
;;
|
||
*)
|
||
# Relative service name > located below DYNTLS_PKI
|
||
EffectivePkiDir="$DYNTLS_PKI/$PkiDir"
|
||
;;
|
||
esac
|
||
|
||
# Convert legacy user.group notation to user:group for chown to avoid deprecation warnings
|
||
UG_USER=${UserGroup%%.*}
|
||
UG_GROUP=${UserGroup#*.}
|
||
UserGroup=$UG_USER:$UG_GROUP
|
||
|
||
_log "Processing mapping: domain=$Domain, pkiDir=$PkiDir, userGroup=$UserGroup, perm=$FilePerm, service=$Service, owner=$ServiceOwner, restart=$RestartFlag, mode=$RestartMode, name=$DisplayName" 1
|
||
|
||
# Match against current hostname
|
||
if [ "$Domain" = "$DYNTLS_MEMBER_HOSTNAME" ]; then
|
||
_log "$DisplayName-Domain recognized! Try to provide the certificate to $DisplayName..." 2
|
||
|
||
# Ensure required directories exist
|
||
_log "Domain '$Domain' matches CN. Preparing service directory '$EffectivePkiDir'..." 1
|
||
mkdir -p "$EffectivePkiDir/certs" "$EffectivePkiDir/private"
|
||
|
||
# Set ownership/permissions on directories
|
||
_log "Adjust the owner of '$EffectivePkiDir/'..." 1
|
||
find "$EffectivePkiDir" -type d | xargs -r chown "$UserGroup"
|
||
find "$EffectivePkiDir" -type d | xargs -r chmod 750
|
||
|
||
# Provide base server key if missing
|
||
_log "Check whether the server base key '$EffectivePkiDir/private/$DYNTLS_PKI_SERVER_BASEKEY_FILE' exists..." 1
|
||
if [ ! -f "$EffectivePkiDir/private/$DYNTLS_PKI_SERVER_BASEKEY_FILE" ]; then
|
||
_log "$DYNTLS_PKI_SERVER_BASEKEY doesn't exist. Copy it..." 1
|
||
cp "$DYNTLS_PKI_SERVER_BASEKEY" "$EffectivePkiDir/private/"
|
||
fi
|
||
|
||
# Copy issued certificates into service PKI area
|
||
_log "Copy the domain certificate, LE_CA chain and full chain from $DYNTLS_PKI_HTTP_CERT_DIR to the PKI service dir '$EffectivePkiDir/certs/'..." 1
|
||
cp "$DYNTLS_DOMAIN_TARGET_CERT" \
|
||
"$DYNTLS_PKI_LECA_CHAIN" \
|
||
"$DYNTLS_PKI_LECA_R12_CHAIN" \
|
||
"$DYNTLS_PKI_LECA_R13_CHAIN" \
|
||
"$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_FULLCHAIN_SUFFIX" \
|
||
"$EffectivePkiDir/certs/"
|
||
|
||
# Provide server key: either symlink to base or copy unique key
|
||
KeyPath="$EffectivePkiDir/private/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_KEY_SUFFIX"
|
||
if [ "$DYNTLS_PKI_KEY_LNS" -eq 1 ]; then
|
||
_log "Create a symlink '$EffectivePkiDir/private/base.$DYNTLS_PKI_KEY_SUFFIX' to '$KeyPath'." 1
|
||
ln -sf "$EffectivePkiDir/private/base.$DYNTLS_PKI_KEY_SUFFIX" "$KeyPath"
|
||
else
|
||
_log "Copy unique key from '$DYNTLS_PKI_HTTP_KEY_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_KEY_SUFFIX' to '$EffectivePkiDir/private/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_KEY_SUFFIX'." 1
|
||
cp "$DYNTLS_PKI_HTTP_KEY_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_KEY_SUFFIX" "$EffectivePkiDir/private/"
|
||
fi
|
||
|
||
# Adjust ownership and permissions on .pem files
|
||
_log "Adjust the ownership of '$EffectivePkiDir/certs/*.pem'..." 1
|
||
find "$EffectivePkiDir/certs" -type f -name '*.pem' | xargs -r chown "$UserGroup"
|
||
_log "Adjust the ownership of '$EffectivePkiDir/private/*.pem'..." 1
|
||
find "$EffectivePkiDir/private" -type f -name '*.pem' | xargs -r chown "$UserGroup"
|
||
_log "Adjust the permissions of '$EffectivePkiDir/certs/*.pem'..." 1
|
||
find "$EffectivePkiDir/certs" -type f -name '*.pem' | xargs -r chmod "$FilePerm"
|
||
_log "Adjust the permissions of '$EffectivePkiDir/private/*.pem'..." 1
|
||
find "$EffectivePkiDir/private" -type f -name '*.pem' | xargs -r chmod 440
|
||
|
||
_log "Certificates and keys provided for service '$DisplayName'." 2
|
||
|
||
# Restart or reload the service if configured
|
||
if [ "$RestartFlag" -eq 1 ] && [ -n "$Service" ]; then
|
||
# fallback to restart if mode is missing or invalid
|
||
[ "$RestartMode" = "restart" ] || [ "$RestartMode" = "reload" ] || RestartMode="restart"
|
||
|
||
if [ -n "$ServiceOwner" ] && [ "$ServiceOwner" != "root" ]; then
|
||
_log "Reloading service '$Service' as non-root user '$ServiceOwner'..." 1
|
||
if sudo -u "$ServiceOwner" XDG_RUNTIME_DIR="/run/user/$(id -u $ServiceOwner)" \
|
||
systemctl --user "$RestartMode" "$Service.service"; then
|
||
_log "Service '$DisplayName' successfully $RestartMode-ed as '$ServiceOwner'." 2
|
||
else
|
||
_log "Error while $RestartMode-ing service '$DisplayName'." 4
|
||
MyIsError=1
|
||
fi
|
||
else
|
||
_log "Reloading service '$Service' as root..." 1
|
||
if systemctl "$RestartMode" "$Service.service"; then
|
||
_log "Service '$DisplayName' successfully $RestartMode-ed as root." 2
|
||
else
|
||
_log "Error while $RestartMode-ing service '$DisplayName'." 4
|
||
MyIsError=1
|
||
fi
|
||
fi
|
||
else
|
||
_log "No restart required for service '$DisplayName'." 2
|
||
fi
|
||
fi
|
||
done
|
||
|
||
_log "Leave the function '_ProvideCertDomainService()'..." 1
|
||
|
||
return $MyIsError
|
||
} #=> _ProvideCertDomainService()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Create or renew a certificate with Let's Encrypt
|
||
#
|
||
# Creates or renews certificate for a CN hostname and subject alt names,
|
||
# calls external LE signing scripts, processes results, backs up old certs,
|
||
# installs new certificates, and triggers webserver reload and service updates.
|
||
#
|
||
# Uses DYNTLS_MEMBER_HOSTNAME, DYNTLS_SAN_LIST, DYNTLS_DOMAINSERVICE_LIST
|
||
#
|
||
# Outputs files under PKI directories and triggers reload flags.
|
||
_create_cert() {
|
||
_log "Enter the function '_create_cert()'..." 1
|
||
|
||
# Debug context for this certificate run
|
||
_log "CN (DYNTLS_MEMBER_HOSTNAME): $DYNTLS_MEMBER_HOSTNAME" 1
|
||
_log "SAN list (DYNTLS_SAN_LIST): $DYNTLS_SAN_LIST" 1
|
||
_log "Domain service mapping (DYNTLS_DOMAINSERVICE_LIST): $DYNTLS_DOMAINSERVICE_LIST" 1
|
||
|
||
########################################
|
||
# 1. Temporary output for the new cert
|
||
########################################
|
||
mkdir -p "$DYNTLS_TMP"
|
||
out_file_tmp="$DYNTLS_TMP/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_CERT_SUFFIX"
|
||
_log "Temporary certificate file: $out_file_tmp" 1
|
||
|
||
########################################
|
||
# 2. Build LE command options
|
||
########################################
|
||
# Initialize with empty
|
||
DYNTLS_DNS_OPTIONS=""
|
||
|
||
# Add all signing options
|
||
DYNTLS_DNS_OPTIONS="-a $DYNTLS_ENCRYPT_ACCOUNTKEY -k $DYNTLS_PKI_SERVER_BASEKEY"
|
||
_log "Added account key and base server key options: -a $DYNTLS_ENCRYPT_ACCOUNTKEY -k $DYNTLS_PKI_SERVER_BASEKEY" 1
|
||
|
||
# Staging mode: prepend -s
|
||
[ "$DYNTLS_PRODUCTIVE" -eq 0 ] && {
|
||
_log "LetsEncrypt staging mode enabled (-s)." 1
|
||
DYNTLS_DNS_OPTIONS="-s $DYNTLS_DNS_OPTIONS"
|
||
}
|
||
|
||
# http-01 vs dns-01 challenge
|
||
if echo "$DYNTLS_SAN_LIST" | grep -E -q '(^\*\.)|( \*\.)'; then
|
||
DYNTLS_DNS_ZONE=$(_Hostname get domain)
|
||
_log "Wildcard detected in SAN list; using dns-01 challenge for zone '$DYNTLS_DNS_ZONE'." 1
|
||
DYNTLS_DNS_OPTIONS="$DYNTLS_DNS_OPTIONS -l dns-01 -d $DYNTLS_DNS_SERVER -t $DYNTLS_DNS_TSIG -z $DYNTLS_DNS_ZONE"
|
||
_log "DNS challenge options: -l dns-01 -d $DYNTLS_DNS_SERVER -t $DYNTLS_DNS_TSIG -z $DYNTLS_DNS_ZONE" 1
|
||
else
|
||
_log "No wildcard in SAN list; using http-01 challenge with token dir '$DYNTLS_ENCRYPT_TOKEN_DIR'." 1
|
||
DYNTLS_DNS_OPTIONS="$DYNTLS_DNS_OPTIONS -w $DYNTLS_ENCRYPT_TOKEN_DIR"
|
||
fi
|
||
|
||
DYNTLS_DNS_OPTIONS="$DYNTLS_DNS_OPTIONS -c $out_file_tmp"
|
||
|
||
_log "Final ACME options string: $DYNTLS_DNS_OPTIONS" 1
|
||
|
||
########################################
|
||
# 3. Run pre-commands if defined
|
||
########################################
|
||
if [ -n "$DYNTLS_CMD_PRE_LIST" ]; then
|
||
for entry in $(echo "$DYNTLS_CMD_PRE_LIST" | sed 's/&/\n/g'); do
|
||
cmd=$(echo "$entry" | cut -d: -f1 | sed 's/%20/ /g')
|
||
remark=$(echo "$entry" | cut -d: -f2- | sed 's/%20/ /g')
|
||
[ -n "$cmd" ] && { eval "$cmd" && _log "$remark" 1; }
|
||
done
|
||
fi
|
||
|
||
########################################
|
||
# 4. Call LE script
|
||
########################################
|
||
_log "Executing ACME client: $DYNTLS/$DYNTLS_LE_PROGRAM sign $DYNTLS_DNS_OPTIONS $DYNTLS_SAN_LIST" 1
|
||
MyLEError=$(eval "$DYNTLS/$DYNTLS_LE_PROGRAM sign $DYNTLS_DNS_OPTIONS $DYNTLS_SAN_LIST 2>&1")
|
||
RetVal=$?
|
||
|
||
_log "ACME client returned code: $RetVal" 1
|
||
[ -n "$MyLEError" ] && _log "ACME client output: $MyLEError" 1
|
||
|
||
########################################
|
||
# 5. Run post-commands if defined
|
||
########################################
|
||
if [ -n "$DYNTLS_CMD_POST_LIST" ]; then
|
||
for entry in $(echo "$DYNTLS_CMD_POST_LIST" | sed 's/&/\n/g'); do
|
||
cmd=$(echo "$entry" | cut -d: -f1 | sed 's/%20/ /g')
|
||
remark=$(echo "$entry" | cut -d: -f2- | sed 's/%20/ /g')
|
||
[ -n "$cmd" ] && { eval "$cmd" && _log "$remark" 1; }
|
||
done
|
||
fi
|
||
|
||
########################################
|
||
# 6. Validate and install new certificate
|
||
########################################
|
||
if [ "$DYNTLS_PRODUCTIVE" -eq 1 ] && [ $RetVal -ne 0 ]; then
|
||
_log "Error issuing certificate: $MyLEError" 4
|
||
rm -f "$out_file_tmp"
|
||
elif [ -f "$out_file_tmp" ]; then
|
||
_log "Temporary certificate file exists, starting validation." 1
|
||
if openssl x509 -checkend $(($DYNTLS_PKI_CERT_EXPIRE*86400)) -noout -in "$out_file_tmp"; then
|
||
_log "The verification of the new certificate was successful. The certificate seems to be valid and it is moved to its destination folder." "2"
|
||
_log "New certificate meets minimum validity window (${DYNTLS_PKI_CERT_EXPIRE} days)." 1
|
||
if [ "$DYNTLS_PRODUCTIVE" -eq 1 ]; then
|
||
# Backup old cert
|
||
BackupDir="$DYNTLS_PKI_HTTP_CERT_BACKUP_DIR/$DYNTLS_MEMBER_HOSTNAME"
|
||
_log "Using backup directory: $BackupDir" 1
|
||
mkdir -p "$BackupDir" && chmod 750 "$BackupDir" && chown root:root "$BackupDir"
|
||
[ -f "$DYNTLS_DOMAIN_TARGET_CERT" ] && {
|
||
_log "Backup the expired certificate '$DYNTLS_DOMAIN_TARGET_CERT' to '$BackupDir/'." "2"
|
||
cp "$DYNTLS_DOMAIN_TARGET_CERT" "$BackupDir/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_CERT_SUFFIX-$(date +%y%m%d)"
|
||
}
|
||
|
||
# Fix backup directory permissions and ownership recursively
|
||
find "$BackupDir" -type d -exec chown root:root {} \;
|
||
find "$BackupDir" -type d -exec chmod 750 {} \;
|
||
find "$BackupDir" -type f -exec chown root:root {} \;
|
||
find "$BackupDir" -type f -exec chmod 640 {} \;
|
||
|
||
# Clean old backups if enabled
|
||
if [ -n "$DYNTLS_BACKUP_EXPIRATION" ] && [ "$DYNTLS_BACKUP_EXPIRATION" -gt 0 ]; then
|
||
_log "Removing backup files older than $DYNTLS_BACKUP_EXPIRATION days in $BackupDir" 2
|
||
find "$BackupDir" -type f -name "*.$DYNTLS_PKI_CERT_SUFFIX-*" -mtime +$DYNTLS_BACKUP_EXPIRATION -exec rm -f {} \;
|
||
if [ -d "$BackupDir" ] && [ -z "$(find "$BackupDir" -type f -name "*.$DYNTLS_PKI_CERT_SUFFIX-*")" ]; then
|
||
_log "Backup directory $BackupDir is empty; removing it." 2
|
||
rmdir "$BackupDir"
|
||
fi
|
||
else
|
||
_log "Backup expiration disabled (DYNTLS_BACKUP_EXPIRATION=$DYNTLS_BACKUP_EXPIRATION). Skipping cleanup." 2
|
||
fi
|
||
|
||
# Install new cert
|
||
_log "Moving issued certificate to target: $DYNTLS_DOMAIN_TARGET_CERT" 2
|
||
mv "$out_file_tmp" "$DYNTLS_DOMAIN_TARGET_CERT"
|
||
|
||
# Create fullchain file choosing correct chain (R12 vs R13)
|
||
issuer_CN=$(openssl x509 -noout -issuer -in "$DYNTLS_DOMAIN_TARGET_CERT" | sed -n 's/^issuer=.*CN=//p')
|
||
_log "Detected issuer CN for chain selection: $issuer_CN" 1
|
||
chainFile="$DYNTLS_PKI_LECA_R13_CHAIN"
|
||
|
||
[ "$issuer_CN" = "R12" ] && chainFile="$DYNTLS_PKI_LECA_R12_CHAIN"
|
||
_log "Using chain file: $chainFile" 1
|
||
fullchain_path="$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_FULLCHAIN_SUFFIX"
|
||
_log "Creating fullchain file: $fullchain_path" 1
|
||
cat "$DYNTLS_DOMAIN_TARGET_CERT" "$chainFile" > "$fullchain_path"
|
||
chmod 640 "$DYNTLS_PKI_HTTP_CERT_DIR"/*.pem*
|
||
|
||
# Now copy or link the server key AFTER cert is issued
|
||
KeyFile="$DYNTLS_PKI_HTTP_KEY_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_KEY_SUFFIX"
|
||
_log "Planned server key path: $KeyFile" 1
|
||
|
||
if [ "$DYNTLS_PKI_KEY_LNS" -eq 1 ]; then
|
||
_log "Linking base server key to: $KeyFile" 1
|
||
ln -sf "$DYNTLS_PKI_SERVER_BASEKEY" "$KeyFile"
|
||
else
|
||
_log "Copying dedicated server key to: $KeyFile" 1
|
||
cp -a "$DYNTLS_PKI_SERVER_BASEKEY" "$KeyFile"
|
||
fi
|
||
|
||
export DYNTLS_RELOAD_WEBSERVER="true"
|
||
_log "Certificate installation complete, webserver reload requested." 1
|
||
|
||
# Provide to services if requested
|
||
[ -n "$DYNTLS_DOMAINSERVICE_LIST" ] && _ProvideCertDomainService
|
||
else
|
||
_log "Staging mode: certificate issued but not installed." 2
|
||
rm -f "$out_file_tmp"
|
||
export DYNTLS_RELOAD_WEBSERVER="false"
|
||
fi
|
||
else
|
||
_log "Issued certificate does not satisfy validity window; removing temporary file." 4
|
||
rm -f "$out_file_tmp"
|
||
fi
|
||
fi
|
||
|
||
unset DYNTLS_DNS_OPTIONS
|
||
|
||
_log "Leave the function '_create_cert()'..." 1
|
||
|
||
return 0
|
||
} #=> _create_cert()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Reload webserver configuration to activate new certs
|
||
#
|
||
# Runs apachectl configtest to validate syntax,
|
||
# reloads httpd service if config OK, else logs error and aborts.
|
||
#
|
||
# Returns 0 on success or nonzero on failure.
|
||
_reload_webserver() {
|
||
_log "Enter the function '_reload_webserver()'..." 1
|
||
_log "\n----------------------------------------------\n" 6
|
||
|
||
########################################
|
||
# 1. Check Apache config syntax
|
||
########################################
|
||
MySyntaxCheckResult=$(apachectl configtest 2>&1)
|
||
|
||
if [ "$MySyntaxCheckResult" = "Syntax OK" ]; then
|
||
_log "Apache config syntax OK." 2
|
||
|
||
########################################
|
||
# 2. Reload httpd service
|
||
########################################
|
||
if systemctl reload httpd; then
|
||
_log "Webserver reloaded successfully with new config." 2
|
||
else
|
||
_log "Error: Failed to reload httpd service!" 4
|
||
MyIsError=true
|
||
fi
|
||
|
||
else
|
||
########################################
|
||
# Invalid config
|
||
########################################
|
||
_log "Apache configtest failed: $MySyntaxCheckResult" 4
|
||
_log "There is a problem with configured vHosts. Reload aborted." 4
|
||
MyIsError=true
|
||
fi
|
||
|
||
_log "Leave the function '_reload_webserver()'..." 1
|
||
|
||
return 0
|
||
} #=> _reload_webserver()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Add a new certificate for a domain or SAN list
|
||
#
|
||
# Normalizes DYNTLS_DOMAINS input,
|
||
# calls _SetupDomainSANList and _create_cert,
|
||
# reloads webserver if certificates were updated.
|
||
#
|
||
# Exits the script upon completion.
|
||
_add_cert() {
|
||
_log "Enter the function '_add_cert()'..." 1
|
||
_log "Domains: $DYNTLS_DOMAINS" 1
|
||
|
||
# Initialize a fresh domainset ID for this explicit add-cert run
|
||
# Uses the full DYNTLS_DOMAINS string (CN and all SANs) as hash source
|
||
_init_domainset_id "$DYNTLS_DOMAINS"
|
||
|
||
# Setup SAN list
|
||
_SetupDomainSANList
|
||
|
||
# Create the certificate
|
||
_create_cert
|
||
|
||
# Reload webserver if certificates were updated
|
||
if $DYNTLS_RELOAD_WEBSERVER; then
|
||
_reload_webserver
|
||
else
|
||
_log "Webserver reload not required (DYNTLS_RELOAD_WEBSERVER=false)" 2
|
||
fi
|
||
|
||
_log "Leave the function '_add_cert()'..." 1
|
||
|
||
exit 0
|
||
} #=> _add_cert()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Check whether a certificate exists for a given hostname
|
||
#
|
||
# Validates the hostname,
|
||
# lists matching cert files in PKI directory,
|
||
# logs results,
|
||
# then exits.
|
||
_check_cert() {
|
||
_log "Enter the function '_check_cert()'..." 1
|
||
|
||
# Verify FQHN
|
||
_Hostname "check" "hostname"
|
||
|
||
if [ "$?" -ne 0 ]; then
|
||
_log "The provided hostname '$DYNTLS_MEMBER_HOSTNAME' is not a valid FQHN. Aborting check." 3
|
||
_log "Leave the function '_check_cert()'..." 1
|
||
exit 1
|
||
fi
|
||
|
||
# Check certificates
|
||
_log "Checking for certificate file(s) in $DYNTLS_PKI_HTTP_CERT_DIR..." 1
|
||
|
||
for MyPath in "$DYNTLS_PKI_HTTP_CERT_DIR"/*."$DYNTLS_PKI_CERT_SUFFIX"; do
|
||
[ -f "$MyPath" ] || continue
|
||
MyCert="${MyPath##*/}"
|
||
_log "Found certificate file: $MyCert" 1
|
||
if openssl x509 -noout -subject -in "$MyPath" 2>/dev/null | grep -q "$DYNTLS_MEMBER_HOSTNAME"; then
|
||
_log "Certificate '$MyCert' contains CN/SAN for hostname '$DYNTLS_MEMBER_HOSTNAME'." 2
|
||
else
|
||
_log "Certificate '$MyCert' does not match hostname '$DYNTLS_MEMBER_HOSTNAME'." 3
|
||
fi
|
||
done
|
||
|
||
# Add a blank line after finishing one domain's check
|
||
printf '\n' >&2
|
||
|
||
_log "Leave the function '_check_cert()'..." 1
|
||
|
||
exit 0
|
||
} #=> _check_cert()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Remove a certificate entry for a hostname
|
||
#
|
||
# Validates hostname,
|
||
# checks HostnameDB for existence,
|
||
# removes it if found,
|
||
# and logs outcome.
|
||
_remove_cert() {
|
||
_log "Enter the function '_remove_cert()'..." 1
|
||
|
||
# Verify FQHN
|
||
_Hostname "check" "hostname"
|
||
|
||
if [ "$?" -ne 0 ]; then
|
||
_log "The provided hostname '$DYNTLS_MEMBER_HOSTNAME' is not a valid FQHN. Cannot proceed." 3
|
||
_log "Leave the function '_remove_cert()'..." 1
|
||
|
||
exit 1
|
||
fi
|
||
|
||
# Check HostnameDB
|
||
_HostnameDB "check"
|
||
if [ "$?" -eq 0 ]; then
|
||
# Remove hostname
|
||
_log "Hostname '$DYNTLS_MEMBER_HOSTNAME' found. Removing from HostnameDB..." 1
|
||
|
||
_HostnameDB "remove"
|
||
|
||
_log "Hostname '$DYNTLS_MEMBER_HOSTNAME' successfully removed." 2
|
||
else
|
||
_log "Cannot remove hostname '$DYNTLS_MEMBER_HOSTNAME'. It does not exist in HostnameDB." 2
|
||
fi
|
||
|
||
_log "Leave the function '_remove_cert()'..." 1
|
||
|
||
exit 0
|
||
} #=> _remove_cert()
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Check and renew certificates if missing/expiring soon
|
||
#
|
||
# Loops over DYNTLS_DOMAIN_LIST entries,
|
||
# validates CN format,
|
||
# builds SAN list,
|
||
# checks cert validity,
|
||
# renews if necessary,
|
||
# reloads webserver if updated,
|
||
# optionally sends email notifications.
|
||
#
|
||
# Exits upon completion.
|
||
_update_cert() {
|
||
_log "Enter the function '_update_cert()'..." 1
|
||
|
||
###########################
|
||
# 1. Calculate expiration window
|
||
###########################
|
||
MyExpSeconds=$((DYNTLS_PKI_CERT_EXPIRE*86400))
|
||
MyNowDate=$(date +%s)
|
||
let MyTestTime=$MyNowDate+$MyExpSeconds
|
||
|
||
_log "Expiration threshold: ${DYNTLS_PKI_CERT_EXPIRE} days (~until $(date --date="@$MyTestTime" '+%Y-%m-%d'))" 2
|
||
_log "Domain list: $DYNTLS_DOMAIN_LIST" 1
|
||
|
||
# Add a blank line after finishing one domain's check
|
||
printf '\n' >&2
|
||
|
||
###########################
|
||
# 2. Loop over domain sets
|
||
###########################
|
||
DomainList=$(echo "$DYNTLS_DOMAIN_LIST" | sed 's/&/\n/g')
|
||
|
||
for DomainEntry in $DomainList; do
|
||
# Initialize a fresh domainset ID for this specific CN+SAN set
|
||
# This ID will be used as log prefix for all nested calls (_create_cert, _SetupDomainSANList, etc.)
|
||
_init_domainset_id "$DomainEntry"
|
||
|
||
_log "Checking domain entry: $DomainEntry" 1
|
||
|
||
CN=$(echo "$DomainEntry" | cut -d: -f1)
|
||
_log "Check the CN domain '$CN'..." 2
|
||
export DYNTLS_MEMBER_HOSTNAME="$CN"
|
||
export DYNTLS_DOMAINS="$DomainEntry"
|
||
|
||
###########################
|
||
# 2a. Validate DYNTLS_MEMBER_HOSTNAME
|
||
###########################
|
||
if ! _Hostname "check" "hostname" ; then
|
||
_log "Skipping invalid CN '$DYNTLS_MEMBER_HOSTNAME' (not FQHN)" 3
|
||
continue
|
||
fi
|
||
|
||
###########################
|
||
# 2b. Setup SAN list
|
||
###########################
|
||
_SetupDomainSANList
|
||
|
||
###########################
|
||
# 2c. Locate existing certificate
|
||
###########################
|
||
#export DYNTLS_DOMAIN_TARGET_CERT="$DYNTLS_PKI_HTTP_CERT_DIR/$CN.$DYNTLS_PKI_CERT_SUFFIX"
|
||
export DYNTLS_DOMAIN_TARGET_CERT="$DYNTLS_PKI_HTTP_CERT_DIR/$DYNTLS_MEMBER_HOSTNAME.$DYNTLS_PKI_CERT_SUFFIX"
|
||
|
||
if [ ! -f "$DYNTLS_DOMAIN_TARGET_CERT" ]; then
|
||
_log "No existing certificate for '$DYNTLS_MEMBER_HOSTNAME'. Will create new one." 3
|
||
_create_cert
|
||
continue
|
||
fi
|
||
|
||
_log "Found existing cert file: $DYNTLS_DOMAIN_TARGET_CERT" 1
|
||
|
||
###########################
|
||
# 2d. Check certificate validity
|
||
###########################
|
||
EndDateStr=$(openssl x509 -noout -dates -in "$DYNTLS_DOMAIN_TARGET_CERT" | grep notAfter | sed -e "s/^.*notAfter=//")
|
||
if [ -z "$EndDateStr" ]; then
|
||
_log "Cert $DYNTLS_DOMAIN_TARGET_CERT invalid or corrupted. Reissue required." 4
|
||
_create_cert
|
||
continue
|
||
fi
|
||
|
||
Expiry=$(date --date="$EndDateStr" +%s)
|
||
ExpiryIso=$(date --date="@$Expiry" '+%Y-%m-%d %H:%M:%S')
|
||
DaysLeft=$(( (Expiry - MyNowDate) / 86400 ))
|
||
_log "CN '$DYNTLS_MEMBER_HOSTNAME' cert expires on $ExpiryIso (~$DaysLeft days left)" 2
|
||
|
||
if openssl x509 -checkend $MyExpSeconds -noout -in "$DYNTLS_DOMAIN_TARGET_CERT"; then
|
||
_log "CN '$DYNTLS_MEMBER_HOSTNAME' cert still valid beyond threshold." 2
|
||
#continue # Commented out deliberately to execute blank line after every domain
|
||
else
|
||
_log "CN '$DYNTLS_MEMBER_HOSTNAME' cert will expire within $DYNTLS_PKI_CERT_EXPIRE days > renewing." 2
|
||
_create_cert
|
||
fi
|
||
|
||
# Add a blank line after finishing one domain's check
|
||
printf '\n' >&2
|
||
done
|
||
|
||
###########################
|
||
# 3. Reload web server if needed
|
||
###########################
|
||
if $DYNTLS_RELOAD_WEBSERVER; then
|
||
_reload_webserver
|
||
fi
|
||
|
||
###########################
|
||
# 4. Mail notification if enabled
|
||
###########################
|
||
if $DYNTLS_SEND_MAIL; then
|
||
Subject="Certificate check on $HOSTNAME"
|
||
$MyIsError && Subject="ERROR: $Subject"
|
||
echo -e "$MyMailText" | mail -s "$Subject" -r "$MyMailFrom" "$MyMailAddresses"
|
||
fi
|
||
|
||
_log "Leave the function '_update_cert()'..." 1
|
||
|
||
exit 0
|
||
} #=> _update_cert()
|
||
|
||
|
||
|
||
########################################
|
||
# Invocation entry point:
|
||
|
||
NL='
|
||
'
|
||
|
||
# Be secure with a restrictive umask
|
||
[ -z "$DYNTLS_NO_UMASK" ] && umask 077
|
||
|
||
# Ignore some env vars
|
||
DYNTLS_PASSIN=
|
||
DYNTLS_PASSOUT=
|
||
|
||
# Parse options
|
||
#echo "$@"
|
||
while :; do
|
||
opt="${1%%=*}"
|
||
val="${1#*=}"
|
||
|
||
case "$opt" in
|
||
--days)
|
||
# Accept --days=VAL or --days VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_PKI_CERT_EXPIRE="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_PKI_CERT_EXPIRE="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --days" 1
|
||
fi
|
||
fi
|
||
;;
|
||
--hostnames|-H)
|
||
# Accept --hostnames=VAL or --hostnames VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_DOMAINS="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_DOMAINS="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --hostnames/-H" 1
|
||
fi
|
||
fi
|
||
# Normalize domain string
|
||
# Replace whitespaces with ':' to keep structured domain list
|
||
# export DYNTLS_DOMAINS=$(echo "$DYNTLS_DOMAINS" | sed -e 's/ */:/g')
|
||
export DYNTLS_DOMAINS=$(echo "$DYNTLS_DOMAINS" | tr -s ' ' ':')
|
||
;;
|
||
--key|-K)
|
||
# Accept --key=VAL or --key VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_BIND_ZONE_KEY="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_BIND_ZONE_KEY="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --key/-K" 1
|
||
fi
|
||
fi
|
||
;;
|
||
--key-force-renew)
|
||
# Accept --key-force-renew=VAL or --key-force-renew VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_PKI_KEY_FORCE_RENEW="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_PKI_KEY_FORCE_RENEW="$2"
|
||
shift 2
|
||
else
|
||
echo "Error: --key-force-renew requires an argument (1 or 0)"
|
||
exit 1
|
||
fi
|
||
fi
|
||
;;
|
||
--productive|-P)
|
||
export DYNTLS_PRODUCTIVE="1"
|
||
shift
|
||
;;
|
||
--DNS|-D)
|
||
# Accept --DNS=VAL or --DNS VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_DNS_SERVER="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_DNS_SERVER="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --DNS/-D" 1
|
||
fi
|
||
fi
|
||
;;
|
||
--tsig|-T)
|
||
# Accept --tsig=VAL or --tsig VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_DNS_TSIG="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_DNS_TSIG="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --tsig/-T" 1
|
||
fi
|
||
fi
|
||
;;
|
||
--vars)
|
||
# Accept --vars=VAL or --vars VAL
|
||
if [ "${1#*=}" != "$1" ]; then
|
||
export DYNTLS_VARS_FILE="$val"
|
||
shift
|
||
else
|
||
if [ -n "$2" ] && [ "${2%%--*}" = "$2" ]; then
|
||
export DYNTLS_VARS_FILE="$2"
|
||
shift 2
|
||
else
|
||
_die "Missing value for --vars" 1
|
||
fi
|
||
fi
|
||
;;
|
||
*)
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
|
||
# SDuesterhaupt: 2019-07-16 - Intelligent env-var detection and auto-loading
|
||
_vars_setup
|
||
|
||
# SDuesterhaupt: 2019-07-28 - Create the log directory
|
||
_init_log
|
||
|
||
|
||
# SDuesterhaupt: 2019-07-16 - Register cleanup on EXIT
|
||
trap "cleanup" EXIT
|
||
|
||
# SDuesterhaupt: 2019-07-16 - When SIGHUP, SIGINT, SIGQUIT, SIGABRT and SIGTERM,
|
||
# explicitly exit to signal EXIT (non-bash shells)
|
||
trap "exit 1" 1
|
||
trap "exit 2" 2
|
||
trap "exit 3" 3
|
||
trap "exit 6" 6
|
||
trap "exit 14" 15
|
||
|
||
|
||
# SDuesterhaupt: 2025-09-14 - Validate required parameters for commands
|
||
#
|
||
# Checks mandatory options for commands like add-cert,
|
||
# fails with clear error if missing.
|
||
#
|
||
# @param1: Command string (e.g., "add-cert")
|
||
_validate_command_params() {
|
||
local cmd="$1"
|
||
|
||
case "$cmd" in
|
||
add-cert)
|
||
# Requires --hostnames / DYNTLS_DOMAINS to be set (one or more domains/SAN)
|
||
if [ -z "$DYNTLS_DOMAINS" ]; then
|
||
_die "'--hostnames' (DYNTLS_DOMAINS) option is required for add-cert command." 1
|
||
fi
|
||
;;
|
||
check-cert)
|
||
# Requires --hostnames / DYNTLS_DOMAINS (domain to check)
|
||
if [ -z "$DYNTLS_DOMAINS" ]; then
|
||
_die "'--hostnames' (DYNTLS_DOMAINS) option is required for check-cert command." 1
|
||
fi
|
||
;;
|
||
remove-cert)
|
||
# Requires --hostnames / DYNTLS_DOMAINS (domain to remove)
|
||
if [ -z "$DYNTLS_DOMAINS" ]; then
|
||
_die "'--hostnames' (DYNTLS_DOMAINS) option is required for remove-cert command." 1
|
||
fi
|
||
;;
|
||
update-cert)
|
||
# No mandatory CLI options, but could add checks if your workflow requires them.
|
||
# Example: Validate DYNTLS_DOMAIN_LIST is set:
|
||
# if [ -z "$DYNTLS_DOMAIN_LIST" ]; then
|
||
# _die "'DYNTLS_DOMAIN_LIST' is required for update-cert command." 1
|
||
# fi
|
||
;;
|
||
init-pki)
|
||
# No mandatory parameters; PKI configuration is handled in _verify_pki_init
|
||
;;
|
||
*)
|
||
# Unknown or unsupported command; handled elsewhere.
|
||
;;
|
||
esac
|
||
}
|
||
|
||
|
||
# SDuesterhaupt: 2019-07-16 - determine how we were called, then hand off to the function responsible
|
||
cmd="$1"
|
||
[ -n "$1" ] && shift # scrape off command
|
||
|
||
# Validate required parameters for the command
|
||
_validate_command_params "$cmd"
|
||
|
||
case "$cmd" in
|
||
#init-pki|clean-all)
|
||
init-pki)
|
||
_init_pki "$@"
|
||
;;
|
||
add-cert)
|
||
# SDuesterhaupt: 2019-09-11 - Verify the PKI directories
|
||
_verify_pki_init
|
||
_add_cert "$@"
|
||
;;
|
||
check-cert)
|
||
# SDuesterhaupt: 2019-09-11 - Verify the PKI directories
|
||
_verify_pki_init
|
||
_check_cert "$@"
|
||
;;
|
||
remove-cert)
|
||
# SDuesterhaupt: 2019-09-11 - Verify the PKI directories
|
||
_verify_pki_init
|
||
_remove_cert "$@"
|
||
;;
|
||
update-cert)
|
||
# SDuesterhaupt: 2019-09-11 - Verify the PKI directories
|
||
_verify_pki_init
|
||
_update_cert "$@"
|
||
;;
|
||
""|help|-h|--help|--usage)
|
||
_cmd_help "$1"
|
||
exit 0
|
||
;;
|
||
*)
|
||
_die "Unknown command '$cmd'. Run without commands for usage help." "1"
|
||
;;
|
||
esac
|
||
|
||
# vim: ft=sh nu ai sw=8 ts=8 noet
|