Technical Documentation

Introduction

RunCertbot is a script that automatically calls the 'certbot' command-line program, which obtains Let's Encrypt SSL certificates for websites.

For each Apache site configuration file, if there is not a corresponding Let's Encrypt or other SSL configuration file, RunCertbot will either: call 'certbot' using the domains specified in the ServerName and/or ServerAlias directives, or, for domains ending in '.test', will created an SSL-enabled configuration file that uses a self-signed certificate. RunCertbot will automatically create the self-signed certificate and key if required.

Once an SSL configuration has been created, the existing non-ssl configuration file will be deleted. Therefore you should have a generic redirect Apache configuration file that will redirect port 80 requests to port 443 (i.e., http to https). For example, the following is based on:

https://cwiki.apache.org/confluence/display/HTTPD/RewriteHTTPToHTTPS
#
#   /etc/apache2/sites-enabled/80-REDIRECT_TO_HTTPS.conf
#
<VirtualHost *:80>
    ServerAdmin webmaster@example.com

    RewriteEngine On
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>

If any new SSL configuration files have been created, and if 'apachectl checkconfig' passes, the Apache server will be restarted.

Dependencies

In order for RunCertbot to run, 'certbot' must already be installed.

sudo bash
apt-get install certbot

Configuration

No configuration is necessary - any required information is passed on the command line. The specified 'Apache ssl cert dir' and 'Apache ssl key dir' are used to create, if necessary, the wildcard self-signed certificate for the '*.text' domain.

Please note, certificates obtained from Let's Encrypt are stored in the default 'certbot' location.

Usage:

    runcertbot.sh [--help] [<Apache config dir> [<Apache ssl cert dir> [<Apache ssl key dir>]]]

    --help      Print out usage information

    <Apache config dir>     Apache2 configuration directory -                      default: /etc/apache2
    <Apache ssl cert dir>   Directory to store generated self-signed certificate - default: /etc/apache2/ssl
    <Apache ssl key dir>    Directory to store generated key -                     default: /etc/apache2/ssl

RunCertbot is intended to be called periodically from 'cron'. The root account's crontab file can be edited by calling:

sudo bash
crontab -e

If installed under '/opt' and 'latest' is a symbolic link to the latest version, the following line will automatically call runcertbot each minute.

* * * * *   /opt/runcertbot/latest/runcertbot/sbin/runcertbot.sh >> /var/log/runcertbot.log

Implementation

RunCertbot is implemented using the Bash scripting language.

#!/usr/bin/env bash

Default files and directories

By default, RunCertbot assumes that the Apache configuration directory is located at '/etc/apache2' and that the self-signed certificates for any '.test' domains are located at '/etc/apache/ssl'. However, if desired, the directories used for the Apache configuration directory, the SSL certificates directory, and the SSL private keys directory may be specified on the command-line.

/etc/apache2                      Default Apache2 configuration directory
/etc/apache2/sites-enabled        Default directory for site configuration files
/etc/apache2/ssl                  Default directory for ssl certificates

Global variables

The RunCertbot script uses one global variable that is used to keep track of whether a restart of Apache is required. It should always be set to "FALSE" when defined; and should only ever be set to "TRUE" when a sub-routine determines that a restart of Apache is necesary.

RESTART_APACHE="FALSE"

Main

The 'main' function is the main entry point of the script - called from the last line of the script. The main function does the following:

  1. Determines the Apache configuration directory
  2. Determines the directory to store the generated self-signed wildcard certificate
  3. Determines the directory to store the generated private key
  4. Create self-signed wild-card certificates for websites ending in '.test', if necessary
  5. Processes all site configuration files and either call 'certbot' for registered domains, or uses the self-signed certificate for '.test' domains

If any errors occur, RunCertbot should exit with an error message and return -1.

function main()
{
    usage $1

    local date=`date`

    local apache_dir_cfg=`determine_apache_directory_config "$1"`
    local apache_dir_crt=`determine_apache_directory_cert   "$apache_dir_cfg" "$2"`
    local apache_dir_key=`determine_apache_directory_key    "$apache_dir_crt" "$3"`

    if [ ! -d "$apache_dir_cfg" -o ! -d "$apache_dir_crt" -o ! -d "$apache_dir_key" ]
    then
        echo "$date - ERROR"
        echo "Could not resolve all directories"
        echo "apache_dir_cfg: $apache_dir_cfg"
        echo "apache_dir_crt: $apache_dir_crt"
        echo "apache_dir_key: $apache_dir_key"
        exit -1

    else

        echo -e "\n$date - Running certbot for Apache config dir: $apache_dir_cfg"

        if ! create_test_wildcard_certs_if_necessary "$apache_dir_cfg" "$apache_dir_crt" "$apache_dir_key"
        then
            echo "$date - ERROR"
            echo "Could not create self-signed wildcard certificate"
            exit -1

        elif process_apache_site_configurations    "$apache_dir_cfg" "$apache_dir_crt" "$apache_dir_key"
        then

            if apache_config_test
            then
                if [ "TRUE" = "$RESTART_APACHE" ]
                then
                    echo "Restarting Apache"
                    service apache2 restart
                fi
            fi

        else
            return -1

        fi
    fi
}

