open-balena/cert-provider/cert-provider.sh
Rich Bayliss 99dd615e55
certs: Add support for an ACME certificate provider
Add a service which will acquire certificates from an ACME cert
provider, such as LetsEncrypt (), to allow an openBalena instance
to use a publicly trusted certificate instead of the self-signed
one it wil generate on setup.

Change-type: patch
Signed-off-by: Rich Bayliss <rich@balena.io>
2019-03-08 12:23:46 +00:00

181 lines
4.9 KiB
Bash
Executable File

#!/bin/bash
# the acme.sh client script, installed via Git in the Dockerfile...
ACME_BIN="$(realpath ~/.acme.sh/acme.sh)"
# the path to a bundle of certs to verify a LetsEncrypt staging certificate until Apr 2036...
ACME_STAGING_CA="/usr/src/app/fake-le-bundle.pem"
# the path to a file which stores the last successful mode of certificate we acquired...
ACME_MODE_FILE="/usr/src/app/certs/last_run_mode"
# colour output helpers...
reset=$(tput -T xterm sgr0)
red=$(tput -T xterm setaf 1)
green=$(tput -T xterm setaf 2)
yellow=$(tput -T xterm setaf 3)
blue=$(tput -T xterm setaf 4)
logError() {
echo "${red}[Error]${reset} $1"
}
logWarn() {
echo "${yellow}[Warn]${reset} $1"
}
logInfo() {
echo "${blue}[Info]${reset} $1"
}
logSuccess() {
echo "${green}[Success]${reset} $1"
}
logErrorAndStop() {
logError "$1 [Stopping]"
while true; do
# do nothing forever...
sleep 60
done
}
retryWithDelay() {
RETRIES=${2:-3}
DELAY=${3:-5}
local ATTEMPT=0
while [ $RETRIES -gt $ATTEMPT ]; do
let "ATTEMPT++"
if $1; then
return $?
fi
echo "($ATTEMPT/$RETRIES) Retrying in ${DELAY} seconds..."
sleep $DELAY
done
return 1
}
waitForOnline() {
ADDRESS="${1,,}"
logInfo "Waiting for ${ADDRESS} to be available via HTTP..."
retryWithDelay "curl --output /dev/null --silent --head --fail http://${ADDRESS}" 6 5
}
isUsingStagingCert() {
HOST="${1,,}"
echo "" | openssl s_client -host "$HOST" -port 443 -showcerts 2>/dev/null | awk '/BEGIN CERT/ {p=1} ; p==1; /END CERT/ {p=0}' | openssl verify -CAfile "$ACME_STAGING_CA" > /dev/null 2>&1
}
pre-flight() {
case "$ACTIVE" in
"true"|"yes")
;;
*)
logError "ACTIVE variable is not enabled. Value should be \"true\" or \"yes\" to continue."
return 1
;;
esac
if [ -z "$DOMAINS" ]; then
logError "DOMAINS must be set. Value should be a comma-delimited string of domains."
return 1
else
IFS=, read -r -a ACME_DOMAINS <<< "$DOMAINS"
IFS=' ' read -r -a ACME_DOMAIN_ARGS <<< "${ACME_DOMAINS[@]/#/-d }"
fi
if [ -z "$VALIDATION" ]; then
logInfo "VALIDATION not set. Using default: http-01"
VALIDATION="http-01"
else
case "$VALIDATION" in
"http-01")
logInfo "Using validation method: $VALIDATION"
;;
*)
logError "VALIDATION is invalid. Use a valid value: http-01"
return 1
;;
esac
fi
if [ -z "$OUTPUT_PEM" ]; then
logError "OUTPUT_PEM must be set. Value should be the path to install your certificate to."
return 1
fi
}
waitToSeeStagingCert() {
logInfo "Waiting for ${ACME_DOMAINS[0]} to use a staging certificate..."
retryWithDelay "isUsingStagingCert ${ACME_DOMAINS[0]}" 3 5
}
lastAcquiredCertFor() {
ACME_MODE="${1:-none}"
ACME_LAST_MODE="$(cat $ACME_MODE_FILE || echo '')"
logInfo "Last acquired certificate for ${ACME_LAST_MODE^^}"
[ "${ACME_LAST_MODE,,}" == "${ACME_MODE,,}" ]
}
acquireCertificate() {
ACME_MODE="${1:-staging}"
ACME_FORCE="${2:-false}"
ACME_OPTS=()
if [ "${ACME_FORCE,,}" == "true" ];then ACME_OPTS+=("--force"); fi
case "$ACME_MODE" in
"production")
logInfo "Using PRODUCTION mode"
;;
*)
logInfo "Using STAGING mode"
ACME_OPTS+=("--staging")
;;
esac
case "$VALIDATION" in
"http-01")
ACME_OPTS+=("--standalone")
;;
*)
logError "VALIDATION is invalid. Use a valid value: http-01"
return 1
;;
esac
if ! waitForOnline "${ACME_DOMAINS[0]}"; then
logError "Unable to access site over HTTP"
return 1
fi
logInfo "Issuing certificates..."
"$ACME_BIN" --issue "${ACME_OPTS[@]}" "${ACME_DOMAIN_ARGS[@]}"
logInfo "Installing certificates..." && \
"$ACME_BIN" --install-cert "${ACME_DOMAIN_ARGS[@]}" \
--cert-file /tmp/cert.pem \
--key-file /tmp/key.pem \
--fullchain-file /tmp/fullchain.pem \
--reloadcmd "cat /tmp/fullchain.pem /tmp/key.pem > $OUTPUT_PEM" && \
echo "${ACME_MODE}" > "${ACME_MODE_FILE}"
}
pre-flight || logErrorAndStop "Unable to continue due to misconfiguration. See errors above."
waitForOnline "${ACME_DOMAINS[0]}" || logErrorAndStop "Unable to access ${ACME_DOMAINS[0]} on port 80. This is needed for certificate validation."
if ! lastAcquiredCertFor "production"; then
acquireCertificate "staging" || logErrorAndStop "Unable to acquire a staging certificate."
waitToSeeStagingCert || logErrorAndStop "Unable to detect certificate change over. Cannot issue a production certificate."
acquireCertificate "production" "true" || logErrorAndStop "Unable to acquire a production certificate."
fi
logSuccess "Done!"
logInfo "Running cron..."
crond -f -d 7