add cert command to deploy.sh, use deploy user for cert issuance

- deploy.sh: add cert-<domain> command for zero-downtime SSL issuance
- new-app.sh: SSH as deploy user with key from Bitwarden instead of personal user
- remove DEPLOY_USER/DEPLOY_HOST env var requirements
- update all docs to reflect new flow
This commit is contained in:
2026-03-16 02:05:18 -04:00
parent b51fe596c6
commit 9a950524eb
6 changed files with 51 additions and 35 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ configs, deploy scripts, and Gitea Actions workflows.
``` ```
setup.sh # Server provisioning (idempotent) 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) scripts/new-app.sh # Scaffolds + deploys a new app (single command)
docker/nginx/ # nginx reverse proxy docker/nginx/ # nginx reverse proxy
compose.yml # nginx:alpine container compose.yml # nginx:alpine container
+1 -1
View File
@@ -10,7 +10,7 @@ Actions workflows.
## Key files ## Key files
- `setup.sh` -- Server provisioning (idempotent, run as root) - `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) - `scripts/new-app.sh` -- Single-command new app (DNS + Gitea repo + cert + deploy + verify)
- `docker/nginx/` -- Reverse proxy (nginx:alpine + certbot on host) - `docker/nginx/` -- Reverse proxy (nginx:alpine + certbot on host)
- `docker/<domain>/compose.yml` -- Per-app compose files - `docker/<domain>/compose.yml` -- Per-app compose files
+6 -4
View File
@@ -15,7 +15,7 @@ docker/
timothykim.net/ # timothykim.net static site timothykim.net/ # timothykim.net static site
compose.yml compose.yml
scripts/ 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 new-app.sh # Scaffolding script to add a new app
setup.sh # Server provisioning script (idempotent) setup.sh # Server provisioning script (idempotent)
.gitea/workflows/ # Per-app deploy workflows triggered by path changes .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 4. Fetches the Docker registry token and deploy SSH public key from Bitwarden
5. Logs into the Gitea Docker registry 5. Logs into the Gitea Docker registry
6. Creates the `deploy` user with restricted SSH access and sudo 6. Creates the `deploy` user with restricted SSH access and sudo
7. Issues SSL certificates via certbot for all configured domains 7. Opens firewall ports (HTTP/HTTPS)
8. Starts all services (nginx first, then all apps) 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 **Prerequisites**: DNS for all configured domains must point to the server
before running setup. 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. and verifies the site is live.
**Prerequisites:** domain nameservers pointed to Vultr, `bw`/`jq`/`dig` **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: After the site is live, clone the app repo and customize it:
+5 -5
View File
@@ -15,6 +15,7 @@ bash /opt/hantim/setup.sh
- Fetches secrets from Bitwarden (git-crypt key, registry token, deploy SSH key) - Fetches secrets from Bitwarden (git-crypt key, registry token, deploy SSH key)
- Unlocks git-crypt, logs into Docker registry - Unlocks git-crypt, logs into Docker registry
- Creates deploy user with restricted SSH + sudo - Creates deploy user with restricted SSH + sudo
- Opens firewall ports (HTTP/HTTPS)
- Issues SSL certs for all domains in `docker/nginx/conf.d/` - Issues SSL certs for all domains in `docker/nginx/conf.d/`
- Starts nginx and all app containers - Starts nginx and all app containers
@@ -43,14 +44,14 @@ From your dev machine (single command):
``` ```
**Prerequisites:** **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) - 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` - Dependencies: `bw`, `jq`, `dig`
**What happens (fully automated):** **What happens (fully automated):**
1. Resolves server IP from `hantim.net` 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 3. Creates DNS zone on Vultr (if needed) and A records for bare + www
4. Waits for DNS to propagate 4. Waits for DNS to propagate
5. Creates Gitea repo `hantim/hcsuzuki.net` from `static-site-template` 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`) - `docker/hcsuzuki.net/compose.yml` (container name: `hcsuzuki_net`)
- `.gitea/workflows/deploy-hcsuzuki.net.yml` - `.gitea/workflows/deploy-hcsuzuki.net.yml`
- `docker/nginx/conf.d/hcsuzuki.net.conf` (HTTP->HTTPS, bare->www, proxy) - `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 7. SSHes to server as deploy user, issues SSL cert via webroot (zero downtime)
via webroot (zero downtime)
8. Commits and pushes hantim-server (triggers deploy) 8. Commits and pushes hantim-server (triggers deploy)
9. Triggers initial build of the app repo via Gitea API 9. Triggers initial build of the app repo via Gitea API
10. Polls `https://www.hcsuzuki.net` until it responds (up to 3 minutes) 10. Polls `https://www.hcsuzuki.net` until it responds (up to 3 minutes)
+28
View File
@@ -3,6 +3,34 @@ set -euo pipefail
CMD="${SSH_ORIGINAL_COMMAND:-${1:-}}" 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" <<NGINXCONF
server {
listen 80;
listen [::]:80;
server_name $APP www.$APP;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 444;
}
}
NGINXCONF
docker exec nginx nginx -t && docker exec nginx nginx -s reload
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 "Certificate issued for $APP"
exit 0
fi
if ! [[ "$CMD" =~ ^deploy-[a-zA-Z0-9._-]+$ ]]; then if ! [[ "$CMD" =~ ^deploy-[a-zA-Z0-9._-]+$ ]]; then
echo "Unknown command: $CMD" echo "Unknown command: $CMD"
exit 1 exit 1
+10 -24
View File
@@ -9,8 +9,7 @@ if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-app.sh <domain>" echo "Usage: ./scripts/new-app.sh <domain>"
echo " Example: ./scripts/new-app.sh hcsuzuki.net" echo " Example: ./scripts/new-app.sh hcsuzuki.net"
echo "" echo ""
echo "Requires environment variables:" echo "Requires: bw, jq, dig"
echo " DEPLOY_USER - SSH username (default: \$USER)"
exit 1 exit 1
fi fi
@@ -23,8 +22,6 @@ if [ -z "$SERVER_IP" ]; then
exit 1 exit 1
fi fi
SSH_TARGET="${DEPLOY_USER:-$USER}@$SERVER_IP"
if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
echo "Error: app name must be alphanumeric (hyphens, dots, underscores allowed)." echo "Error: app name must be alphanumeric (hyphens, dots, underscores allowed)."
exit 1 exit 1
@@ -72,6 +69,12 @@ fi
bw sync --session "${BW_SESSION:-}" bw sync --session "${BW_SESSION:-}"
GITEA_TOKEN=$(bw get notes hantim-new-app-script --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:-}") 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 --- # --- Set up DNS records on Vultr ---
@@ -259,25 +262,8 @@ NGINX
# --- Issue SSL cert (zero downtime) --- # --- Issue SSL cert (zero downtime) ---
echo "==> Writing temporary HTTP-only config on server..." echo "==> Issuing SSL certificate via deploy user..."
TEMP_CONF="server { ssh -o StrictHostKeyChecking=accept-new -i "$DEPLOY_KEY_FILE" deploy@"$SERVER_IP" "cert-$APP"
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"
# --- Commit and push --- # --- Commit and push ---
@@ -310,4 +296,4 @@ echo "WARNING: https://www.$APP is not responding after 3 minutes."
echo " Check that:" echo " Check that:"
echo " - DNS for $APP and www.$APP points to $SERVER_IP" echo " - DNS for $APP and www.$APP points to $SERVER_IP"
echo " - The build completed: $GITEA_URL/$GITEA_ORG/$APP/actions" 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"