diff --git a/script/ssl/Dockerfile b/script/ssl/Dockerfile new file mode 100644 index 0000000..bad10b7 --- /dev/null +++ b/script/ssl/Dockerfile @@ -0,0 +1,20 @@ +########################################################################### +# Dockerfile to build a Container to update TLS Certificates automatically. +# Based on latest Ubuntu LTS +########################################################################### +# See https://hub.docker.com/_/ubuntu +FROM ubuntu:latest + +# Update repositories +RUN echo Version 20251030v1 +RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y apt-utils + +#### BEGIN INSTALLATION ################################################### +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y cron curl dnsutils openssh-client + +ADD acme.sh-3.1.1.tar.gz /tmp/acme.sh-setup/ +COPY renewCerts.sh /renewCerts.sh +COPY start.sh /start.sh + +# Run the command on container startup +CMD ["bash", "/start.sh"] diff --git a/script/ssl/README.md b/script/ssl/README.md new file mode 100644 index 0000000..bff5ab1 --- /dev/null +++ b/script/ssl/README.md @@ -0,0 +1,52 @@ +Issuing SSL certificates +======================== +There are two modes you can use the script `renewCerts.sh`. + +1. dns +2. http + + + +Dns mode +-------- +This mode is meant to use inside a docker container defined by the `Dockerfile`. +To configure, build and run the Container there is a file `docker-compose.yml.template`. +You can copy this file to `docker-compose.yml` and set the needed environment variables there. + +- __AUTOACME_CONTAINER_HOSTNAME__ + is used to enable the use of the host name within the container. + For example, for meaningful commit messages. +- __AUTOACME_GIT_REPOSITORY_VIA_SSH__ (optional) + is used to specify a Git repository to which the keys and certificates are transferred. + Therefore, SSH keys are generated on first launch (`docker compose up -d`) and the repository is cloned to `~/acmeResults/`. + The public key must be granted __write access__ to the repository + (e.g. as repository's deploy key). + The key can be viewed via `docker compose logs`. +- __AUTOACME_PATH_IN_GIT_REPOSITORY__ (optional) + specifies a path inside the repository were the certiticates are saved. + (e.g. AUTOACME_PATH_IN_GIT_REPOSITORY="/foo/bar/" => /root/autoACME/foo/bar/your-domain.net/fullchain.crt) +- __AUTOACME_DNS_PROVIDER__ + sets the provider modul of acme.sh used to communicate with your domain provider. + (For further information see: https://github.com/acmesh-official/acme.sh/wiki/dnsapi) + +You may have to set additional environment variables depending on your provider... + + + +### Manual docker commands +Instead of using `docker compose` you can build and run the container manually: +``` +docker build -t cis/autoacme . +docker run --name autoacme -d cis/autoacme +``` +This may be useful for investiagtion... + + +Http mode +--------- +If you plan to use `renewCerts.sh` directly on your host computer this mode may fit your needs. +Here you need a `nginx` webserver. The domain have to point to it and following configuration is needed: + +1. The content of folder `/var/www/letsencrypt/.well-known/acme-challenge/` has to be accessable via `http://your-domain.net/.well-known/acme-challenge/` +2. The certificates are stored to `/etc/nginx/ssl`. If this folder is a git repository then changes will be commited and pushed. +3. An entry into the crontab is needed to do automatic updates. diff --git a/script/ssl/acme.sh-3.1.1.tar.gz b/script/ssl/acme.sh-3.1.1.tar.gz new file mode 100644 index 0000000..ac03c0f Binary files /dev/null and b/script/ssl/acme.sh-3.1.1.tar.gz differ diff --git a/script/ssl/docker-compose.yml.template b/script/ssl/docker-compose.yml.template new file mode 100644 index 0000000..bc66545 --- /dev/null +++ b/script/ssl/docker-compose.yml.template @@ -0,0 +1,18 @@ +services: + autoacme: + container_name: autoacme + image: cis/autoacme + build: . + restart: unless-stopped + volumes: + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + environment: + AUTOACME_CONTAINER_HOSTNAME: ${HOSTNAME:?"HINT - You may run 'export HOSTNAME' first."} + AUTOACME_GIT_REPOSITORY_VIA_SSH: 'ssh://git@git.your-domain.net/your-repo.git' + # Optionally you can set a path inside the git repository, requires a repository. + AUTOACME_PATH_IN_GIT_REPOSITORY: 'hosts/all/etc/ssl/domains/' + # See: https://github.com/acmesh-official/acme.sh/wiki/dnsapi and search your provider like 'hetzner' e.g. + AUTOACME_DNS_PROVIDER: 'dns_hetzner' + HETZNER_Token: 'your-token' + diff --git a/script/ssl/renewCerts.sh b/script/ssl/renewCerts.sh new file mode 100755 index 0000000..695f8ed --- /dev/null +++ b/script/ssl/renewCerts.sh @@ -0,0 +1,550 @@ +#!/bin/bash + +# curl http://your.domain.net/.well-known/acme-challenge/test +# curl http://85.183.145.8/.well-known/acme-challenge/test +# /var/www/letsencrypt/.well-known/acme-challenge + +function checkConfigViaHttp() { + local _DOMAIN _MODE _LOCAL_FOLDER + _MODE="${1:?"checkConfigViaHttp(): Missing first parameter MODE"}" + _DOMAIN="${2:?"checkConfigViaHttp(): Missing second parameter DOMAIN"}" + _LOCAL_FOLDER="/var/www/letsencrypt/.well-known/acme-challenge/" + readonly _DOMAIN _MODE _LOCAL_FOLDER + + local _LOCAL_FILE _LOCAL_URL _PUBLIC_URL + _LOCAL_FILE="${_LOCAL_FOLDER}${_DOMAIN}" + _LOCAL_URL="http://localhost/.well-known/acme-challenge/${_DOMAIN}" + _PUBLIC_URL="http://${_DOMAIN}/.well-known/acme-challenge/${_DOMAIN}" + readonly _LOCAL_FILE _LOCAL_URL _PUBLIC_URL + + # Skip check if mode is not http + [ "${_MODE}" != "http" ] \ + && return 0 + + # Fail because wildcard certificate + [ "${_MODE}" == "http" ] \ + && isWildcardCertificate "${_DOMAIN}" \ + && echo "Wildcard certificates are not supported via HTTP." \ + && return 1 + + _CHECK="Available on $(hostname)@$(date)" + + echo -n "Check domain '${_DOMAIN}'..." \ + && [ -d "${_LOCAL_FOLDER}" ] \ + && echo "${_CHECK}" > "${_LOCAL_FILE}" \ + && curl -4s "${_PUBLIC_URL}" | grep -q "${_CHECK}" \ + && echo " Done" \ + && return 0 + + echo + echo "FAILED: configuration of domain '${_DOMAIN}' is INCORRECT:" + echo -n " ${_PUBLIC_URL} was not found." + + curl -4s "${_LOCAL_URL}" | grep -q "${_CHECK}" \ + && echo " (check DNS first)" \ + || echo " (check Webserver first)" + + return 1 +} + +function isActive() { + local _DOMAIN _MODE _RESULT_CERTS + _RESULT_CERTS="${RESULT_CERTS:?"isActive(): Missing global parameter RESULT_CERTS"}" + _MODE="${1:?"isActive(): Missing first parameter MODE"}" + _DOMAIN="${2:?"isActive(): Missing second parameter DOMAIN"}" + readonly _DOMAIN _MODE _RESULT_CERTS + + # If mode is dns the domain is active always + [ "${_MODE}" == "dns" ] \ + && return 0 + + nginx -T 2> /dev/null | grep -q "${_RESULT_CERTS}${_DOMAIN}/fullchain.crt" \ + && return 0 + + echo "Domain '${_DOMAIN}' is inaktiv and therefore it will be skipped." + return 1 +} + +function isGitRepository() { + local _FOLDER + _FOLDER="${1:?"isGitRepository(): Missing first parameter FOLDER"}" + readonly _FOLDER + + git -C "${_FOLDER}" ls-tree main &> /dev/null \ + && return 0 + + return 1 +} + +function isWildcardCertificate() { + local _DOMAIN + _DOMAIN="${1:?"isWildcardCertificate(): Missing first parameter DOMAIN"}" + readonly _DOMAIN + + echo "${_DOMAIN}" | grep -q -F "_." \ + && return 0 + + echo "${_DOMAIN}" | grep -q -F "*." \ + && return 0 + + return 1 +} + +function tryGitPush() { + local _DOMAIN _NOW _RESULT_CERTS + _RESULT_CERTS="${RESULT_CERTS:?"tryGitPush(): Missing global parameter RESULT_CERTS"}" + _DOMAIN="${1:?"tryGitPush(): Missing first parameter DOMAIN"}" + _NOW="$(date +%Y%m%d_%H%M)" + readonly _DOMAIN _NOW _RESULT_CERTS + + ! isGitRepository "${_RESULT_CERTS}" \ + && echo \ + && echo "Folder '${_RESULT_CERTS}' is not part of a git repository, therefore nothing will be pushed." \ + && return 1 + + pushd "${_RESULT_CERTS}" > /dev/null + git pull > /dev/null + git add * > /dev/null + git commit -m "${_NOW} - Certificate for '${_DOMAIN}' was updated." \ + && git push > /dev/null \ + && popd > /dev/null \ + && echo "SUCCESS: certificate for '${_DOMAIN}' pushed." \ + && return 0 + + popd > /dev/null + echo "FAILED: unable to push certificate for '${_DOMAIN}'." + return 0 +} + +function own() { + ! [ -d "${RESULT_CERTS:?"own(): Missing global parameter RESULT_CERTS"}" ] \ + && echo "Trying to derive domain names from subfolders of '${RESULT_CERTS}', but it is not a folder!" \ + && return 1 + + local _DOMAINS _MODE + _DOMAINS=("${RESULT_CERTS}"*) + _MODE="${1:?"own(): Missing first parameter MODE"}" + readonly _DOMAINS _MODE + + local _domain + for _domain in "${_DOMAINS[@]}"; do + # just take names of folders + ! [ -d "${_domain}" ] && continue + # cut pfad (like basename) + _domain="${_domain##*/}" + + # folder default => skip + [ "${_domain}" == "default" ] && continue + + case "${_MODE}" in + dns) + # dns supports all options + ;; + http) + # http and wildcard certifikate => skip + isWildcardCertificate "${_domain}" && continue + # ssl on domain inaktiv => skip + ! isActive "{_MODE}" "${_domain}" && continue + ;; + *) + echo "Unknown mode: ${_MODE}" + return 1 + ;; + esac + + single "${_MODE}" "${_domain}" "${2}" + done + + return 0 +} + +function continueIssuingCertificate() { + local _CERT_FILE_FULLCHAIN _DOMAIN + _CERT_FILE_FULLCHAIN="${1:?"continueIssuingCertificate(): Missing first parameter CERT_FILE_FULLCHAIN"}" + _DOMAIN="${2:?"continueIssuingCertificate(): Missing second parameter DOMAIN"}" + local _CERT_FILE_FULLCHAIN _DOMAIN + + local _PRETTY_DOMAIN + _PRETTY_DOMAIN="$(printPrettyDomain ${_DOMAIN})" + readonly _PRETTY_DOMAIN + + # forced => should be issued + [ "${3:-""}" == "--force" ] \ + && echo "Certificate for domain '${_PRETTY_DOMAIN}' is forced to be issued." \ + && return 0 + + # no cert => should be issued + ! [ -f "${_CERT_FILE_FULLCHAIN}" ] \ + && echo "No certificate for domain '${_PRETTY_DOMAIN}', so it will be issued." \ + && return 0 + + local _ENDDATE _NOW _REMAINING_DAYS + _ENDDATE="$(openssl x509 -enddate -noout -in ${_CERT_FILE_FULLCHAIN} | cut -d= -f2)" + _ENDDATE="$(date --date="${_ENDDATE}" --utc +%s)" + + _NOW="$(date --date now +%s)" + _REMAINING_DAYS="$(( (_ENDDATE - _NOW) / 86400 ))" + readonly _ENDDATE _NOW _REMAINING_DAYS + + # less than 30 days remaining => should be issued + [ "${_REMAINING_DAYS}" -le "30" ] \ + && echo "Certificate for domain '${_PRETTY_DOMAIN}' (${_REMAINING_DAYS} days remaining) will be issued." \ + && return 0 + + echo "Certificate for domain '${_PRETTY_DOMAIN}' (${_REMAINING_DAYS} days remaining) will be skipped." + return 1 +} + +function printBaseDomain() { + local _DOMAIN + _DOMAIN="${1:?"printBaseDomain(): Missing first parameter DOMAIN"}" + readonly _DOMAIN + + local _BASE_DOMAIN + # cut front '*.' or '_.' + _BASE_DOMAIN="${_DOMAIN#\*.}" + _BASE_DOMAIN="${_BASE_DOMAIN#_.}" + readonly _BASE_DOMAIN + + echo "${_BASE_DOMAIN}" \ + && return 0 + + return 1 +} + +function printPrettyDomain() { + local _BASE_DOMAIN _DOMAIN + _DOMAIN="${1:?"printPrettyDomain(): Missing first parameter DOMAIN"}" + _BASE_DOMAIN="$(printBaseDomain ${_DOMAIN})" + readonly _BASE_DOMAIN _DOMAIN + + isWildcardCertificate "${_DOMAIN}" \ + && echo "*.${_BASE_DOMAIN}" \ + && return 0 + + echo "${_DOMAIN}" \ + && return 0 + + return 1 +} + +function printFullDomainFolder() { + local _BASE_DOMAIN _DOMAIN _RESULT_CERTS + _RESULT_CERTS="${RESULT_CERTS:?"printFullDomainFolder(): Missing global parameter RESULT_CERTS"}" + _DOMAIN="${1:?"printFullDomainFolder(): Missing first parameter DOMAIN"}" + _BASE_DOMAIN="$(printBaseDomain ${_DOMAIN})" + readonly _BASE_DOMAIN _DOMAIN _RESULT_CERTS + + isWildcardCertificate "${_DOMAIN}" \ + && echo "${_RESULT_CERTS}_.${_BASE_DOMAIN}/" \ + && return 0 + + echo "${_RESULT_CERTS}${_DOMAIN}/" \ + && return 0 + + return 1 +} + +function prepareFullDomainFolder() { + local _DOMAIN _DOMAIN_FOLDER + _DOMAIN="${1:?"prepareFullDomainFolder(): Missing first parameter DOMAIN"}" + _DOMAIN_FOLDER="$(printFullDomainFolder "${_DOMAIN}")" + readonly _DOMAIN _DOMAIN_FOLDER + + [ -d "${_DOMAIN_FOLDER}" ] \ + && return 0 + + # create folder for results + echo -n "Creating folder '${_DOMAIN_FOLDER}'... " \ + && mkdir -p "${_DOMAIN_FOLDER}" \ + && echo "Done" + + [ -d "${_DOMAIN_FOLDER}" ] \ + && return 0 + + return 1 +} + +function prepareAndCheckAliasDomain() { + local _ALIAS_DOMAIN _DOMAIN + _DOMAIN="${1:?"prepareAndCheckAliasDomain(): Missing first parameter DOMAIN"}" + _ALIAS_DOMAIN="${2:?"prepareAndCheckAliasDomain(): Missing second parameter ALIAS_DOMAIN"}" + readonly _ALIAS_DOMAIN _DOMAIN + + local _BASE_DOMAIN _CHALLENGE_ALIAS_DOMAIN_FILE _DOMAIN_FOLDER + _BASE_DOMAIN="$(printBaseDomain ${_DOMAIN})" + _DOMAIN_FOLDER="$(printFullDomainFolder ${_DOMAIN})" + _CHALLENGE_ALIAS_DOMAIN_FILE="${_DOMAIN_FOLDER}challenge-alias-domain" + readonly _BASE_DOMAIN _CHALLENGE_ALIAS_DOMAIN_FILE _DOMAIN_FOLDER + + [ -d "${_DOMAIN_FOLDER}" ] \ + && [ "$(dig +short _acme-challenge.${_BASE_DOMAIN} CNAME)" == "_acme-challenge.${_ALIAS_DOMAIN}." ] \ + && echo "${_ALIAS_DOMAIN}" > "${_CHALLENGE_ALIAS_DOMAIN_FILE}" \ + && echo "SUCCESS: alias domain '${_ALIAS_DOMAIN}' is used when issuing certificates for '${_BASE_DOMAIN}' via DNS." \ + && return 0 + + echo "FAILED: unable to use alias domain '${_ALIAS_DOMAIN}' to issue certificates for '${_BASE_DOMAIN}'." + echo " You have to configure your domain '${_BASE_DOMAIN}' first before you can use the alias domain as proof." + echo " So check if there is a CNAME entry '_acme-challenge.${_BASE_DOMAIN}' pointing to:" + echo " - '_acme-challenge.${_ALIAS_DOMAIN}'" + return 1 +} + +function single() { + local _ACME_FILE _DOMAIN _MODE _RESULT_CERTS + _RESULT_CERTS="${RESULT_CERTS:?"single(): Missing global parameter RESULT_CERTS"}" + _ACME_FILE="${ACME_FILE:?"single(): Missing global parameter ACME_FILE"}" + _MODE="${1:?"single(): Missing first parameter MODE"}" + _DOMAIN="${2:?"single(): Missing second parameter DOMAIN"}" + readonly _ACME_FILE _DOMAIN _MODE _RESULT_CERTS + + local _BASE_DOMAIN _CHALLENGE_ALIAS_DOMAIN_FILE _DOMAIN_FOLDER _PRETTY_DOMAIN + _BASE_DOMAIN="$(printBaseDomain ${_DOMAIN})" + _DOMAIN_FOLDER="$(printFullDomainFolder ${_DOMAIN})" + _PRETTY_DOMAIN="$(printPrettyDomain ${_DOMAIN})" + _CHALLENGE_ALIAS_DOMAIN_FILE="${_DOMAIN_FOLDER}challenge-alias-domain" + readonly _BASE_DOMAIN _CHALLENGE_ALIAS_DOMAIN_FILE _DOMAIN_FOLDER _PRETTY_DOMAIN + + ! [ -f "${_ACME_FILE}" ] \ + && echo "Program 'acme.sh' seams not to be installed. Try run 'renewCerts.sh --setup'." \ + && return 1 + + # cancel on broken configuration + ! checkConfigViaHttp "${_MODE}" "${_DOMAIN}" \ + && return 1 + + # cancel if folder is not prepared + ! [ -d "${_DOMAIN_FOLDER}" ] \ + && echo "Certificate of domain '${_PRETTY_DOMAIN}' skipped because of missing folder:" \ + && echo " - '${_DOMAIN_FOLDER}'" \ + && return 1 + + # check enddate if third parameter is not --force + ! continueIssuingCertificate "${_DOMAIN_FOLDER}fullchain.crt" "${_DOMAIN}" "${3:-""}" \ + && return 0 + + # backup the keys + [ -f "${_DOMAIN_FOLDER}fullchain.crt" ] \ + && cp "${_DOMAIN_FOLDER}fullchain.crt" "${_DOMAIN_FOLDER}fullchain.crt.bak" + [ -f "${_DOMAIN_FOLDER}private.key" ] \ + && cp --preserve=mode,ownership "${_DOMAIN_FOLDER}private.key" "${_DOMAIN_FOLDER}private.key.bak" + + local _OPTIONS + # always --force because we check expiring on ourself + # _OPTIONS="--issue --force --test" + _OPTIONS="--issue --force" + if [ "${_MODE}" == "dns" ]; then + _OPTIONS="${_OPTIONS} --dns ${AUTOACME_DNS_PROVIDER:?"single(): Missing global parameter AUTOACME_DNS_PROVIDER"}" + [ -f "${_CHALLENGE_ALIAS_DOMAIN_FILE}" ] \ + && _OPTIONS="${_OPTIONS} --challenge-alias $(cat "${_CHALLENGE_ALIAS_DOMAIN_FILE}")" + isWildcardCertificate "${_DOMAIN}" \ + && _OPTIONS="${_OPTIONS} --domain ${_PRETTY_DOMAIN}" + elif [ "${_MODE}" == "http" ]; then + _OPTIONS="${_OPTIONS} --webroot /var/www/letsencrypt" + fi + readonly _OPTIONS + + ${_ACME_FILE} ${_OPTIONS} \ + --domain "${_BASE_DOMAIN}" \ + --server "letsencrypt" \ + --keylength "ec-384" \ + --fullchain-file "${_DOMAIN_FOLDER}fullchain.crt" \ + --key-file "${_DOMAIN_FOLDER}private.key" \ + && openssl pkcs12 -export -in "${_DOMAIN_FOLDER}fullchain.crt" -inkey "${_DOMAIN_FOLDER}private.key" -out "${_DOMAIN_FOLDER}bundle.pkx" -passout pass: \ + && echo "Certificate of domain '${_PRETTY_DOMAIN}' was updated." \ + && tryGitPush "${_PRETTY_DOMAIN}" \ + && return 0 + + echo "Certificate of domain '${_PRETTY_DOMAIN}' remains unchanged." + return 0 +} + +function isInstalled() { + local _ACME_FILE + _ACME_FILE="${ACME_FILE:?"isInstalled(): Missing global parameter ACME_FILE"}" + readonly _ACME_FILE + + [ -f "${_ACME_FILE}" ] \ + && echo "Following version of acme.sh is installed:" \ + && echo "------------------------------------------" \ + && ${_ACME_FILE} --version | tail -n 1 \ + && return 0 + + return 1 +} + +function extractTarArchive() { + local _ACME_SETUP_FILE _ACME_TAR_FILE + _ACME_SETUP_FILE="${ACME_SETUP_FILE:?"extractTarArchive(): Missing global parameter ACME_SETUP_FILE"}" + _ACME_TAR_FILE="${ACME_TAR_FILE:?"extractTarArchive(): Missing global parameter ACME_TAR_FILE"}" + readonly _ACME_SETUP_FILE _ACME_TAR_FILE + + # extracted file already exists + [ -f "${_ACME_SETUP_FILE}" ] \ + && return 0 + + [ -f "${_ACME_TAR_FILE}" ] \ + && mkdir -p "/tmp/acme.sh-setup/" \ + && tar -xzf "${_ACME_TAR_FILE}" -C "/tmp/acme.sh-setup/" \ + && [ -f "${_ACME_SETUP_FILE}" ] \ + && return 0 + + echo "Missing setup file '${_ACME_SETUP_FILE}' after trying to extract '${_ACME_TAR_FILE}'" + return 1 +} + +function setup() { + local _ACME_SETUP_FILE + _ACME_SETUP_FILE="${ACME_SETUP_FILE:?"setup(): Missing global parameter ACME_SETUP_FILE"}" + readonly _ACME_SETUP_FILE + + isInstalled \ + && return 0 + + ! [ $(id -u) == 0 ] \ + && echo "Setup requires execution as user 'root'." \ + && exit 1 + + ! [ "$(echo $HOME)" == "/root" ] \ + && echo "The setup is executed with 'root' privileges but not in the 'root' user environment." \ + && exit 1 + + ! extractTarArchive \ + && exit 1 + + echo "Starting install of acme.sh:" + echo "----------------------------" + pushd "${_ACME_SETUP_FILE%/*}" > /dev/null 2>&1 #Removes shortest matching pattern '/*' from the end + ./acme.sh --install --no-cron --no-profile 2>&1 + popd > /dev/null 2>&1 + isInstalled \ + && echo \ + && echo 'Now this script can be added into cron-tab (crontab -e), like this e.g.:' \ + && echo \ + && echo '# Each day at 6:00am renew certificates:' \ + && echo '0 6 * * * /renewCerts.sh --http --own > /var/log/renewCerts.sh.log 2>&1' \ + && return 0 + + echo "Something went wrong during setup." + return 1 +} + +function usage() { + echo + echo 'Commands:' + echo ' --prepare DOMAIN --usingAlias ALIAS-DOMAIN : Prepares a domain to issue certificate using an alias domain in DNS mode.' + echo ' See: https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode' + echo ' --dns --single DOMAIN [--force] : Issues a certificate for the given domain using DNS mode.' + echo ' --http --single DOMAIN [--force] : Issues a certificate for the given domain using HTTP mode.' + echo + echo ' (--dns|--http) --own [--force] : Iterates all domains found in RESULT_CERTS.' + echo + echo 'Current environment:' + echo " Full name of this script: OWN_FULLNAME='${OWN_FULLNAME}'" + echo " Configuration:" + echo " Version of 'acme.sh' that will be installed: ACME_VERSION='${ACME_VERSION}'" + echo " Tar file containing the setup of 'acme.sh': ACME_TAR_FILE='${ACME_TAR_FILE}'" + echo " Setup file of 'acme.sh' after extraction: ACME_SETUP_FILE='${ACME_SETUP_FILE}'" + echo " Full name of the installed script 'acme.sh': ACME_FILE='${ACME_FILE}'" + echo " Output:" + echo " Path were the issued certificate are saved: RESULT_CERTS='${RESULT_CERTS}'" + + return 0 +} + +function main() { + + echo + + [ -f "/autoACME.env" ] \ + && source "/autoACME.env" \ + && echo "[$(date)] Environment '/autoACME.env' loaded." + + local ACME_FILE ACME_VERSION OWN_FULLNAME + OWN_FULLNAME="$(readlink -e ${0})" + ACME_FILE="/root/.acme.sh/acme.sh" + ACME_VERSION="acme.sh-3.1.1" + readonly ACME_FILE ACME_VERSION OWN_FULLNAME + + local ACME_SETUP_FILE ACME_TAR_FILE RESULT_CERTS + ACME_SETUP_FILE="/tmp/acme.sh-setup/${ACME_VERSION}/acme.sh" + ACME_TAR_FILE="${OWN_FULLNAME%/*}/${ACME_VERSION}.tar.gz" + RESULT_CERTS="${AUTOACME_RESULT_CERTS%/}" #Removes shortest matching pattern '/' from the end + RESULT_CERTS="${RESULT_CERTS:-"/etc/nginx/ssl"}/" + readonly ACME_SETUP_FILE ACME_TAR_FILE RESULT_CERTS + + local REPOSITORY_URL + isGitRepository "${RESULT_CERTS}" \ + && REPOSITORY_URL="$(git -C ${RESULT_CERTS} config --get remote.origin.url)" + readonly REPOSITORY_URL + + case "${1}" in + --dns) + case "${2}" in + --single) + echo "[$(date)] Issue single certificate '${3}' via DNS:" \ + && prepareFullDomainFolder "${3}" \ + && single "dns" "${3}" "${4}" \ + && return 0 + ;; + --own) + echo "[$(date)] Renewing own certificates via DNS:" + own "dns" "${3}" \ + && echo "Finished successfully." \ + && return 0 + ;; + esac + ;; + --http) + case "${2}" in + --single) + echo "[$(date)] Issue single certificate '${3}' via HTTP:" \ + && prepareFullDomainFolder "${3}" \ + && single "http" "${3}" "${4}" \ + && echo \ + && echo "Checking configuration of nginx and restart the webserver:" \ + && echo "==========================================================" \ + && nginx -t && systemctl reload nginx \ + && return 0 + ;; + --own) + echo "[$(date)] Renewing own certificates via HTTP:" \ + && own "http" "${3}" \ + && echo \ + && echo "Checking configuration of nginx and restart the webserver:" \ + && echo "==========================================================" \ + && nginx -t && systemctl reload nginx \ + && return 0 + ;; + esac + ;; + --prepare) + case "${3}" in + --usingAlias) + echo "[$(date)] Prepare domain '${2}' using the alias-domain '${4}' via DNS:" \ + && prepareFullDomainFolder "${2}" \ + && prepareAndCheckAliasDomain "${2}" "${4}" \ + && return 0 + ;; + *) + echo "Unknown command '${1}' '${2}' '${3}' '${4}'" + usage + return 1 + ;; + esac + ;; + --setup) + setup \ + && return 0 + ;; + *) + echo "Unknown command '${1}' '${2}'" + usage + return 1 + ;; + esac + + return 1 +} + +main "$@" && exit 0 || exit 1 diff --git a/script/ssl/start.sh b/script/ssl/start.sh new file mode 100755 index 0000000..288cafe --- /dev/null +++ b/script/ssl/start.sh @@ -0,0 +1,161 @@ +#/bin/bash + +function createEnvironmentFile() { + local _ENVIRONMENT_FILE _REPOSITORY_FOLDER + _ENVIRONMENT_FILE="${ENVIRONMENT_FILE:?"createEnvironmentFile(): Missing global parameter ENVIRONMENT_FILE"}" + _REPOSITORY_FOLDER="${AUTOACME_REPOSITORY_FOLDER:?"createEnvironmentFile(): Missing global parameter AUTOACME_REPOSITORY_FOLDER"}" + readonly _ENVIRONMENT_FILE _REPOSITORY_FOLDER + + # Save environment for cronjob + export -p | grep -v -E "(HOME|OLDPWD|PWD|SHLVL)" > "${_ENVIRONMENT_FILE}" \ + && echo "SUCCESS: there values were exported into file: '${_ENVIRONMENT_FILE}'" \ + && echo " - AUTOACME_CONTAINER_HOSTNAME: ${AUTOACME_CONTAINER_HOSTNAME}" \ + && echo " - AUTOACME_DNS_PROVIDER: ${AUTOACME_DNS_PROVIDER}" \ + && echo " - AUTOACME_GIT_REPOSITORY_VIA_SSH: ${AUTOACME_GIT_REPOSITORY_VIA_SSH}" \ + && echo " - AUTOACME_PATH_IN_GIT_REPOSITORY: ${AUTOACME_PATH_IN_GIT_REPOSITORY}" + + [ "${AUTOACME_GIT_REPOSITORY_VIA_SSH}" == "" ] \ + && echo "declare -x AUTOACME_RESULT_CERTS=\"${AUTOACME_REPOSITORY_FOLDER#/}\"" >> "${_ENVIRONMENT_FILE}" \ + && echo "SUCCESS: added AUTOACME_RESULT_CERTS (without git) into file '${_ENVIRONMENT_FILE}'." \ + && echo " - AUTOACME_RESULT_CERTS: ${AUTOACME_REPOSITORY_FOLDER#/}" \ + && echo " (depends on if there is a git repo and the path for the certs in it)" + + ! [ "${AUTOACME_GIT_REPOSITORY_VIA_SSH}" == "" ] \ + && echo "declare -x AUTOACME_RESULT_CERTS=\"${AUTOACME_REPOSITORY_FOLDER}${AUTOACME_PATH_IN_GIT_REPOSITORY#/}\"" >> "${_ENVIRONMENT_FILE}" \ + && echo "SUCCESS: added AUTOACME_RESULT_CERTS (with git) into file '${_ENVIRONMENT_FILE}'." \ + && echo " - AUTOACME_RESULT_CERTS: ${AUTOACME_REPOSITORY_FOLDER}${AUTOACME_PATH_IN_GIT_REPOSITORY#/}" \ + && echo " (depends on if there is a git repo and the path for the certs in it)" + + return 0 +} + +function ensureThereAreSSHKeys() { + grep -F 'ssh' "/root/.ssh/id_ed25519.pub" &> /dev/null \ + && echo "SUCCESS: ssh-keys found, printing public key:" \ + && cat "/root/.ssh/id_ed25519.pub" \ + && return 0 + + # -t type of the key pair + # -f defines the filenames (we use the standard for the selected type here) + # -q quiet, no output or interaction + # -N "" means the private key will not be secured by a passphrase + # -C defines a comment + ssh-keygen \ + -t ed25519 \ + -f "/root/.ssh/id_ed25519" -q -N "" \ + -C "$(date +%Y%m%d)-root@$(hostname -s)_onHost_${AUTOACME_CONTAINER_HOSTNAME%%.*}" + + grep -F 'ssh' "/root/.ssh/id_ed25519.pub" &> /dev/null \ + && echo "SUCCESS: ssh-keys generated, printing public key:" \ + && cat "/root/.ssh/id_ed25519.pub" \ + && return 0 + + echo + echo "FAILED: something went wrong during the generation of the ssh keys..." + echo " These keys are mandantory to access the git repository." + echo "You can try to restart this script." + echo + return 1 +} + +function ensureGitIsInstalled() { + git --version &> /dev/null \ + && return 0 + + echo \ + && echo "Installing Git in 30s (ensure the SSH-Key is trusted and has write pemissions)... " \ + && sleep 30 \ + && DEBIAN_FRONTEND=noninteractive \ + && apt-get install git -y &> /dev/null \ + && echo "SUCCESS: $(git --version) is usable now." \ + && return 0 + + echo + echo "FAILED: something went wrong during the installation of Git..." + echo " Git is mandantory to push the keys into the specified repository." + echo "You can try to install git manually (apt install git)." + echo + return 1 +} + +function ensureRepositoryIsAvailableAndWritable() { + local _REPOSITORY_FOLDER + _REPOSITORY_FOLDER="${AUTOACME_REPOSITORY_FOLDER:?"ensureRepositoryIsAvailableAndWritable(): Missing global parameter AUTOACME_REPOSITORY_FOLDER"}" + readonly _REPOSITORY_FOLDER + + [ -d "${_REPOSITORY_FOLDER}.git" ] \ + && echo \ + && git -C "${_REPOSITORY_FOLDER}" pull &> /dev/null \ + && git -C "${_REPOSITORY_FOLDER}" push --dry-run &> /dev/null \ + && echo "Writable repository found in folder '${_REPOSITORY_FOLDER}'." \ + && return 0 + + ! [ -d "${_REPOSITORY_FOLDER}.git" ] \ + && echo \ + && echo "Cloning repository '${AUTOACME_GIT_REPOSITORY_VIA_SSH}'... " \ + && GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new" git clone "${AUTOACME_GIT_REPOSITORY_VIA_SSH}" "${_REPOSITORY_FOLDER}" &> /dev/null \ + && git -C "${_REPOSITORY_FOLDER}" config user.name "autoacme on ${AUTOACME_CONTAINER_HOSTNAME%%.*}" \ + && git -C "${_REPOSITORY_FOLDER}" config user.email "autoacme@${AUTOACME_CONTAINER_HOSTNAME%%.*}" \ + && git -C "${_REPOSITORY_FOLDER}" push --dry-run &> /dev/null \ + && echo "SUCCESS: repository cloned into folder '${_REPOSITORY_FOLDER}' and it is writable." \ + && return 0 + + echo + echo "FAILED: something went wrong during cloning the repository to '${_REPOSITORY_FOLDER}' from:" + echo " - ${AUTOACME_GIT_REPOSITORY_VIA_SSH}" + echo + echo "1.) You can try to clone it manually into: git clone ${AUTOACME_GIT_REPOSITORY_VIA_SSH} '${_REPOSITORY_FOLDER}'" + echo "2.) Check if the repositoty is writable: git -C '${_REPOSITORY_FOLDER}' push --dry-run" + return 1 +} + +function prepareThisRuntimeForUsingGitOrIgnore() { + createEnvironmentFile \ + || return 1 + + [ "${AUTOACME_GIT_REPOSITORY_VIA_SSH}" == "" ] \ + && echo "There is no git repository specified." \ + && echo "To distribute all keys and certificates via a git repository set environment variable:" \ + && echo " - AUTOACME_GIT_REPOSITORY_VIA_SSH" \ + && echo \ + && echo "FIRST AND ONLY WARNING: DO NOT USE ANY PUBLIC GIT SERVICE FOR THAT!" \ + && echo \ + && return 0 + + echo \ + && ensureThereAreSSHKeys \ + && ensureGitIsInstalled \ + && ensureRepositoryIsAvailableAndWritable \ + && return 0 + + echo "No job will run inside this container because there is an issue." + echo "The container keeps running for 10min, please check your setup..." + return 1 +} + +AUTOACME_REPOSITORY_FOLDER="/root/acmeResults/" +ENVIRONMENT_FILE="/autoACME.env" + +echo +echo '################################################################################' +echo "# Container started at $(date +%F_%T) on host ${AUTOACME_CONTAINER_HOSTNAME}" +echo '################################################################################' +echo + +# Log start and truncate file: /autoACME.log +echo > /autoACME.log + +# Generate SSH keys and setup Git if a repository is specified, on failure keep the container running +prepareThisRuntimeForUsingGitOrIgnore \ + || timeout --preserve-status 10m tail -f /autoACME.log + +# Ensure acme.sh ist installed +/renewCerts.sh --setup >> /autoACME.log \ + && echo >> /autoACME.log + +echo "Register following entry to crontab:" >> /autoACME.log +echo "------------------------------------" >> /autoACME.log +_CRON_ENTRY="$((RANDOM % 59)) $((RANDOM % 5)) * * * /renewCerts.sh --dns --own >> /autoACME.log 2>&1" +echo "${_CRON_ENTRY}" | tee -a /autoACME.log | crontab - + +cron && tail -n 100 -f /autoACME.log