diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c5a7999..0286305 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -45,7 +45,7 @@ configs, deploy scripts, and Gitea Actions workflows. ``` setup.sh # Server provisioning (idempotent) -scripts/deploy.sh # Receives deploy commands via SSH +scripts/deploy.sh # Receives deploy + cert commands via SSH scripts/new-app.sh # Scaffolds + deploys a new app (single command) docker/nginx/ # nginx reverse proxy compose.yml # nginx:alpine container diff --git a/CLAUDE.md b/CLAUDE.md index e29ed87..2e53bac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Actions workflows. ## Key files - `setup.sh` -- Server provisioning (idempotent, run as root) -- `scripts/deploy.sh` -- SSH-triggered deploy (validates command, pulls, restarts) +- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance (deploy-* and cert-* commands) - `scripts/new-app.sh` -- Single-command new app (DNS + Gitea repo + cert + deploy + verify) - `docker/nginx/` -- Reverse proxy (nginx:alpine + certbot on host) - `docker//compose.yml` -- Per-app compose files diff --git a/README.md b/README.md index abbc92e..2fd0584 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ docker/ timothykim.net/ # timothykim.net static site compose.yml scripts/ - deploy.sh # Deploy script (validates command, git pull, docker compose up) + deploy.sh # Deploy + cert issuance script (called via SSH) new-app.sh # Scaffolding script to add a new app setup.sh # Server provisioning script (idempotent) .gitea/workflows/ # Per-app deploy workflows triggered by path changes @@ -39,8 +39,9 @@ bash /opt/hantim/setup.sh 4. Fetches the Docker registry token and deploy SSH public key from Bitwarden 5. Logs into the Gitea Docker registry 6. Creates the `deploy` user with restricted SSH access and sudo -7. Issues SSL certificates via certbot for all configured domains -8. Starts all services (nginx first, then all apps) +7. Opens firewall ports (HTTP/HTTPS) +8. Issues SSL certificates via certbot for all configured domains +9. Starts all services (nginx first, then all apps) **Prerequisites**: DNS for all configured domains must point to the server before running setup. @@ -59,7 +60,8 @@ the SSL cert (zero downtime), commits and pushes, triggers the first build, and verifies the site is live. **Prerequisites:** domain nameservers pointed to Vultr, `bw`/`jq`/`dig` -installed, SSH access to the server. See USECASES.md for full details. +installed. Uses the deploy SSH key from Bitwarden -- no personal server +access needed. See USECASES.md for full details. After the site is live, clone the app repo and customize it: diff --git a/USECASES.md b/USECASES.md index e8014b8..de8e0dd 100644 --- a/USECASES.md +++ b/USECASES.md @@ -15,6 +15,7 @@ bash /opt/hantim/setup.sh - Fetches secrets from Bitwarden (git-crypt key, registry token, deploy SSH key) - Unlocks git-crypt, logs into Docker registry - Creates deploy user with restricted SSH + sudo +- Opens firewall ports (HTTP/HTTPS) - Issues SSL certs for all domains in `docker/nginx/conf.d/` - Starts nginx and all app containers @@ -43,14 +44,14 @@ From your dev machine (single command): ``` **Prerequisites:** -- Bitwarden vault must contain `hantim-new-app-script` and `hantim-vultr-api-key` +- Bitwarden vault must contain `hantim-new-app-script`, `hantim-vultr-api-key`, + and `hantim-server-deploy` - Domain nameservers must be pointed to Vultr (configured on directnic.com) -- SSH access to the server as your user with passwordless sudo - Dependencies: `bw`, `jq`, `dig` **What happens (fully automated):** 1. Resolves server IP from `hantim.net` -2. Fetches Gitea API token and Vultr API key from Bitwarden +2. Fetches Gitea API token, Vultr API key, and deploy SSH key from Bitwarden 3. Creates DNS zone on Vultr (if needed) and A records for bare + www 4. Waits for DNS to propagate 5. Creates Gitea repo `hantim/hcsuzuki.net` from `static-site-template` @@ -58,8 +59,7 @@ From your dev machine (single command): - `docker/hcsuzuki.net/compose.yml` (container name: `hcsuzuki_net`) - `.gitea/workflows/deploy-hcsuzuki.net.yml` - `docker/nginx/conf.d/hcsuzuki.net.conf` (HTTP->HTTPS, bare->www, proxy) -7. SSHes to server: writes temporary HTTP-only nginx config, issues SSL cert - via webroot (zero downtime) +7. SSHes to server as deploy user, issues SSL cert via webroot (zero downtime) 8. Commits and pushes hantim-server (triggers deploy) 9. Triggers initial build of the app repo via Gitea API 10. Polls `https://www.hcsuzuki.net` until it responds (up to 3 minutes) diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 397081e..25496b1 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -3,6 +3,34 @@ set -euo pipefail CMD="${SSH_ORIGINAL_COMMAND:-${1:-}}" +if [[ "$CMD" =~ ^cert-[a-zA-Z0-9._-]+$ ]]; then + APP="${CMD#cert-}" + + # Write temporary HTTP-only config so nginx can serve ACME challenges + cat > "/opt/hantim/docker/nginx/conf.d/$APP.conf" <" echo " Example: ./scripts/new-app.sh hcsuzuki.net" echo "" - echo "Requires environment variables:" - echo " DEPLOY_USER - SSH username (default: \$USER)" + echo "Requires: bw, jq, dig" exit 1 fi @@ -23,8 +22,6 @@ if [ -z "$SERVER_IP" ]; then exit 1 fi -SSH_TARGET="${DEPLOY_USER:-$USER}@$SERVER_IP" - if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then echo "Error: app name must be alphanumeric (hyphens, dots, underscores allowed)." exit 1 @@ -72,6 +69,12 @@ fi bw sync --session "${BW_SESSION:-}" GITEA_TOKEN=$(bw get notes hantim-new-app-script --session "${BW_SESSION:-}") VULTR_API_KEY=$(bw get notes hantim-vultr-api-key --session "${BW_SESSION:-}") +DEPLOY_KEY=$(bw get item hantim-server-deploy --session "${BW_SESSION:-}" | jq -r '.sshKey.privateKey') + +DEPLOY_KEY_FILE=$(mktemp) +echo "$DEPLOY_KEY" > "$DEPLOY_KEY_FILE" +chmod 600 "$DEPLOY_KEY_FILE" +trap "rm -f $DEPLOY_KEY_FILE" EXIT # --- Set up DNS records on Vultr --- @@ -259,25 +262,8 @@ NGINX # --- Issue SSL cert (zero downtime) --- -echo "==> Writing temporary HTTP-only config on server..." -TEMP_CONF="server { - listen 80; - listen [::]:80; - server_name $APP www.$APP; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 444; - } -}" -echo "$TEMP_CONF" | ssh "$SSH_TARGET" "sudo tee /opt/hantim/docker/nginx/conf.d/$APP.conf > /dev/null" -ssh "$SSH_TARGET" "sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload" - -echo "==> Issuing SSL certificate..." -ssh "$SSH_TARGET" "sudo certbot certonly --webroot -w /opt/hantim/docker/nginx/certbot/www --non-interactive --agree-tos --register-unsafely-without-email --cert-name $APP -d $APP -d www.$APP" +echo "==> Issuing SSL certificate via deploy user..." +ssh -o StrictHostKeyChecking=accept-new -i "$DEPLOY_KEY_FILE" deploy@"$SERVER_IP" "cert-$APP" # --- Commit and push --- @@ -310,4 +296,4 @@ echo "WARNING: https://www.$APP is not responding after 3 minutes." echo " Check that:" echo " - DNS for $APP and www.$APP points to $SERVER_IP" echo " - The build completed: $GITEA_URL/$GITEA_ORG/$APP/actions" -echo " - The container is running: ssh $SSH_TARGET sudo docker ps" +echo " - The container is running on the server: docker ps"