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:
- Determines the Apache configuration directory
- Determines the directory to store the generated self-signed wildcard certificate
- Determines the directory to store the generated private key
- Create self-signed wild-card certificates for websites ending in '.test', if necessary
- 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 "$@"