Usage

function usage()
{
    if [ "--help" = "$1" -o "root" != `whoami` ]
    then
        echo "Usage (as root):"
        echo "run_certbot.sh [<Apache config dir> [<Apache ssl cert dir> [<Apache ssl key dir>]]]"

        if [ "root" != `whoami` ]
        then
            echo "MUST be run as root account"

        fi

        exit -1
    fi
}
Configuration
function determine_apache_directory_config()
{
    local apache_dir_cfg=$1

    if [ -z "$apache_dir_cfg" ]
    then
        apache_dir_cfg="/etc/apache2"

    fi

    echo "$apache_dir_cfg"
}
function determine_apache_directory_cert()
{
    local apache_dir_cfg=$1
    local apache_dir_crt=$2

    if [ -z "$apache_dir_crt" ]
    then
        apache_dir_crt="$apache_dir_cfg/ssl"

    fi

    echo "$apache_dir_crt"
}
function determine_apache_directory_key()
{
    local apache_dir_crt=$1
    local apache_dir_key=$2

    if [ -z "$apache_dir_key" ]
    then
        apache_dir_key="$apache_dir_crt"

    fi

    echo "$apache_dir_key"
}

https:justin.kelly.org.au/how-to-create-self-signed-wildcard-ssl-certificates-for-apache/

