From 99dd615e55d5fb6d2cd6447d747e434b76dc8c82 Mon Sep 17 00:00:00 2001 From: Rich Bayliss Date: Tue, 5 Mar 2019 11:36:27 +0000 Subject: [PATCH] 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 --- cert-provider/Dockerfile | 20 ++++ cert-provider/cert-provider.sh | 181 +++++++++++++++++++++++++++++++ cert-provider/entry.sh | 3 + cert-provider/fake-le-bundle.pem | 56 ++++++++++ compose/services.yml | 51 ++++++--- haproxy/Dockerfile | 10 +- haproxy/entry.sh | 11 -- haproxy/haproxy.cfg | 9 ++ haproxy/start-haproxy.sh | 32 ++++++ scripts/make-env | 1 + scripts/quickstart | 27 ++++- 11 files changed, 364 insertions(+), 37 deletions(-) create mode 100644 cert-provider/Dockerfile create mode 100755 cert-provider/cert-provider.sh create mode 100755 cert-provider/entry.sh create mode 100644 cert-provider/fake-le-bundle.pem delete mode 100755 haproxy/entry.sh create mode 100755 haproxy/start-haproxy.sh diff --git a/cert-provider/Dockerfile b/cert-provider/Dockerfile new file mode 100644 index 0000000..497da15 --- /dev/null +++ b/cert-provider/Dockerfile @@ -0,0 +1,20 @@ +FROM alpine + +EXPOSE 80 +WORKDIR /usr/src/app +VOLUME [ "/usr/src/app/certs" ] + +RUN apk add --update bash curl git openssl ncurses socat + +RUN git clone https://github.com/Neilpang/acme.sh.git && \ + cd acme.sh && \ + git checkout 08357e3cb0d80c84bdaf3e42ce0e439665387f57 . && \ + ./acme.sh --install \ + --cert-home /usr/src/app/certs + +COPY entry.sh /entry.sh +COPY cert-provider.sh ./cert-provider.sh +COPY fake-le-bundle.pem ./ + +ENTRYPOINT [ "/entry.sh" ] +CMD [ "/usr/src/app/cert-provider.sh" ] \ No newline at end of file diff --git a/cert-provider/cert-provider.sh b/cert-provider/cert-provider.sh new file mode 100755 index 0000000..d4b20bc --- /dev/null +++ b/cert-provider/cert-provider.sh @@ -0,0 +1,181 @@ +#!/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 \ No newline at end of file diff --git a/cert-provider/entry.sh b/cert-provider/entry.sh new file mode 100755 index 0000000..5fc4448 --- /dev/null +++ b/cert-provider/entry.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec "$@" diff --git a/cert-provider/fake-le-bundle.pem b/cert-provider/fake-le-bundle.pem new file mode 100644 index 0000000..c2e33c6 --- /dev/null +++ b/cert-provider/fake-le-bundle.pem @@ -0,0 +1,56 @@ +-----BEGIN CERTIFICATE----- +MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2 +MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ +diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP +xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG +TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj +EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd +O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa +aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0 +A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr +IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe +Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb +Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50 +qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA +A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln +uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H +sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm +dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd +oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV +/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ +zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc +VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1 +Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4 +8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c +idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 +MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 +8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym +oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 +ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN +xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 +dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 +AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw +HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 +BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu +b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu +Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq +hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF +UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 +AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp +DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 +IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf +zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI +PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w +SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em +2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 +WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt +n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= +-----END CERTIFICATE----- diff --git a/compose/services.yml b/compose/services.yml index d4620cf..b91dead 100644 --- a/compose/services.yml +++ b/compose/services.yml @@ -1,10 +1,12 @@ -version: '2.1' +version: "2.1" volumes: - db: - registry: - s3: - redis: + certs: {} + cert-provider: {} + db: {} + redis: {} + registry: {} + s3: {} services: api: @@ -20,7 +22,7 @@ services: API_VPN_SERVICE_API_KEY: ${OPENBALENA_API_VPN_SERVICE_API_KEY} BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA} COOKIE_SESSION_SECRET: ${OPENBALENA_COOKIE_SESSION_SECRET} - DB_HOST: db.${OPENBALENA_HOST_NAME} + DB_HOST: db DB_PASSWORD: docker DB_PORT: 5432 DB_USER: docker @@ -36,10 +38,10 @@ services: JSON_WEB_TOKEN_EXPIRY_MINUTES: 10080 JSON_WEB_TOKEN_SECRET: ${OPENBALENA_JWT_SECRET} MIXPANEL_TOKEN: __unused__ - PRODUCTION_MODE: '${OPENBALENA_PRODUCTION_MODE}' + PRODUCTION_MODE: "${OPENBALENA_PRODUCTION_MODE}" PUBNUB_PUBLISH_KEY: __unused__ PUBNUB_SUBSCRIBE_KEY: __unused__ - REDIS_HOST: redis.${OPENBALENA_HOST_NAME} + REDIS_HOST: redis REDIS_PORT: 6379 REGISTRY2_HOST: registry.${OPENBALENA_HOST_NAME} REGISTRY_HOST: registry.${OPENBALENA_HOST_NAME} @@ -49,7 +51,7 @@ services: TOKEN_AUTH_CERT_KEY: ${OPENBALENA_TOKEN_AUTH_KEY} TOKEN_AUTH_CERT_KID: ${OPENBALENA_TOKEN_AUTH_KID} TOKEN_AUTH_CERT_PUB: ${OPENBALENA_TOKEN_AUTH_PUB} - TOKEN_AUTH_JWT_ALGO: 'ES256' + TOKEN_AUTH_JWT_ALGO: "ES256" VPN_HOST: vpn.${OPENBALENA_HOST_NAME} VPN_PORT: 443 VPN_SERVICE_API_KEY: ${OPENBALENA_VPN_SERVICE_API_KEY} @@ -73,10 +75,10 @@ services: BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA} BALENA_TOKEN_AUTH_ISSUER: api.${OPENBALENA_HOST_NAME} BALENA_TOKEN_AUTH_REALM: https://api.${OPENBALENA_HOST_NAME}/auth/v1/token - COMMON_REGION: - REGISTRY2_S3_BUCKET: - REGISTRY2_S3_KEY: - REGISTRY2_S3_SECRET: + COMMON_REGION: + REGISTRY2_S3_BUCKET: + REGISTRY2_S3_KEY: + REGISTRY2_S3_SECRET: REGISTRY2_SECRETKEY: ${OPENBALENA_REGISTRY_SECRET_KEY} REGISTRY2_STORAGEPATH: /data @@ -94,10 +96,10 @@ services: BALENA_API_HOST: api.${OPENBALENA_HOST_NAME} BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA} BALENA_VPN_PORT: 443 - PRODUCTION_MODE: '${OPENBALENA_PRODUCTION_MODE}' + PRODUCTION_MODE: "${OPENBALENA_PRODUCTION_MODE}" RESIN_VPN_GATEWAY: 10.2.0.1 - SENTRY_DSN: - VPN_HAPROXY_USEPROXYPROTOCOL: 'true' + SENTRY_DSN: + VPN_HAPROXY_USEPROXYPROTOCOL: "true" VPN_OPENVPN_CA_CRT: ${OPENBALENA_VPN_CA} VPN_OPENVPN_SERVER_CRT: ${OPENBALENA_VPN_SERVER_CRT} VPN_OPENVPN_SERVER_DH: ${OPENBALENA_VPN_SERVER_DH} @@ -135,11 +137,12 @@ services: build: ../haproxy depends_on: - api - - registry - - vpn + - cert-provider - db - s3 - redis + - registry + - vpn ports: - "80:80" - "443:443" @@ -162,3 +165,15 @@ services: BALENA_HAPROXY_KEY: ${OPENBALENA_ROOT_KEY} BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA} HAPROXY_HOSTNAME: ${OPENBALENA_HOST_NAME} + volumes: + - certs:/certs:ro + + cert-provider: + build: ../cert-provider + volumes: + - certs:/certs + - cert-provider:/usr/src/app/certs + environment: + ACTIVE: ${OPENBALENA_ACME_CERT_ENABLED} + DOMAINS: "api.${OPENBALENA_HOST_NAME},registry.${OPENBALENA_HOST_NAME},s3.${OPENBALENA_HOST_NAME},vpn.${OPENBALENA_HOST_NAME}" + OUTPUT_PEM: /certs/open-balena.pem diff --git a/haproxy/Dockerfile b/haproxy/Dockerfile index 3416c32..8d10f5d 100644 --- a/haproxy/Dockerfile +++ b/haproxy/Dockerfile @@ -1,6 +1,10 @@ -FROM haproxy:1.8-alpine +FROM haproxy:1.9-alpine + +VOLUME [ "/certs" ] + +RUN apk add --update inotify-tools COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg -COPY entry.sh /open-balena-entry +COPY start-haproxy.sh /start-haproxy -CMD /open-balena-entry +CMD /start-haproxy diff --git a/haproxy/entry.sh b/haproxy/entry.sh deleted file mode 100755 index b89e333..0000000 --- a/haproxy/entry.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -e - -HAPROXY_CHAIN=/etc/ssl/private/open-balena.pem -mkdir -p "$(dirname "${HAPROXY_CHAIN}")" -( - echo "${BALENA_HAPROXY_CRT}" | base64 -d - echo "${BALENA_HAPROXY_KEY}" | base64 -d - echo "${BALENA_ROOT_CA}" | base64 -d -) > "${HAPROXY_CHAIN}" -exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index b33137e..d76a756 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -12,6 +12,9 @@ frontend http-in bind *:80 reqadd X-Forwarded-Proto:\ http + acl is_cert_validation path -i -m beg "/.well-known/acme-challenge/" + use_backend cert-provider if is_cert_validation + acl host_api hdr_dom(host) -i "api.${HAPROXY_HOSTNAME}" use_backend backend_api if host_api @@ -80,6 +83,12 @@ backend backend_s3 option forwardfor balance roundrobin +backend cert-provider + mode http + option forwardfor + balance roundrobin + server resin_cert-provider_1 cert-provider:80 no-check + backend vpn-devices mode tcp server resin_vpn_1 vpn:443 send-proxy-v2 check-send-proxy port 443 diff --git a/haproxy/start-haproxy.sh b/haproxy/start-haproxy.sh new file mode 100755 index 0000000..dad19da --- /dev/null +++ b/haproxy/start-haproxy.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +OPENBALENA_CERT=/etc/ssl/private/open-balena.pem +mkdir -p "$(dirname "${OPENBALENA_CERT}")" + +if [ -f "/certs/open-balena.pem" ]; then + echo "Using certificate from cert-provider..." + cp /certs/open-balena.pem "${OPENBALENA_CERT}" +else + echo "Building certificate from environment variables..." + ( + echo "${BALENA_HAPROXY_CRT}" | base64 -d + echo "${BALENA_HAPROXY_KEY}" | base64 -d + echo "${BALENA_ROOT_CA}" | base64 -d + ) > "${OPENBALENA_CERT}" +fi + +haproxy -f /usr/local/etc/haproxy/haproxy.cfg -W & +HAPROXY_PID=$! + +while true; do + inotifywait -r -e create -e modify -e delete /certs + + if [ -f "/certs/open-balena.pem" ]; then + echo "Updating certificate from cert-provider..." + cp /certs/open-balena.pem "${OPENBALENA_CERT}" + fi + + echo "Certificate change detected. Reloading..." + kill -SIGUSR2 $HAPROXY_PID + sleep 1; +done diff --git a/scripts/make-env b/scripts/make-env index 68914e7..203f5b5 100755 --- a/scripts/make-env +++ b/scripts/make-env @@ -94,4 +94,5 @@ export OPENBALENA_REGISTRY_SECRET_KEY=$(randstr 32) export OPENBALENA_SSH_AUTHORIZED_KEYS= export OPENBALENA_SUPERUSER_EMAIL=$SUPERUSER_EMAIL export OPENBALENA_SUPERUSER_PASSWORD=$(printf "%q" "${SUPERUSER_PASSWORD}") +export OPENBALENA_ACME_CERT_ENABLED=${ACME_CERT_ENABLED:-false} STR diff --git a/scripts/quickstart b/scripts/quickstart index 020e268..b341e83 100755 --- a/scripts/quickstart +++ b/scripts/quickstart @@ -21,6 +21,10 @@ fi source "${BASH_SOURCE%/*}/_realpath" +domainResolves() { + getent hosts "$1" > /dev/null 2>&1 +} + CMD="$(realpath "$0")" DIR="$(dirname "${CMD}")" BASE_DIR="$(dirname "${DIR}")" @@ -30,8 +34,9 @@ CERTS_DIR="${CONFIG_DIR}/certs" DOMAIN=openbalena.local usage() { - echo "usage: $0 [-h] [-p] [-d DOMAIN] -U EMAIL -P PASSWORD" + echo "usage: $0 [-c] [-h] [-p] [-d DOMAIN] -U EMAIL -P PASSWORD" echo + echo " -c enable the ACME certificate service in staging or production mode." echo " -p patch hosts - patch the host /etc/hosts file" echo " -d DOMAIN the domain name this deployment will run as, eg. example.com. Default is 'openbalena.local'" echo " -U EMAIL the email address of the superuser account, used to login to your install from the Balena CLI" @@ -41,7 +46,7 @@ usage() { show_help=false patch_hosts=false -while getopts ":hpxd:U:P:" opt; do +while getopts ":chpxd:U:P:" opt; do case "${opt}" in h) show_help=true;; p) patch_hosts=true;; @@ -49,6 +54,7 @@ while getopts ":hpxd:U:P:" opt; do d) DOMAIN="${OPTARG}";; U) SUPERUSER_EMAIL="${OPTARG}";; P) SUPERUSER_PASSWORD="${OPTARG}";; + c) ACME_CERT_ENABLED="true";; *) echo "Invalid argument: -${OPTARG}" usage @@ -68,8 +74,17 @@ if [ "$show_help" = "true" ]; then exit 1 fi +if [ ! -z "$ACME_CERT_ENABLED" ]; then + echo "${BLUE}[INFO]${RESET} ACME Certificate request is ${BOLD}ENABLED${RESET}." + + if ! domainResolves "api.${DOMAIN}"; then + echo "${YELLOW}[WARN]${RESET} Unable to resolve \"api.${DOMAIN}\"!" + echo "${YELLOW}[WARN]${RESET} This might mean that you cannot use an ACME issued certificate." + fi +fi + echo_bold() { - printf "\\033[1m%s\\033[0m\\n" "${@}" + echo "${BOLD}${@}${RESET}" } echo_bold "==> Creating new configuration at: $CONFIG_DIR" @@ -110,5 +125,7 @@ fi echo_bold "==> Success!" echo ' - Start the instance with: ./scripts/compose up -d' echo ' - Stop the instance with: ./scripts/compose stop' -echo ' - To create the superuser, see: ./scripts/create-superuser -h' -echo " - Use the following certificate with Balena CLI: ${CERTS_DIR}/root/ca.crt" + +if [ -z "${ACME_CERT_ENABLED}" ]; then + echo " - Use the following certificate with Balena CLI: ${CERTS_DIR}/root/ca.crt" +fi