diff --git a/README.md b/README.md index 2c6b1487..4d922bb0 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,10 @@ If you want to create multi-domain ([SAN](https://www.digicert.com/subject-alter If you want to create test certificates that don't have the 5 certs/week/domain limits define the `LETSENCRYPT_TEST` environment variable with a value of `true` (in the containers where you request certificates with `LETSENCRYPT_HOST`). If you want to do this globally for all containers, set `ACME_CA_URI` as described below. ##### Automatic certificate renewal -Every hour (3600 seconds) the certificates are checked and every certificate that will expire in the next [30 days](https://github.com/kuba/simp_le/blob/ecf4290c4f7863bb5427b50cdd78bc3a5df79176/simp_le.py#L72) (90 days / 3) are renewed. +Every hour (3600 seconds) the certificates are checked and per default every certificate that will expire in the next [30 days](https://github.com/zenhack/simp_le/blob/a8a8013c097910f8f3cce046f1077b41b745673b/simp_le.py#L73) (90 days / 3) is renewed. + +If you want to manually set a different minimum validity for certificates, you can set the `LETSENCRYPT_MIN_VALIDIDTY` environment variable (in each container that defines the `LETSENCRYPT_HOST` variable) to the desired period in seconds. +Note that the possible values are internally capped at an upper bound of 7603200 (88 days) and a lower bound of 7200 (2 hours) as a security margin, considering that the Let's Encrypt CA does only issues certificates with a lifetime of [90 days](https://letsencrypt.org/2015/11/09/why-90-days.html) (upper bound), the rate limits imposed on certificate renewals are [5 per week](https://letsencrypt.org/docs/rate-limits/) (upper bound), and the fact that the certificates are checked and renewed accordingly every hour (lower bound). ##### Force certificates renewal If needed, you can force a running letsencrypt-nginx-proxy-companion container to renew all certificates that are currently in use. Replace `nginx-letsencrypt` with the name of your letsencrypt-nginx-proxy-companion container in the following command: diff --git a/app/letsencrypt_service b/app/letsencrypt_service index 9cd993d8..ebc0787c 100755 --- a/app/letsencrypt_service +++ b/app/letsencrypt_service @@ -8,6 +8,8 @@ ACME_CA_URI="${ACME_CA_URI:-https://acme-v01.api.letsencrypt.org/directory}" DEFAULT_KEY_SIZE=4096 REUSE_ACCOUNT_KEYS="$(lc ${REUSE_ACCOUNT_KEYS:-true})" REUSE_PRIVATE_KEYS="$(lc ${REUSE_PRIVATE_KEYS:-false})" +MIN_VALIDITY_CAP=7603200 +DEFAULT_MIN_VALIDITY=2592000 function create_link { local -r source=${1?missing source argument} @@ -174,7 +176,28 @@ function update_certs { [[ "$(lc $DEBUG)" == true ]] && params_d_str+=" -v" [[ $REUSE_PRIVATE_KEYS == true ]] && params_d_str+=" --reuse_key" - [[ "${1}" == "--force-renew" ]] && params_d_str+=" --valid_min 7776000" + + min_validity="LETSENCRYPT_${cid}_MIN_VALIDITY" + min_validity="${!min_validity}" + if [[ "$min_validity" == "" ]]; then + min_validity=$DEFAULT_MIN_VALIDITY + fi + # Sanity Check + # Upper Bound + if [[ $min_validity -gt $MIN_VALIDITY_CAP ]]; then + min_validity=$MIN_VALIDITY_CAP + fi + # Lower Bound + if [[ $min_validity -lt $(($seconds_to_wait * 2)) ]]; then + min_validity=$(($seconds_to_wait * 2)) + fi + + if [[ "${1}" == "--force-renew" ]]; then + # Manually set to highest certificate lifetime given by LE CA + params_d_str+=" --valid_min 7776000" + else + params_d_str+=" --valid_min $min_validity" + fi # Create directory for the first domain, # make it root readable only and make it the cwd diff --git a/app/letsencrypt_service_data.tmpl b/app/letsencrypt_service_data.tmpl index 3b3a4c78..e8c72a8d 100644 --- a/app/letsencrypt_service_data.tmpl +++ b/app/letsencrypt_service_data.tmpl @@ -11,6 +11,7 @@ LETSENCRYPT_{{ $cid }}_KEYSIZE="{{ $container.Env.LETSENCRYPT_KEYSIZE }}" LETSENCRYPT_{{ $cid }}_TEST="{{ $container.Env.LETSENCRYPT_TEST }}" LETSENCRYPT_{{ $cid }}_ACCOUNT_ALIAS="{{ $container.Env.LETSENCRYPT_ACCOUNT_ALIAS }}" LETSENCRYPT_{{ $cid }}_RESTART_CONTAINER="{{ $container.Env.LETSENCRYPT_RESTART_CONTAINER }}" +LETSENCRYPT_{{ $cid }}_MIN_VALIDITY="{{ $container.Env.LETSENCRYPT_MIN_VALIDITY }}" {{ end }} {{ end }} diff --git a/test/config.sh b/test/config.sh index ae70c3f2..9196611d 100755 --- a/test/config.sh +++ b/test/config.sh @@ -12,6 +12,7 @@ imageTests+=( certs_single certs_san force_renew + certs_validity container_restart permissions_default permissions_custom diff --git a/test/setup/setup-boulder.sh b/test/setup/setup-boulder.sh index fc1e0490..7e665642 100755 --- a/test/setup/setup-boulder.sh +++ b/test/setup/setup-boulder.sh @@ -11,12 +11,22 @@ setup_boulder() { $GOPATH/src/github.com/letsencrypt/boulder pushd $GOPATH/src/github.com/letsencrypt/boulder if [[ "$(uname)" == 'Darwin' ]]; then + # Set Standard Ports sed -i '' 's/ 5002/ 80/g' test/config/va.json sed -i '' 's/ 5001/ 443/g' test/config/va.json + # Set certificate lifetime to 88 days + sed -i '' 's/2160h/2112h/g' test/config/ca-a.json + sed -i '' 's/2160h/2112h/g' test/config/ca-b.json + # Modify custom rate limit sed -i '' 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml else + # Set Standard Ports sed --in-place 's/ 5002/ 80/g' test/config/va.json sed --in-place 's/ 5001/ 443/g' test/config/va.json + # Set certificate lifetime to 88 days + sed --in-place 's/2160h/2112h/g' test/config/ca-a.json + sed --in-place 's/2160h/2112h/g' test/config/ca-b.json + # Modify custom rate limit sed --in-place 's/le.wtf,le1.wtf/le1.wtf,le2.wtf,le3.wtf/g' test/rate-limit-policies.yml fi docker-compose build --pull diff --git a/test/tests/certs_validity/expected-std-out.txt b/test/tests/certs_validity/expected-std-out.txt new file mode 100644 index 00000000..09849c91 --- /dev/null +++ b/test/tests/certs_validity/expected-std-out.txt @@ -0,0 +1,13 @@ +Started letsencrypt container for test certs_validity +Started test web server for le1.wtf +Started test web server for le2.wtf +Started test web server for le3.wtf +Symlink to le1.wtf certificate has been generated. +The link is pointing to the file ./le1.wtf/fullchain.pem +Symlink to le2.wtf certificate has been generated. +The link is pointing to the file ./le2.wtf/fullchain.pem +Symlink to le3.wtf certificate has been generated. +The link is pointing to the file ./le3.wtf/fullchain.pem +Certificate for le1.wtf was not renewed. +Certificate for le2.wtf was not renewed. +Certificate for le3.wtf was renewed. diff --git a/test/tests/certs_validity/run.sh b/test/tests/certs_validity/run.sh new file mode 100755 index 00000000..8d348d85 --- /dev/null +++ b/test/tests/certs_validity/run.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +## Test for the LETSENCRYPT_MIN_VALIDITY environment variable. + +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 { + # Remove any remaining Nginx container(s) silently. + for domain in "${domains[@]}"; do + docker rm --force "$domain" > /dev/null 2>&1 + done + # Cleanup the files created by this run of the test to avoid foiling following test(s). + docker exec "$le_container_name" bash -c 'rm -rf /etc/nginx/certs/le?.wtf*' + # Stop the LE container + docker stop "$le_container_name" > /dev/null +} +trap cleanup EXIT + +# Run a separate nginx container for each domain in the $domains array. +# Default validity +docker run --rm -d \ + --name "${domains[0]}" \ + -e "VIRTUAL_HOST=${domains[0]}" \ + -e "LETSENCRYPT_HOST=${domains[0]}" \ + --network boulder_bluenet \ + nginx:alpine > /dev/null && echo "Started test web server for ${domains[0]}" +# Manual validity (same as default) +docker run --rm -d \ + --name "${domains[1]}" \ + -e "VIRTUAL_HOST=${domains[1]}" \ + -e "LETSENCRYPT_HOST=${domains[1]}" \ + -e "LETSENCRYPT_MIN_VALIDITY=2592000" \ + --network boulder_bluenet \ + nginx:alpine > /dev/null && echo "Started test web server for ${domains[1]}" +# Manual validity (few seconds shy of MIN_VALIDITY_CAP=7603200) +docker run --rm -d \ + --name "${domains[2]}" \ + -e "VIRTUAL_HOST=${domains[2]}" \ + -e "LETSENCRYPT_HOST=${domains[2]}" \ + -e "LETSENCRYPT_MIN_VALIDITY=7603190" \ + --network boulder_bluenet \ + nginx:alpine > /dev/null && echo "Started test web server for ${domains[2]}" + +# Wait for a symlinks +wait_for_symlink "${domains[0]}" "$le_container_name" +wait_for_symlink "${domains[1]}" "$le_container_name" +wait_for_symlink "${domains[2]}" "$le_container_name" +# Grab the expiration times of the certificates +first_cert_expire_1="$(get_cert_expiration_epoch "${domains[0]}" "$le_container_name")" +first_cert_expire_2="$(get_cert_expiration_epoch "${domains[1]}" "$le_container_name")" +first_cert_expire_3="$(get_cert_expiration_epoch "${domains[2]}" "$le_container_name")" + +# Wait for ${domains[2]} set certificate validity to expire +sleep 10 + +# Manually trigger letsencrypt_service +docker exec "$le_container_name" /bin/bash -c "source /app/letsencrypt_service --source-only; update_certs" > /dev/null 2>&1 + +# Grab the new expiration times of the certificates +second_cert_expire_1="$(get_cert_expiration_epoch "${domains[0]}" "$le_container_name")" +second_cert_expire_2="$(get_cert_expiration_epoch "${domains[1]}" "$le_container_name")" +second_cert_expire_3="$(get_cert_expiration_epoch "${domains[2]}" "$le_container_name")" + +if [[ $second_cert_expire_1 -eq $first_cert_expire_1 ]]; then + echo "Certificate for ${domains[0]} was not renewed." +else + echo "Certificate for ${domains[0]} was incorrectly renewed." + echo "First certificate expiration epoch : $first_cert_expire_1." + echo "Second certificate expiration epoch : $second_cert_expire_1." +fi +if [[ $second_cert_expire_2 -eq $first_cert_expire_2 ]]; then + echo "Certificate for ${domains[1]} was not renewed." +else + echo "Certificate for ${domains[1]} was incorrectly renewed." + echo "First certificate expiration epoch : $first_cert_expire_2." + echo "Second certificate expiration epoch : $second_cert_expire_2." +fi +if [[ $second_cert_expire_3 -gt $first_cert_expire_3 ]]; then + echo "Certificate for ${domains[2]} was renewed." +else + echo "Certificate for ${domains[2]} was not renewed." + echo "First certificate expiration epoch : $first_cert_expire_3." + echo "Second certificate expiration epoch : $second_cert_expire_3." +fi