function create_test_wildcard_certs_if_necessary()
{
    local apache_dir_cfg=$1
    local apache_dir_crt=$2
    local apache_dir_key=$3

    if [ -f "$apache_dir_key/wildcard.test.key" -o -f "$apache_dir_crt/wildcard.test.pem" ]
    then
        return 0

    else
        mkdir -p $apache_dir_key &&
        mkdir -p $apache_dir_crt &&

        openssl genrsa 2048 > "$apache_dir_key/wildcard.test.key" &&

        openssl req -new -x509 -nodes -sha256 -days 365 -batch -subj '/CN=*.test' \
            -key "$apache_dir_key/wildcard.test.key" > "$apache_dir_crt/wildcard.test.cert" &&

        openssl x509 -noout -fingerprint -text < "$apache_dir_crt/wildcard.test.cert" > "$apache_dir_crt/host.info" &&

        cat "$apache_dir_crt/wildcard.test.cert" "$apache_dir_key/wildcard.test.key" > "$apache_dir_key/wildcard.test.pem" &&

        chmod 400 "$apache_dir_key/wildcard.test.key" "$apache_dir_key/wildcard.test.pem"

        return $?
    fi
}
function process_apache_site_configurations()
{
    local apache_dir_cfg=$1
    local apache_dir_crt=$2
    local apache_dir_key=$3
 
    if [ ! -d "$apache_dir_cfg/sites-enabled" ]
    then
        echo "Could not resolve Apache sites directory: $apache_dir_cfg/sites-enabled"
        exit -1

    else
        for config_file in $( ls "$apache_dir_cfg/sites-enabled" ); do

            if [ -f "$apache_dir_cfg/sites-enabled/$config_file" ]
            then
                process_config_file "$apache_dir_cfg/sites-enabled/$config_file" "$apache_dir_crt" "$apache_dir_key"

                if [ 0 != $? ]
                then
                    echo "+!  Aborting: error processing Apache config file: $apache_dir_cfg/sites-enabled/$config_file"
                    return -1
                fi

            fi

        done
    fi
}
function process_config_file()
{
    local config_file=$1
    local apache_dir_crt=$2
    local apache_dir_key=$3
    local config_filename=`basename $config_file`

    if ! string_starts_with "$config_filename" "_"
    then
        if   string_contains  "$config_filename" "ssl"
        then
            : #echo "+   Skipping: $config_file"

        elif string_contains  "$config_filename" "SSL"
        then
            : #echo "+   Skipping: $config_file"

        elif string_ends_with  "$config_filename" ".conf"
        then
            echo "+   Considering: $config_file"

            local server_name=`extract_server_name "$config_file"`

            if [ -n "$server_name" ]
            then
                local owner=`ls -l "$config_file" | cut -d ' ' -f3`
                local group=`ls -l "$config_file" | cut -d ' ' -f4`

                local dir=`dirname "$config_file"`
                local prefix=`basename -s .conf "$config_file"`

                if string_ends_with "$server_name" ".test"
                then
                    process_config_file_using_self_signed "$config_file" "$apache_dir_crt" "$apache_dir_key"

                else
                    process_config_file_using_certbot "$config_file"

                fi

                chown $owner:$group $dir/$prefix*.conf
            fi

        else

            : #echo "+   Skipping: $config_file"

        fi
    fi
}
function process_config_file_using_self_signed()
{
    local config_file=$1
    local apache_dir_crt=$2
    local apache_dir_key=$3
    local target=${config_file/.conf/.ssl.conf}

    echo "++  Processing using self-signed certificate: $server_name"

    if [ -f "$target" ]
    then
        echo "++  Ignoring, file exists"

    else
        local ssl=""
        
        ssl+="    SSLEngine on\n"
        ssl+="    SSLCertificateFile    ${apache_dir_crt}/wildcard.test.pem\n"
        ssl+="    SSLCertificateKeyFile ${apache_dir_key}/wildcard.test.key\n"
        ssl+="\n"
        ssl+="</VirtualHost>"

        local content=`cat "$config_file"`
        local content="${content/:8080>/:8443>}"
        local content="${content/:80>/:443>}"
        local content="${content/<\/VirtualHost>/$ssl}"
        local content="<IfModule mod_ssl.c>\n$content\n</IfModule>"

        if echo -e "$content" > "$target"
        then
            echo "+++ Written to $target"

            if [ -f "$target" ]
            then
                if apache_config_test
                then
                    if rm "$config_file"
                    then
                        RESTART_APACHE="TRUE"
                    fi
                fi
            fi

        else
            echo "++! Warning, error writing to target: $target"

        fi
    fi
}
function process_config_file_using_certbot()
{
    local config_file=$1
    local domains=`extract_domains "$config_file"`
    local email=`extract_server_admin "$config_file"`

    local prov_lets_encrypt_config_file=${config_file/.conf/-le-ssl.conf}
    local flags=""
    local valid_ip_addresses="TRUE"
    local my_address=`dig +short myip.opendns.com @resolver1.opendns.com`
    local server_name=`extract_server_name "$config_file"`

    echo "++  For $server_name, looking for $prov_lets_encrypt_config_file"

    if [ ! -f "$prov_lets_encrypt_config_file" ]
    then

        for domain in $domains
        do
            if [ -n "$domain" ]
            then
                echo "+++ Checking domain for my IP: $domain"

                if ! domain_has_my_ip "$domain"
                then
                    echo "+++ Waiting for DNS update to $my_address for: ${domain}"
                    valid_ip_addresses="FALSE"

                fi
            fi
        done

        if [ "TRUE" = "$valid_ip_addresses" ]
        then

            for domain in $domains
            do
                flags+="-d $domain "

            done

            if [ -n "$email" ]
            then
                flags+="-m $email"
            fi

            if [ -n "$flags" ]
            then

                echo "+++ Running certbot for: $domain"
                echo sudo certbot -n --agree-tos --apache --redirect --hsts --uir --expand $flags
                     sudo certbot -n --agree-tos --apache --redirect --hsts --uir --expand $flags

            fi
        fi

    fi

    if [ -f "$prov_lets_encrypt_config_file" ]
    then
        if apache_config_test
        then
            if rm "$config_file"
            then
                RESTART_APACHE="TRUE"
            fi
        fi
    fi
}
function extract_domains()
{
    local apache_config=$1
    local    name=`extract_server_name "$apache_config"`
    local aliases=`extract_aliases     "$apache_config"`

    echo "$name $aliases"
}
function extract_server_name()
{
    local apache_config=$1
    local names=`grep "ServerName"  "$apache_config" | grep -v "#" | sed 's|ServerName||g'  | sed 's|\t||g' | sed 's| ||g'`

    echo "$names"
}
function extract_aliases()
{
    local apache_config=$1
    local aliases=`grep "ServerAlias" "$apache_config" | grep -v "#" | sed 's|ServerAlias||g' | sed 's|\t||g' | sed 's| ||g'`

    echo "$aliases"
}
function extract_server_admin()
{
    local apache_config=$1
    local admin=`grep "ServerAdmin" "$apache_config" | grep -v "#" | sed 's|ServerAdmin||g' | sed 's|\t||g' | sed 's| ||g'`

    echo "$admin"
}
function domain_has_my_ip()
{
    local domain=$1
    local ip_address=`dig +noall +answer +short "$domain"`
    local my_address=`dig +short myip.opendns.com @resolver1.opendns.com`

    if [ -z "$ip_address" ]
    then
        echo "+++ Invalid IP address for $domain (should be $my_address)"
        return -1

    elif [ -z "$my_address" ]
    then
        echo "+++ Invalid IP address for localhost"
        return -1

    elif string_contains "$ip_address" "$my_address"
    then
        return 0

    else
        echo "+++ Invalid IP address for $domain ($ip_address) - (should be $my_address)"
        return -1

    fi
}
function string_contains()
{
    local haystack=$1
    local needle=$2

    if [ -z "${haystack##*$needle*}" ]
    then
        return 0  # success
    else
        return -1 # failure
    fi
}
function string_starts_with()
{
    local haystack=$1
    local prefix=$2

    if [ "${haystack}" = "$prefix${haystack/$prefix}" ]
    then
        return 0  # success
    else
        return -1 # failure
    fi
}
function string_ends_with()
{
    local haystack=$1
    local suffix=$2

    if [ "${haystack}" = "${haystack/$suffix}${suffix}" ]
    then
        return 0  # success
    else
        return -1 # failure
    fi
}
function apache_config_test()
{
    /usr/sbin/apachectl configtest > /dev/null 2>&1
}
main "$@"