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)
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
+1 -1
View File
@@ -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/<domain>/compose.yml` -- Per-app compose files
+6 -4
View File
@@ -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:
+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)
- 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)
+28
View File
@@ -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" <<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
echo "Unknown command: $CMD"
exit 1
+10 -24
View File
@@ -9,8 +9,7 @@ if [ -z "${1:-}" ]; then
echo "Usage: ./scripts/new-app.sh <domain>"
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"