From ab5fb14232039968116456b77697b12d677d4e37 Mon Sep 17 00:00:00 2001 From: Tommy <contact@tommytran.io> Date: Sat, 29 Jun 2024 17:53:27 -0700 Subject: [PATCH] SELinux support for certbot-ocsp-fetcher Signed-off-by: Tommy <contact@tommytran.io> --- setup.sh | 4 +- usr/local/bin/certbot-ocsp-fetcher | 684 +++++++++++++++++++++++++++++ 2 files changed, 686 insertions(+), 2 deletions(-) create mode 100644 usr/local/bin/certbot-ocsp-fetcher diff --git a/setup.sh b/setup.sh index d33b1ff..422de39 100644 --- a/setup.sh +++ b/setup.sh @@ -53,7 +53,7 @@ chmod 644 /etc/systemd/system/nginx.service.d/override.conf sudo systemctl daemon-reload # Setup certbot-ocsp-fetcher -unpriv curl https://raw.githubusercontent.com/GrapheneOS/infrastructure/main/certbot-ocsp-fetcher | sudo tee /usr/local/bin/certbot-ocsp-fetcher +unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/usr/local/bin/certbot-ocsp-fetcher | sudo tee /usr/local/bin/certbot-ocsp-fetcher ## Explicitly using /var/usrlocal/bin here because SELinux does not follow symlinks sudo semanage fcontext -a -t bin_t /var/usrlocal/bin/certbot-ocsp-fetcher sudo restorecon -Rv /var/usrlocal/bin/certbot-ocsp-fetcher @@ -108,4 +108,4 @@ unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/et unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/etc/nginx/snippets/quic.conf | sudo tee /etc/nginx/snippets/quic.conf unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/etc/nginx/snippets/security.conf | sudo tee /etc/nginx/snippets/security.conf unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/etc/nginx/snippets/cross-origin-security.conf | sudo tee /etc/nginx/snippets/cross-origin-security.conf -unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/etc/nginx/snippets/universal_paths.conf | sudo tee /etc/nginx/snippets/universal_paths.conf \ No newline at end of file +unpriv curl https://raw.githubusercontent.com/TommyTran732/NGINX-Configs/main/etc/nginx/snippets/universal_paths.conf | sudo tee /etc/nginx/snippets/universal_paths.conf diff --git a/usr/local/bin/certbot-ocsp-fetcher b/usr/local/bin/certbot-ocsp-fetcher new file mode 100644 index 0000000..4f7f2f1 --- /dev/null +++ b/usr/local/bin/certbot-ocsp-fetcher @@ -0,0 +1,684 @@ +#!/usr/bin/env bash + +# This file is the same as https://github.com/tomwassenberg/certbot-ocsp-fetcher +# but with extra logic to restore SELinux context + +# Unofficial Bash strict mode +set \ + -o errexit \ + -o errtrace \ + -o noglob \ + -o nounset \ + -o pipefail +IFS=$'\n\t' +shopt -s inherit_errexit + +determine_colored_output() { + declare -gl COLORED_STDOUT COLORED_STDERR + readonly GREEN='\033[0;32m' + readonly RED='\033[0;31m' + readonly COLOR_DEFAULT='\033[0m' + + if [[ -v NO_COLOR || ${TERM-} == dumb ]]; then + COLORED_STDOUT=false COLORED_STDERR=false + else + [[ -t 1 ]] || COLORED_STDOUT=false + [[ -t 2 ]] || COLORED_STDERR=false + fi + +} + +exit_with_error() { + local error_prefix=error:$'\t\t' + + [[ ${COLORED_STDERR-} != false ]] && + local -r COLORED_ERROR_MSG=${RED}${error_prefix}${*}${COLOR_DEFAULT} + + # We will have closed file descriptor 2 unless verbosity was requested, so we + # will try to use FD5 (the FD that stderr was likely redirected to), and + # fallback to FD2 if FD5 wasn't opened yet. + if [[ -f /dev/fd/5 ]]; then + exec >&5 + else + exec >&2 + fi + printf '%b\n' "${COLORED_ERROR_MSG:-${error_prefix}${@}}" + + exit 1 +} + +check_for_dependencies() { + if ((BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3 || BASH_VERSINFO[0] < 4)); then + exit_with_error "${0##*/} requires Bash 4.3+." + fi + + if ! { command -v openssl >&- && + [[ $(openssl version || true) =~ ^OpenSSL\ ([[:digit:]]+)\.([[:digit:]]+) ]] && + ((BASH_REMATCH[1] == 1 && BASH_REMATCH[2] >= 1 || BASH_REMATCH[1] > 1)); }; then + # shellcheck disable=2016 + exit_with_error \ + "${0##*/} requires OpenSSL 1.1.0+," \ + 'but it is not available on $PATH.' + fi +} + +parse_cli_options() { + local -r cli_options=" +Usage: ${0} [-c/--certbot-dir DIRECTORY] [-f/--force-update] \\ + [-h/--help] [-l/--no-color] [-n/--cert-name NAME[,NAME...] \\ + [-u/--ocsp-responder URL]] [-o/--output-dir DIRECTORY] \\ + [-q/--quiet|-v/--verbose] [-w/--no-reload-webserver] +" + + print_option_error() { + local reason=${1} option=${2} + shift 2 + local option_error="${option}: " + + case ${reason} in + --conflict) + local second_option=${1} + shift + option_error+="This option cannot be combined with the option ${second_option}." + ;; + --duplicate) + option_error+="This option cannot be specified multiple times." + ;; + --unknown) + option_error+="Invalid option." + ;; + --value) + option_error+="This option requires a value." + ;; + *) + exit 1 + ;; + esac + + exit_with_error "${option_error}" "${cli_options}" + } + + declare -gl ERROR_ENCOUNTERED + + declare -gi VERBOSITY=${VERBOSITY:-1} + + while ((${#} > 0)); do + local parameter=${1} + + case ${parameter} in + -[^-]?*) + set -- "-${parameter:1:1}" "-${parameter:2}" "${@:2}" + ;; + -c | --certbot-dir | --certbot-dir=?*) + if [[ -v CERTBOT_DIR ]]; then + print_option_error --duplicate "${parameter}" + fi + + if [[ ${parameter} =~ --certbot-dir=(.+) ]]; then + CERTBOT_DIR=${BASH_REMATCH[1]} + else + if [[ -n ${2-} ]]; then + CERTBOT_DIR=${2} + shift + else + print_option_error --value "${parameter}" + fi + fi + + CERTBOT_DIR=$( + realpath \ + --canonicalize-missing \ + --relative-base . \ + -- "${CERTBOT_DIR}" + echo x + ) + CERTBOT_DIR=${CERTBOT_DIR%??} + shift + ;; + -f | --force-update) + if [[ ! -v FORCE_UPDATE ]]; then + declare -glr FORCE_UPDATE=true + fi + shift + ;; + -h | --help) + { + printf '%s\n' certbot-ocsp-fetcher + printf '%s\n' "${cli_options}" + local absolute_tool_path + absolute_tool_path=$(realpath --no-symlinks -- "${0}") + readonly absolute_tool_path + cat <<EOSTRING + +certbot-ocsp-fetcher helps you setup OCSP stapling in nginx. The tool primes +nginx's OCSP cache to work around nginx's flawed OCSP stapling implementation. +The tool does this by fetching and saving OCSP responses for TLS certificates +issued with Certbot. + +--- + +Example: + +1. Fetch OCSP responses for all certificates managed by Certbot, and save +them in the current working directory. This should usually be run on a +schedule, e.g. as a cronjob or systemd timer. + +$ ${0} + +2. Add the path(s) to the resulting OCSP response(s) as the value of the +ssl_stapling_file directive in the corresponding vhosts in Nginx. Don't +forget to reload Nginx afterwards. + +3. Re-issue all certificates managed by Certbot, to add the OCSP Must-Staple +flag to the certs and automatically run certbot-ocsp-fetcher during renewals: + +$ certbot renew --deploy-hook ${absolute_tool_path} --force-renewal --must-staple + +--- + +See the online README for an explanation of all the CLI options: +https://github.com/tomwassenberg/certbot-ocsp-fetcher/blob/main/README.md +EOSTRING + } + exit + ;; + -l | --no-color) + readonly COLORED_STDOUT=false COLORED_STDERR=false + ;; + -n | --cert-name | --cert-name=?*) + if [[ ${parameter} =~ --cert-name=(.+) ]]; then + local cert_lineages_value=${BASH_REMATCH[1]} + shift + else + if [[ -n ${2-} ]]; then + local cert_lineages_value=${2} + shift 2 + else + print_option_error --value "${parameter}" + fi + fi + + # Loop over any lineages passed in the same value of --cert-name. + OLDIFS=${IFS} + IFS=, + declare -Ag CERT_LINEAGES + # Check if a hardcoded OCSP responder was specified for this set of + # lineages. + case ${1-} in + -u | --ocsp-responder) + if [[ -n ${2-} ]]; then + for lineage_name in ${cert_lineages_value}; do + CERT_LINEAGES["${lineage_name}"]=${2} + done + shift + else + print_option_error --value "${parameter}" + fi + shift + ;; + --ocsp-responder=?*) + [[ ${1} =~ --ocsp-responder=(.+) ]] + for lineage_name in ${cert_lineages_value}; do + CERT_LINEAGES["${lineage_name}"]=${BASH_REMATCH[1]} + done + shift + ;; + *) + # If no OCSP responder was specified, just save the lineage + # name as the key, with an empty value. + for lineage_name in ${cert_lineages_value}; do + CERT_LINEAGES["${lineage_name}"]= + done + ;; + esac + unset lineage_name cert_lineages_value + IFS=${OLDIFS} + ;; + -o | --output-dir | --output-dir=?*) + if [[ -v OUTPUT_DIR ]]; then + print_option_error --duplicate "${parameter}" + fi + + if [[ ${parameter} =~ --output-dir=(.+) ]]; then + OUTPUT_DIR=${BASH_REMATCH[1]} + else + if [[ -n ${2-} ]]; then + OUTPUT_DIR=${2} + shift + else + print_option_error --value "${parameter}" + fi + fi + + OUTPUT_DIR=$( + realpath \ + --canonicalize-missing \ + --relative-base . \ + -- "${OUTPUT_DIR}" + echo x + ) + OUTPUT_DIR=${OUTPUT_DIR%??} + shift + ;; + -q | --quiet) + if ((VERBOSITY != 1)); then + print_option_error --conflict "${parameter}" -v/--verbose + else + readonly VERBOSITY=0 + shift + fi + ;; + -v | --verbose) + if ((VERBOSITY == 0)); then + print_option_error --conflict "${parameter}" -q/--quiet + else + VERBOSITY+=1 + shift + fi + ;; + -w | --no-reload-webserver) + if [[ ! -v RELOAD_WEBSERVER ]]; then + declare -glr RELOAD_WEBSERVER=false + fi + shift + ;; + *) + print_option_error --unknown "${parameter}" + ;; + esac + done + + # Respect the common "DEBUG" environment variable if set, unless the --quiet + # or --verbose flag has been passed as well. + if ((${DEBUG:-0} >= 1)) && ((VERBOSITY == 1)); then + # We set VERBOSITY to 0 in case of --quiet, so use the value of $DEBUG + # incremented with 1 to match it with $VERBOSITY. + VERBOSITY=$((DEBUG + 1)) + fi + + # When not parsed, the stdout and/or stderr output of all external commands + # we call in the script is redirected to file descriptor 3. Depending on the + # desired verbosity, we redirect this file descriptor to either stderr or to + # /dev/null. + if ((VERBOSITY >= 2)); then + exec 3>&2 + else + exec 3>/dev/null + fi + + # First copy file descriptor 2 to a new FD, so stderr can still be used + # (unconditionally) in the exit_with_error function. + exec 5>&2 + if ((VERBOSITY < 1)); then + exec 2>/dev/null + fi +} + +# Set output directory if necessary and check if it's writeable +prepare_output_dir() { + if [[ -v OUTPUT_DIR ]]; then + if [[ ! -e ${OUTPUT_DIR} ]]; then + # Don't yet fail if it's not possible to create the directory, so we can + # exit with a custom error down below + mkdir \ + --parents \ + -- "${OUTPUT_DIR}" || true + fi + else + # Use $CACHE_DIRECTORY if set (e.g. when run as a systemd service), + # otherwise the working directory + readonly OUTPUT_DIR=${CACHE_DIRECTORY:-.} + fi + + if [[ ! -w ${OUTPUT_DIR} ]]; then + exit_with_error "no write access to output directory (\"${OUTPUT_DIR}\")" + fi +} + +start_in_correct_mode() { + # Create temporary directory to store OCSP staple file, + # before having checked the certificate status in the response + local temp_output_dir + temp_output_dir=$(mktemp --directory) + readonly temp_output_dir + trap "rm -r -- ""${temp_output_dir}" EXIT + + declare -A lineages_processed + + # These two environment variables are set if this script is invoked by Certbot + if [[ ! -v RENEWED_DOMAINS || ! -v RENEWED_LINEAGE ]]; then + run_standalone + else + run_as_deploy_hook + fi + + print_and_handle_result +} + +# Run in "check one or all certificate lineage(s) managed by Certbot" mode +# $1 - Path to temporary output directory +run_standalone() { + printf >&2 '%s\n\n' "Running in stand-alone mode..." + + readonly CERTBOT_DIR=${CERTBOT_DIR:-/etc/letsencrypt} + + if [[ ! -r ${CERTBOT_DIR} || (-d ${CERTBOT_DIR}/live && ! -r ${CERTBOT_DIR}/live) ]]; then + exit_with_error "can't access ${CERTBOT_DIR}/live" + fi + + # Check specific lineage if passed on CLI, + # or otherwise all lineages in Certbot's dir + if [[ -n ${!CERT_LINEAGES[*]} ]]; then + for lineage_name in "${!CERT_LINEAGES[@]}"; do + if [[ -r ${CERTBOT_DIR}/live/${lineage_name} ]]; then + fetch_ocsp_response \ + --standalone \ + "${temp_output_dir}" \ + "${lineage_name}" \ + "${CERT_LINEAGES["${lineage_name}"]}" + else + exit_with_error "can't access ${CERTBOT_DIR}/live/${lineage_name}" + fi + done + else + set +f + shopt -s nullglob + for lineage_dir in "${CERTBOT_DIR}"/live/*; do + set -f + + # Skip non-directories, like Certbot's README file + [[ -d ${lineage_dir} ]] || continue + + fetch_ocsp_response \ + --standalone "${temp_output_dir}" "${lineage_dir##*/}" + done + unset lineage_dir + fi +} + +# Run in deploy-hook mode, only processing the passed lineage +# $1 - Path to temporary output directory +run_as_deploy_hook() { + printf >&2 '%s\n\n' "Running as a deploy hook of Certbot..." + + if [[ -v CERTBOT_DIR ]]; then + # The directory is already inferred from the environment variable that + # Certbot passes + exit_with_error \ + "-c/--certbot-dir cannot be passed" \ + "when run as Certbot hook" + fi + + if [[ -v FORCE_UPDATE ]]; then + # When run as deploy hook the behavior of this flag is used by default. + # Therefore passing this flag would not have any effect. + exit_with_error \ + "-f/--force-update cannot be passed" \ + "when run as Certbot hook" + fi + + if [[ -n ${!CERT_LINEAGES[*]} ]]; then + # The certificate lineage is already inferred from the environment + # variable that Certbot passes + exit_with_error "-n/--cert-name cannot be passed when run as Certbot hook" + fi + + fetch_ocsp_response \ + --deploy_hook "${temp_output_dir}" "${RENEWED_LINEAGE##*/}" +} + +# Check if it's necessary to fetch a new OCSP response +check_for_existing_ocsp_staple_file() { + [[ -f ${OUTPUT_DIR}/${lineage_name}.der ]] || return 1 + + # Validate and verify the existing local OCSP staple file + local existing_ocsp_response + set +e + existing_ocsp_response=$(openssl ocsp \ + -no_nonce \ + -issuer "${lineage_dir}/chain.pem" \ + -cert "${lineage_dir}/cert.pem" \ + -verify_other "${lineage_dir}/chain.pem" \ + -respin "${OUTPUT_DIR}/${lineage_name}.der" 2>&3) + local -ir existing_ocsp_response_rc=${?} + set -e + readonly existing_ocsp_response + + ((existing_ocsp_response_rc == 0)) || return 1 + + for existing_ocsp_response_line in ${existing_ocsp_response}; do + if [[ ${existing_ocsp_response_line} =~ ^[[:blank:]]*"This Update: "(.+)$ ]]; then + local -r this_update=${BASH_REMATCH[1]} + elif [[ ${existing_ocsp_response_line} =~ ^[[:blank:]]*"Next Update: "(.+)$ ]]; then + local -r next_update=${BASH_REMATCH[1]} + fi + done + [[ -n ${this_update-} && -n ${next_update-} ]] || return 1 + + # Only continue fetching OCSP response if existing response expires within + # half of its lifetime. + { + # The command substitutions here don't respect `set -o errexit`, but in + # case any of them fail, the total command still fails unless both + # substitutions print an integer. This seems very unlikely to occur, so + # let's ignore this. + # shellcheck disable=2312 + local -ri response_lifetime_in_seconds=$(($(date +%s --date "${next_update}") - $(date +%s --date "${this_update}"))) + + # `set -o errexit` isn't respected here either, but we default to renewing + # the OCSP response, so this is fine. + # shellcheck disable=2312 + (($(date +%s) < $(date +%s --date "${this_update}") + response_lifetime_in_seconds / 2)) || return 1 + } +} + +# Generate file used by ssl_stapling_file in nginx config of websites +# $1 - Whether to run as a deploy hook for Certbot, or standalone +# $2 - Path to temporary output directory +# $3 - Name of certificate lineage +# $4 - OCSP endpoint (if specified on command line) +fetch_ocsp_response() { + local -r temp_output_dir=${2} + local -r lineage_name=${3} + + # This validation should be revisited once + # https://github.com/certbot/certbot/issues/6127 is fixed. + if [[ ${lineage_name} =~ ($'\n')|($'\t') ]]; then + ERROR_ENCOUNTERED=true + exit_with_error \ + "Unsupported characters encountered in the following" \ + "lineage name: ${lineage_name}$'\n\n'" \ + "Lineage names with embedded tabs or newlines are not supported," \ + "because Certbot (as of version 1.18.0) does not have well-defined" \ + 'behavior on handling any "unconventional" lineage names.' + fi + + case ${1} in + --standalone) + local -r lineage_dir=${CERTBOT_DIR}/live/${lineage_name} + + # `set -o errexit` is not respected here, but in case of failure we still + # err on the safe side by renewing the OCSP staple file. + # shellcheck disable=2310 + if [[ ${FORCE_UPDATE-} != true ]] && + check_for_existing_ocsp_staple_file; then + lineages_processed["${lineage_name}"]="not updated"$'\t'"valid staple file on disk" + return + fi + ;; + --deploy_hook) + local -r lineage_dir=${RENEWED_LINEAGE} + ;; + *) + return 1 + ;; + esac + shift 3 + + # Verify that the leaf certificate is still valid. If the certificate is + # expired, we don't have to request a (new) OCSP response. + local cert_expiry_output + set +e + cert_expiry_output=$(openssl x509 \ + -in "${lineage_dir}/cert.pem" \ + -checkend 0 \ + -noout 2>&3) + local -ri cert_expiry_rc=${?} + set -e + if ((cert_expiry_rc != 0)); then + ERROR_ENCOUNTERED=true + lineages_processed["${lineage_name}"]="failed to update" + if [[ ${cert_expiry_output} == "Certificate will expire" ]]; then + lineages_processed["${lineage_name}"]+=$'\t'"leaf certificate expired" + fi + return + fi + + local ocsp_endpoint + if [[ -n ${1-} ]]; then + ocsp_endpoint=${1} + else + ocsp_endpoint=$(openssl x509 \ + -noout \ + -ocsp_uri \ + -in "${lineage_dir}/cert.pem" \ + 2>&3) + fi + + # Request, verify and temporarily save the actual OCSP response, + # and check whether the certificate status is "good" + local ocsp_call_output + set +e + ocsp_call_output=$(openssl ocsp \ + -no_nonce \ + -url "${ocsp_endpoint}" \ + -issuer "${lineage_dir}/chain.pem" \ + -cert "${lineage_dir}/cert.pem" \ + -verify_other "${lineage_dir}/chain.pem" \ + -respout "${temp_output_dir}/${lineage_name}.der" 2>&3) + local -ir ocsp_call_rc=${?} + set -e + readonly ocsp_call_output=${ocsp_call_output#"${lineage_dir}"/cert.pem: } + local -r cert_status=${ocsp_call_output%%$'\n'*} + + if [[ ${ocsp_call_rc} != 0 || ${cert_status} != good ]]; then + ERROR_ENCOUNTERED=true + + lineages_processed["${lineage_name}"]="failed to update" + if ((VERBOSITY >= 2)); then + lineages_processed["${lineage_name}"]+=$'\t'"${ocsp_call_output//[[:space:]]/ }" + else + lineages_processed["${lineage_name}"]+=$'\t'"${cert_status}" + fi + + return + fi + + # If arrived here status was good, so move OCSP staple file to definitive + # folder + mv "${temp_output_dir}/${lineage_name}.der" "${OUTPUT_DIR}/" + + # Restore SELinux context on SELinux systems + if [[ -f /usr/sbin/restorecon ]]; then + restorecon "${OUTPUT_DIR}/${lineage_name}.der" + fi + + lineages_processed["${lineage_name}"]=updated +} + +print_and_handle_result() { + local -r header=LINEAGE$'\t'RESULT$'\t'REASON + + local lineages_processed_marked_up + for lineage_name in "${!lineages_processed[@]}"; do + lineages_processed_marked_up+=$'\n'"${lineage_name}"$'\t' + if [[ ${COLORED_STDOUT-} != false ]]; then + if [[ ${lineages_processed["${lineage_name}"]} =~ ^updated ]]; then + lineages_processed_marked_up+=${GREEN} + elif [[ ${lineages_processed["${lineage_name}"]} =~ ^"failed to update" ]]; then + lineages_processed_marked_up+=${RED} + fi + lineages_processed_marked_up+=${lineages_processed["${lineage_name}"]}${COLOR_DEFAULT} + else + lineages_processed_marked_up+=${lineages_processed["${lineage_name}"]} + fi + done + unset lineage_name + lineages_processed_marked_up=$(sort <<<"${lineages_processed_marked_up-}") + readonly lineages_processed_marked_up + + if [[ ${RELOAD_WEBSERVER-} != false ]]; then + reload_webserver + fi + + local output=${header}${lineages_processed_marked_up-}${nginx_status-} + + if ((VERBOSITY >= 1)); then + local output_table + # shellcheck disable=2016 + output_table=$(column \ + --output-separator $'\t' \ + --separator $'\t' \ + --table \ + <<<"${output}" \ + 2>/dev/null) || + output_table=$(column -s$'\t' -t <<<"${output}" 2>/dev/null) || + local -r column_error=($'\n' + 'Install the BSD utility `column` for properly formatted output.' + 'If the version of `column` supports the `--output-separator` flag,' + 'the output will be formatted as TSV.' + $'\n' + ) + readonly output=${output_table:-${output}} + unset output_table + + # Extract header to direct it to stderr + printf '%s\n' "${output%%$'\n'*}" >&2 + # Remove header before printing everything else to stdout + [[ -n ${!lineages_processed[*]} ]] && printf '%b\n' "${output#*$'\n'}" + + if [[ ${COLORED_STDERR-} != false ]]; then + printf %b "${RED}${column_error[*]-}${COLOR_DEFAULT}" >&2 + else + printf %b "${column_error[*]-}" >&2 + fi + fi + + [[ ${ERROR_ENCOUNTERED-} != true ]] +} + +reload_webserver() { + for lineage_name in "${!lineages_processed[@]}"; do + if [[ ${lineages_processed["${lineage_name}"]} == updated ]]; then + local nginx_status + if nginx -s reload >&3 2>&1; then + [[ ${COLORED_STDERR-} != false ]] && nginx_status=${GREEN} + # The last line includes a leading space, to workaround the lack of the + # `-n` flag in later versions of `column`. + nginx_status+=$'\n\n \t'"nginx reloaded" + else + ERROR_ENCOUNTERED=true + [[ ${COLORED_STDERR-} != false ]] && nginx_status=${RED} + nginx_status=$'\n\n \t'"nginx not reloaded"$'\t'"unable to reload nginx service, try manually" + fi + [[ ${COLORED_STDERR-} != false ]] && + readonly nginx_status+=${COLOR_DEFAULT} + break + fi + done + unset lineage_name +} + +main() { + check_for_dependencies + + determine_colored_output + + parse_cli_options "${@}" + + prepare_output_dir + + start_in_correct_mode +} + +main "${@}"