For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.
tunnel/deploy/all-tunnel-cert-hook
| 1 | #!/bin/sh |
| 2 | # certbot --deploy-hook for the ALL tunnel node's OWN certificate. certbot runs it after every successful renewal with RENEWED_LINEAGE set to the |
| 3 | # directory of the new certificate. It never trusts that the files are good and never reports success it did not see: |
| 4 | # 1. the new pair is checked: it is a certificate for THIS node's name, not about to expire, and its key is the key of the certificate; |
| 5 | # 2. it is installed atomically (key first, then chain; the node reads files it can use and refuses a pair that does not match, so a reader never takes half); |
| 6 | # 3. the node is told to reload (systemctl reload sends SIGHUP: no restart, no dropped handshake, the streams that are open keep their certificate); |
| 7 | # 4. when the node's /metrics can be read, it must now report THIS certificate's end. A node that kept the old one is a failure, not a log line. |
| 8 | # Any failure exits non-zero, so certbot fails, all-tunnel-certbot.service fails and all-tunnel-certbot-failed.service shouts (see that unit). |
| 9 | set -eu |
| 10 | umask 077 |
| 11 | here=$(dirname "$0") |
| 12 | . "${ALL_TUNNEL_LIB:-$here/all-tunnel-cert-lib.sh}" |
| 13 | |
| 14 | lineage="${RENEWED_LINEAGE:-}" |
| 15 | [ -n "$lineage" ] || die "RENEWED_LINEAGE is not set: this script is certbot's deploy hook" |
| 16 | src_cert="$lineage/fullchain.pem" |
| 17 | src_key="$lineage/privkey.pem" |
| 18 | [ -r "$src_cert" ] && [ -r "$src_key" ] || die "the renewed files are missing in $lineage" |
| 19 | |
| 20 | host=$(env_value TUNNEL_NODE_HOST) |
| 21 | dst_cert=$(env_value TUNNEL_CERT_FILE) |
| 22 | dst_key=$(env_value TUNNEL_KEY_FILE) |
| 23 | [ -n "$host" ] && [ -n "$dst_cert" ] && [ -n "$dst_key" ] || die "$ALL_TUNNEL_ENV does not say TUNNEL_NODE_HOST, TUNNEL_CERT_FILE and TUNNEL_KEY_FILE" |
| 24 | |
| 25 | openssl x509 -in "$src_cert" -noout -checkend 86400 >/dev/null || die "the renewed certificate is already expired or expires within a day" |
| 26 | openssl x509 -in "$src_cert" -noout -text | grep -Eq "DNS:$host(,|[[:space:]]|\$)" || die "the renewed certificate does not carry this node's name" |
| 27 | cert_key=$(openssl x509 -in "$src_cert" -noout -pubkey | openssl sha256) |
| 28 | file_key=$(openssl pkey -in "$src_key" -pubout | openssl sha256) |
| 29 | [ "$cert_key" = "$file_key" ] || die "the renewed key does not belong to the renewed certificate" |
| 30 | |
| 31 | install_atomic() { # install_atomic MODE SOURCE DESTINATION: copy beside the destination, then rename over it |
| 32 | mode=$1; from=$2; to=$3 |
| 33 | dir=$(dirname "$to") |
| 34 | [ -d "$dir" ] || install -d -m 0755 "$dir" |
| 35 | tmp="$to.new.$$" |
| 36 | install -m "$mode" "$from" "$tmp" |
| 37 | owner=$(stat -c %u:%g "$dir" 2>/dev/null || true) # the node's dynamic user owns its state directory; the files must be readable by it |
| 38 | if [ -n "$owner" ] && [ "$(id -u)" -eq 0 ]; then chown "$owner" "$tmp"; fi |
| 39 | mv -f "$tmp" "$to" |
| 40 | } |
| 41 | install_atomic 0600 "$src_key" "$dst_key" |
| 42 | install_atomic 0644 "$src_cert" "$dst_cert" |
| 43 | |
| 44 | if systemctl is-active --quiet "$ALL_TUNNEL_UNIT"; then |
| 45 | systemctl reload "$ALL_TUNNEL_UNIT" || die "the node did not accept the reload" |
| 46 | expected=$(not_after_epoch "$src_cert") || expected="" |
| 47 | if [ -n "$expected" ]; then |
| 48 | seen="" |
| 49 | tries=${ALL_TUNNEL_RELOAD_TRIES:-10} |
| 50 | i=0 |
| 51 | while [ "$i" -lt "$tries" ]; do |
| 52 | i=$((i + 1)) |
| 53 | seen=$(metric all_tunnel_node_cert_not_after_seconds) |
| 54 | [ -z "$seen" ] && break # /metrics cannot be read: the monitor and ct_watch.py still watch the certificate the node serves |
| 55 | [ "$seen" = "$expected" ] && break |
| 56 | sleep "${ALL_TUNNEL_RELOAD_PAUSE:-1}" |
| 57 | done |
| 58 | if [ -n "$seen" ] && [ "$seen" != "$expected" ]; then die "the node reloaded but still reports a certificate that ends at $seen, not $expected"; fi |
| 59 | [ -n "$seen" ] || say warning "the node's /metrics cannot be read: the new certificate was installed and the node told to reload, but not confirmed" |
| 60 | fi |
| 61 | else |
| 62 | say warning "the node is not running: the new certificate is installed, and the node reads it when it starts" |
| 63 | fi |
| 64 | say info "node certificate renewed and in service for $host" |