|
1 | 1 | #!/usr/bin/env bash |
2 | 2 |
|
3 | | -set -eou pipefail |
| 3 | +set -euo pipefail |
4 | 4 |
|
5 | | -echo "Copying Lehigh's certs into traefik" |
| 5 | +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| 6 | +export PATH |
| 7 | +umask 077 |
6 | 8 |
|
7 | | -cat /etc/ssl/certs/lib.lehigh.edu.crt /etc/ssl/certs/gd_bundle-g2-g1.crt | sudo tee certs/cert.pem |
| 9 | +ERROR_MESSAGE="unexpected error" |
| 10 | +STAGED_CERT="" |
| 11 | +STAGED_KEY="" |
8 | 12 |
|
9 | | -sudo cp /etc/ssl/private/lib.lehigh.edu.key certs/privkey.pem |
10 | | -sudo chmod 700 certs/privkey.pem |
11 | | -sudo chown root certs/privkey.pem |
| 13 | +send_slack_message() { |
| 14 | + local payload |
| 15 | + |
| 16 | + [[ -n "${SLACK_WEBHOOK:-}" ]] || return 1 |
| 17 | + payload="$(jq -cn --arg msg "$*" '{msg: $msg}')" |
| 18 | + |
| 19 | + curl --fail --silent --show-error --output /dev/null \ |
| 20 | + --connect-timeout 5 --max-time 15 \ |
| 21 | + --header 'Content-Type: application/json' \ |
| 22 | + --request POST --data-binary "${payload}" "${SLACK_WEBHOOK}" |
| 23 | +} |
| 24 | + |
| 25 | +cleanup() { |
| 26 | + [[ -z "${STAGED_CERT}" ]] || rm -f -- "${STAGED_CERT}" |
| 27 | + [[ -z "${STAGED_KEY}" ]] || rm -f -- "${STAGED_KEY}" |
| 28 | +} |
| 29 | + |
| 30 | +handle_error() { |
| 31 | + local exit_code="$1" |
| 32 | + local host |
| 33 | + local message |
| 34 | + |
| 35 | + trap - ERR |
| 36 | + set +e |
| 37 | + host="$(hostname --fqdn 2>/dev/null || hostname)" |
| 38 | + message="🚨 ${ALERT_CONTEXT:-TLS certificate rollout} failed on ${host}: ${ERROR_MESSAGE} 🚨" |
| 39 | + |
| 40 | + printf '%s\n' "${message}" >&2 |
| 41 | + logger --priority daemon.err --tag local-cert-hook -- "${message}" || true |
| 42 | + send_slack_message "${message}" || \ |
| 43 | + printf '%s\n' 'Slack alert could not be sent; check SLACK_WEBHOOK, curl, and jq.' >&2 |
| 44 | + |
| 45 | + exit "${exit_code}" |
| 46 | +} |
| 47 | + |
| 48 | +fail() { |
| 49 | + ERROR_MESSAGE="$*" |
| 50 | + return 1 |
| 51 | +} |
| 52 | + |
| 53 | +trap 'handle_error "$?"' ERR |
| 54 | +trap cleanup EXIT |
| 55 | + |
| 56 | +HOOK_CONFIG="${LOCAL_CERT_HOOK_CONFIG:-/etc/default/local-cert-hook}" |
| 57 | +if [[ -r "${HOOK_CONFIG}" ]]; then |
| 58 | + # shellcheck disable=SC1090 |
| 59 | + source "${HOOK_CONFIG}" |
| 60 | +fi |
| 61 | + |
| 62 | +SOURCE_CERT="${SOURCE_CERT:-/etc/ssl/certs/le/lib.lehigh.edu.pem}" |
| 63 | +SOURCE_KEY="${SOURCE_KEY:-/etc/ssl/private/le/lib.lehigh.edu.key}" |
| 64 | +STACK_DIR="${STACK_DIR:-/opt/archivesspace}" |
| 65 | +CERT_DIR="${CERT_DIR:-${STACK_DIR}/certs}" |
| 66 | +TARGET_CERT="${CERT_DIR}/cert.pem" |
| 67 | +TARGET_KEY="${CERT_DIR}/privkey.pem" |
| 68 | +RESTART_REQUIRED="${CERT_DIR}/.traefik-restart-required" |
| 69 | +EXPECTED_HOST="${EXPECTED_HOST:-archivesspace.lib.lehigh.edu}" |
| 70 | +HEALTHCHECK_TIMEOUT="${HEALTHCHECK_TIMEOUT:-60}" |
| 71 | +ALERT_CONTEXT="${ALERT_CONTEXT:-ArchivesSpace TLS certificate rollout}" |
| 72 | + |
| 73 | +COMPOSE=(docker compose --project-directory "${STACK_DIR}" -f "${STACK_DIR}/docker-compose.yml") |
| 74 | +[[ ! -f "${STACK_DIR}/.env" ]] || COMPOSE+=(--env-file "${STACK_DIR}/.env") |
| 75 | + |
| 76 | +compose() { |
| 77 | + "${COMPOSE[@]}" "$@" |
| 78 | +} |
| 79 | + |
| 80 | +sha256_file() { |
| 81 | + sha256sum < "$1" |
| 82 | +} |
| 83 | + |
| 84 | +validate_certificate() { |
| 85 | + local cert_public_key |
| 86 | + local key_public_key |
| 87 | + |
| 88 | + [[ "${SLACK_WEBHOOK:-}" == https://* ]] || fail 'SLACK_WEBHOOK is not configured' |
| 89 | + [[ -r "${SOURCE_CERT}" ]] || fail "certificate is not readable: ${SOURCE_CERT}" |
| 90 | + [[ -r "${SOURCE_KEY}" ]] || fail "private key is not readable: ${SOURCE_KEY}" |
| 91 | + [[ "$(grep -cF -- '-----BEGIN CERTIFICATE-----' "${SOURCE_CERT}" || true)" -ge 2 ]] || \ |
| 92 | + fail 'certificate does not contain a full chain' |
| 93 | + |
| 94 | + openssl x509 -in "${SOURCE_CERT}" -noout -checkend 86400 >/dev/null 2>&1 || \ |
| 95 | + fail 'certificate is expired or expires within 24 hours' |
| 96 | + openssl x509 -in "${SOURCE_CERT}" -noout -checkhost "${EXPECTED_HOST}" >/dev/null 2>&1 || \ |
| 97 | + fail "certificate does not cover ${EXPECTED_HOST}" |
| 98 | + openssl verify -purpose sslserver -untrusted "${SOURCE_CERT}" "${SOURCE_CERT}" >/dev/null 2>&1 || \ |
| 99 | + fail 'certificate chain is not trusted for TLS server use' |
| 100 | + openssl pkey -in "${SOURCE_KEY}" -check -noout </dev/null >/dev/null 2>&1 || \ |
| 101 | + fail 'private key is invalid or encrypted' |
| 102 | + |
| 103 | + cert_public_key="$(openssl x509 -in "${SOURCE_CERT}" -pubkey -noout | |
| 104 | + openssl pkey -pubin -outform DER 2>/dev/null | sha256sum)" |
| 105 | + key_public_key="$(openssl pkey -in "${SOURCE_KEY}" -pubout -outform DER </dev/null 2>/dev/null | |
| 106 | + sha256sum)" |
| 107 | + [[ "${cert_public_key}" == "${key_public_key}" ]] || fail 'certificate and private key do not match' |
| 108 | +} |
| 109 | + |
| 110 | +secrets_are_current() { |
| 111 | + [[ -f "${TARGET_CERT}" && -f "${TARGET_KEY}" ]] || return 1 |
| 112 | + [[ "$(sha256_file "${SOURCE_CERT}")" == "$(sha256_file "${TARGET_CERT}")" ]] || return 1 |
| 113 | + [[ "$(sha256_file "${SOURCE_KEY}")" == "$(sha256_file "${TARGET_KEY}")" ]] |
| 114 | +} |
| 115 | + |
| 116 | +main() { |
| 117 | + validate_certificate |
| 118 | + install -d -o root -g root -m 0755 "${CERT_DIR}" |
| 119 | + |
| 120 | + if secrets_are_current && [[ ! -e "${RESTART_REQUIRED}" ]]; then |
| 121 | + printf '%s\n' 'TLS certificate is already current; Traefik was not recreated.' |
| 122 | + return |
| 123 | + fi |
| 124 | + |
| 125 | + ERROR_MESSAGE='Compose configuration validation failed' |
| 126 | + compose config --quiet |
| 127 | + |
| 128 | + if ! secrets_are_current; then |
| 129 | + ERROR_MESSAGE='could not copy the renewed TLS certificate' |
| 130 | + install -o root -g root -m 0600 /dev/null "${RESTART_REQUIRED}" |
| 131 | + STAGED_CERT="${TARGET_CERT}.new" |
| 132 | + STAGED_KEY="${TARGET_KEY}.new" |
| 133 | + install -o root -g root -m 0644 "${SOURCE_CERT}" "${STAGED_CERT}" |
| 134 | + install -o root -g root -m 0600 "${SOURCE_KEY}" "${STAGED_KEY}" |
| 135 | + mv -f "${STAGED_KEY}" "${TARGET_KEY}" |
| 136 | + STAGED_KEY="" |
| 137 | + mv -f "${STAGED_CERT}" "${TARGET_CERT}" |
| 138 | + STAGED_CERT="" |
| 139 | + fi |
| 140 | + |
| 141 | + ERROR_MESSAGE='Traefik failed to restart or become healthy' |
| 142 | + compose up -d --no-deps --no-build --pull never --force-recreate \ |
| 143 | + --wait --wait-timeout "${HEALTHCHECK_TIMEOUT}" traefik |
| 144 | + |
| 145 | + rm -f -- "${RESTART_REQUIRED}" |
| 146 | + printf '%s\n' 'TLS certificate copied; Traefik is healthy.' |
| 147 | +} |
| 148 | + |
| 149 | +main "$@" |
0 commit comments