The dashboard-hosting stack from the previous commit is gone; it was answering a question nobody asked. What was actually needed is Chrome's real update channel, which an unpacked extension does not have and cannot be given. So the extension is now signed into a CRX with an update manifest beside it, served from the droplet, and installed on the trading machines by policy (ExtensionInstallForcelist). From there Chrome installs it, updates it on its own schedule, and puts it back if it goes missing. The manifest gained the `key` that fixes the extension id, so an unpacked development load keeps the same identity as the published build. The self-update check now branches on how it was installed: policy installs ask Chrome to check the update server now rather than waiting out its five-hour cycle, and unpacked loads keep the reload-from-disk path. chrome.management .getSelf() tells the two apart and needs no permission to do it. The signing key lives at /opt/autofirmer-updates/key.pem on the droplet and nowhere else. It is the extension's identity: a different key is a different extension, and no browser would take it as an update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
2.4 KiB
Bash
Executable File
52 lines
2.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# First run only: get a certificate for EXT_HOST, then hand off to deploy.sh.
|
|
#
|
|
# This uses the autotrader stack's certbot container and its Let's Encrypt
|
|
# account, so it agrees to the Subscriber Agreement the same way that stack's
|
|
# own bootstrap did (certbot --agree-tos):
|
|
# https://letsencrypt.org/repository/
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
|
|
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$1" >&2; exit 1; }
|
|
|
|
[ -f .env ] || fail "no .env — copy .env.example to .env and fill it in"
|
|
set -a; . ./.env; set +a
|
|
: "${EXT_HOST:?set EXT_HOST in .env}"
|
|
SHARED_STACK="${SHARED_STACK:-/opt/autotrader/deploy}"
|
|
|
|
# ACME_EMAIL is not duplicated here: it belongs to the account that already
|
|
# holds the other certificates on this box.
|
|
[ -f "$SHARED_STACK/.env" ] || fail "no .env under $SHARED_STACK"
|
|
ACME_EMAIL=$(grep -E '^ACME_EMAIL=' "$SHARED_STACK/.env" | cut -d= -f2-)
|
|
[ -n "$ACME_EMAIL" ] || fail "ACME_EMAIL is not set in $SHARED_STACK/.env"
|
|
|
|
# Let's Encrypt rate-limits failures, so check DNS before spending an attempt.
|
|
log "Checking DNS"
|
|
ip=$(curl -fsS --max-time 5 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address 2>/dev/null \
|
|
|| curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null) \
|
|
|| fail "could not determine this droplet's public IP"
|
|
got=$(getent ahostsv4 "$EXT_HOST" 2>/dev/null | awk 'NR==1{print $1}') || true
|
|
echo " droplet: $ip"
|
|
echo " $EXT_HOST -> ${got:-<unresolved>}"
|
|
[ "$got" = "$ip" ] || fail "$EXT_HOST does not resolve to $ip — fix DNS and wait for it to propagate"
|
|
|
|
# The challenge is served over plain HTTP by the shared nginx, which needs a
|
|
# server block for this name before it will answer for it.
|
|
log "Rendering the HTTP-only config so the challenge can be served"
|
|
envsubst '${EXT_HOST}' < nginx/ext-acme.conf.template > "$SHARED_STACK/nginx/conf.d/ext.conf"
|
|
docker exec nginx nginx -t
|
|
docker exec nginx nginx -s reload
|
|
|
|
log "Requesting a certificate for $EXT_HOST"
|
|
docker compose --project-directory "$SHARED_STACK" -f "$SHARED_STACK/docker-compose.yml" \
|
|
run --rm --entrypoint certbot certbot \
|
|
certonly --webroot -w /var/www/certbot \
|
|
--email "$ACME_EMAIL" --agree-tos --no-eff-email \
|
|
--non-interactive -d "$EXT_HOST" \
|
|
|| fail "certbot failed — check DNS and that port 80 reaches this droplet"
|
|
|
|
log "Switching to the TLS config"
|
|
exec ./deploy.sh
|