Skip to content

Commit

Permalink
Merge pull request #423 from buchdag/default-cert-key
Browse files Browse the repository at this point in the history
Automatic creation of default cert and private key
  • Loading branch information
buchdag authored Aug 12, 2018
2 parents 1a294ac + a0afb09 commit eaec604
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Please note that [letsencrypt-nginx-proxy-companion does not work with ACME v2 e
### Features:
* Automatic creation/renewal of Let's Encrypt certificates using original nginx-proxy container.
* Support creation of Multi-Domain ([SAN](https://www.digicert.com/subject-alternative-name.htm)) Certificates.
* Automatically creation of a Strong Diffie-Hellman Group (for having an A+ Rate on the [Qualsys SSL Server Test](https://www.ssllabs.com/ssltest/)).
* Automatic creation of a Strong Diffie-Hellman Group (for having an A+ Rate on the [Qualsys SSL Server Test](https://www.ssllabs.com/ssltest/)).
* Automatic creation of a self-signed [default certificate](https://github.com/jwilder/nginx-proxy#how-ssl-support-works) if a user-provided one can't be found.
* Work with all versions of docker.

![schema](./schema.png)
Expand Down
35 changes: 35 additions & 0 deletions app/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ is being created."
) &disown
}

function check_default_cert_key {
local cn='letsencrypt-nginx-proxy-companion'

if [[ -e /etc/nginx/certs/default.crt && -e /etc/nginx/certs/default.key ]]; then
default_cert_cn="$(openssl x509 -noout -subject -in /etc/nginx/certs/default.crt)"
# Check if the existing default certificate is still valid for more
# than 3 months / 7776000 seconds (60 x 60 x 24 x 30 x 3).
check_cert_min_validity /etc/nginx/certs/default.crt 7776000
cert_validity=$?
[[ $DEBUG == true ]] && echo "Debug: a default certificate with $default_cert_cn is present."
fi

# Create a default cert and private key if:
# - either default.crt or default.key are absent
# OR
# - the existing default cert/key were generated by the container
# and the cert validity is less than three months
if [[ ! -e /etc/nginx/certs/default.crt || ! -e /etc/nginx/certs/default.key ]] || [[ "${default_cert_cn:-}" =~ $cn && "${cert_validity:-}" -ne 0 ]]; then
openssl req -x509 \
-newkey rsa:4096 -sha256 -nodes -days 365 \
-subj "/CN=$cn" \
-keyout /etc/nginx/certs/default.key.new \
-out /etc/nginx/certs/default.crt.new \
&& mv /etc/nginx/certs/default.key.new /etc/nginx/certs/default.key \
&& mv /etc/nginx/certs/default.crt.new /etc/nginx/certs/default.crt \
&& reload_nginx
echo "Info: a default key and certificate have been created at /etc/nginx/certs/default.key and /etc/nginx/certs/default.crt."
elif [[ $DEBUG == true && "${default_cert_cn:-}" =~ $cn ]]; then
echo "Debug: the self generated default certificate is still valid for more than three months. Skipping default certificate creation."
elif [[ $DEBUG == true ]]; then
echo "Debug: the default certificate is user provided. Skipping default certificate creation."
fi
}

source /app/functions.sh

if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
Expand Down Expand Up @@ -125,6 +159,7 @@ if [[ "$*" == "/bin/bash /app/start.sh" ]]; then
check_writable_directory '/etc/nginx/vhost.d'
check_writable_directory '/usr/share/nginx/html'
check_deprecated_env_var
check_default_cert_key
check_dh_group
fi

Expand Down
13 changes: 13 additions & 0 deletions app/functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ function remove_all_location_configurations {
eval "$old_shopt_options" # Restore shopt options
}

function check_cert_min_validity {
# Check if a certificate ($1) is still valid for a given amount of time in seconds ($2).
# Returns 0 if the certificate is still valid for this amount of time, 1 otherwise.
local cert_path="$1"
local min_validity="$(( $(date "+%s") + $2 ))"

local cert_expiration
cert_expiration="$(openssl x509 -noout -enddate -in "$cert_path" | cut -d "=" -f 2)"
cert_expiration="$(date --utc --date "${cert_expiration% GMT}" "+%s")"

[[ $cert_expiration -gt $min_validity ]] || return 1
}

function get_self_cid {
DOCKER_PROVIDER=${DOCKER_PROVIDER:-docker}

Expand Down
1 change: 1 addition & 0 deletions test/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ testAlias+=(
imageTests+=(
[le-companion]='
docker_api
default_cert
certs_single
certs_san
force_renew
Expand Down
2 changes: 1 addition & 1 deletion test/tests/certs_san/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ for hosts in "${letsencrypt_hosts[@]}"; do
fi

# Wait for a connection to https://domain then grab the served certificate in text form.
wait_for_conn "$domain"
wait_for_conn --domain "$domain"
served_cert_fingerprint="$(echo \
| openssl s_client -showcerts -servername $domain -connect $domain:443 2>/dev/null \
| openssl x509 -fingerprint -noout)"
Expand Down
2 changes: 1 addition & 1 deletion test/tests/certs_single/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ for domain in "${domains[@]}"; do
fi

# Wait for a connection to https://domain then grab the served certificate fingerprint.
wait_for_conn "$domain"
wait_for_conn --domain "$domain"
served_cert_fingerprint="$(echo \
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
| openssl x509 -fingerprint -noout)"
Expand Down
7 changes: 7 additions & 0 deletions test/tests/default_cert/expected-std-out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Started letsencrypt container for test default_cert
Connection to le1.wtf using https was successful.
Connection to le2.wtf using https was successful.
Connection to le3.wtf using https was successful.
Connection to le1.wtf using https was successful.
Connection to le2.wtf using https was successful.
Connection to le3.wtf using https was successful.
96 changes: 96 additions & 0 deletions test/tests/default_cert/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash

## Test for single domain certificates.

if [[ -z $TRAVIS_CI ]]; then
le_container_name="$(basename ${0%/*})_$(date "+%Y-%m-%d_%H.%M.%S")"
else
le_container_name="$(basename ${0%/*})"
fi
run_le_container ${1:?} "$le_container_name"

# Create the $domains array from comma separated domains in TEST_DOMAINS.
IFS=',' read -r -a domains <<< "$TEST_DOMAINS"

# Cleanup function with EXIT trap
function cleanup {
# Cleanup the files created by this run of the test to avoid foiling following test(s).
docker exec "$le_container_name" sh -c 'rm -rf /etc/nginx/certs/default.*'
docker stop "$le_container_name" > /dev/null
}
trap cleanup EXIT

function default_cert_fingerprint {
docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/default.crt" -fingerprint -noout
}

function default_cert_subject {
docker exec "$le_container_name" openssl x509 -in "/etc/nginx/certs/default.crt" -subject -noout
}

user_cn="user-provided"

i=0
until docker exec "$le_container_name" [[ -f /etc/nginx/certs/default.crt ]]; do
if [ $i -gt 60 ]; then
echo "Default cert wasn't created under one minute at container first launch."
fi
i=$((i + 2))
sleep 2
done

# Connection test to unconfigured domains
for domain in "${domains[@]}"; do
wait_for_conn --domain "$domain" --default-cert
done

# Test if the default certificate get re-created when
# the certificate or private key file are deleted
for file in 'default.key' 'default.crt'; do
old_default_cert_fingerprint="$(default_cert_fingerprint)"
docker exec "$le_container_name" rm -f /etc/nginx/certs/$file
docker restart "$le_container_name" > /dev/null && sleep 5
i=0
while [[ "$(default_cert_fingerprint)" == "$old_default_cert_fingerprint" ]]; do
if [ $i -gt 55 ]; then
echo "Default cert wasn't re-created under one minute after $file deletion."
break
fi
i=$((i + 2))
sleep 2
done
done

# Test if the default certificate get re-created when
# the certificate expire in less than three months
docker exec "$le_container_name" sh -c 'rm -rf /etc/nginx/certs/default.*'
docker exec "$le_container_name" openssl req -x509 \
-newkey rsa:4096 -sha256 -nodes -days 60 \
-subj "/CN=letsencrypt-nginx-proxy-companion" \
-keyout /etc/nginx/certs/default.key \
-out /etc/nginx/certs/default.crt > /dev/null 2>&1
old_default_cert_fingerprint="$(default_cert_fingerprint)"
docker restart "$le_container_name" > /dev/null && sleep 5
i=0
while [[ "$(default_cert_fingerprint)" == "$old_default_cert_fingerprint" ]]; do
if [ $i -gt 55 ]; then
echo "Default cert wasn't re-created under one minute when the certificate expire in less than three months."
break
fi
i=$((i + 2))
sleep 2
done

# Test that a user provided default certificate isn't overwrited
docker exec "$le_container_name" sh -c 'rm -rf /etc/nginx/certs/default.*'
docker exec "$le_container_name" openssl req -x509 \
-newkey rsa:4096 -sha256 -nodes -days 60 \
-subj "/CN=$user_cn" \
-keyout /etc/nginx/certs/default.key \
-out /etc/nginx/certs/default.crt > /dev/null 2>&1
docker restart "$le_container_name" > /dev/null

# Connection test to unconfigured domains
for domain in "${domains[@]}"; do
wait_for_conn --domain "$domain" --subject-match "$user_cn"
done
103 changes: 100 additions & 3 deletions test/tests/test-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function get_base_domain {
}
export -f get_base_domain


# Run a letsencrypt-nginx-proxy-companion container
function run_le_container {
local image="${1:?}"
Expand All @@ -30,6 +31,7 @@ function run_le_container {
}
export -f run_le_container


# Wait for the /etc/nginx/certs/$1.crt symlink to exist inside container $2
function wait_for_symlink {
local domain="${1:?}"
Expand All @@ -50,6 +52,7 @@ function wait_for_symlink {
}
export -f wait_for_symlink


# Wait for the /etc/nginx/certs/$1.crt file to be removed inside container $2
function wait_for_symlink_rm {
local domain="${1:?}"
Expand All @@ -67,11 +70,104 @@ function wait_for_symlink_rm {
}
export -f wait_for_symlink_rm

# Wait for a successful https connection to domain $1

# Attempt to grab the certificate from domain passed with -d/--domain
# then check if the subject either match or doesn't match the pattern
# passed with either -m/--match or -nm/--no-match
# If domain can't be reached return 1
function check_cert_subj {
while [[ $# -gt 0 ]]; do
local flag="$1"

case $flag in
-d|--domain)
local domain="${2:?}"
shift
shift
;;

-m|--match)
local re="${2:?}"
local match_rc=0
local no_match_rc=1
shift
shift
;;

-n|--no-match)
local re="${2:?}"
local match_rc=1
local no_match_rc=0
shift
shift
;;

*) #Unknown option
shift
;;
esac
done

if curl -k https://"$domain" > /dev/null 2>&1; then
local cert_subject
cert_subject="$(echo \
| openssl s_client -showcerts -servername "$domain" -connect "$domain:443" 2>/dev/null \
| openssl x509 -subject -noout)"
else
return 1
fi

if [[ "$cert_subject" =~ $re ]]; then
return $match_rc
else
return $no_match_rc
fi
}
export -f check_cert_subj


# Wait for a successful https connection to domain passed with -d/--domain then wait
# - until the served certificate isn't the default one (default behavior)
# - until the served certificate is the default one (--default-cert)
# - until the served certificate subject match a string (--subject-match)
function wait_for_conn {
local domain="${1:?}"
local action
local domain
local string

while [[ $# -gt 0 ]]; do
local flag="$1"

case $flag in
-d|--domain)
domain="${2:?}"
shift
shift
;;

--default-cert)
action='--match'
shift
;;

--subject-match)
action='--match'
string="$2"
shift
shift
;;

*) #Unknown option
shift
;;
esac
done

local i=0
until curl -k https://"$domain" > /dev/null 2>&1; do
action="${action:---no-match}"
string="${string:-letsencrypt-nginx-proxy-companion}"

until check_cert_subj --domain "$domain" "$action" "$string"; do
if [ $i -gt 120 ]; then
echo "Could not connect to $domain using https under two minutes, timing out."
return 1
Expand All @@ -83,6 +179,7 @@ function wait_for_conn {
}
export -f wait_for_conn


# Get the expiration date in unix epoch of domain $1 inside container $2
function get_cert_expiration_epoch {
local domain="${1:?}"
Expand Down

0 comments on commit eaec604

Please sign in to comment.