From 9865d095311a514c6a7dbf9662de31919391bb1b Mon Sep 17 00:00:00 2001 From: heznpc Date: Fri, 24 Apr 2026 04:00:02 +0900 Subject: [PATCH] feat: ssh key rotation guide + env preflight check - docs: new SSH Key Rotation section in VPS_DEPLOY.md (quarterly cadence, zero-downtime two-key window, recovery via provider console) - cd.yml: preflight SSH step that fails with a pointer to docs if ~/.env.app is missing or unreadable, warns on world-readable perms (octal-aware bitmask) - docs: section 5 now states ~/.env.app must pre-exist and documents the 600 permission expectation --- .github/workflows/cd.yml | 24 ++++++++++++++++++++++++ docs/VPS_DEPLOY.md | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6073270..ef76405 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -80,6 +80,30 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Preflight — verify ~/.env.app on VPS + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USER }} + key: ${{ secrets.VPS_SSH_KEY }} + script: | + set -e + if [ ! -f "$HOME/.env.app" ]; then + echo "::error::~/.env.app not found on VPS. Create it before deploying — see docs/VPS_DEPLOY.md section 5 (Set Up Production Environment)." + exit 1 + fi + if [ ! -r "$HOME/.env.app" ]; then + echo "::error::~/.env.app exists but is not readable by $USER. Fix with: chmod 600 ~/.env.app" + exit 1 + fi + PERMS="$(stat -c '%a' "$HOME/.env.app" 2>/dev/null || stat -f '%Lp' "$HOME/.env.app")" + # Warn (don't fail) if world-readable — users may have deliberate setups. + # $PERMS is a digit string like "600" or "644"; force octal interpretation. + if [ "$((8#$PERMS & 4))" -ne 0 ]; then + echo "::warning::~/.env.app is world-readable (mode $PERMS). Recommend: chmod 600 ~/.env.app" + fi + echo "Preflight OK (mode $PERMS)" + - name: Copy deploy script to VPS uses: appleboy/scp-action@v1 with: diff --git a/docs/VPS_DEPLOY.md b/docs/VPS_DEPLOY.md index 2476ac1..1447d76 100644 --- a/docs/VPS_DEPLOY.md +++ b/docs/VPS_DEPLOY.md @@ -43,7 +43,7 @@ Go to your repo → **Settings** → **Secrets and variables** → **Actions** ## 5. Set Up Production Environment -On your VPS, create the environment file: +On your VPS, create the environment file at `~/.env.app` — **this file must exist before the first deploy**. The CD workflow runs a preflight check and will fail with a clear message if it is missing. ```bash # On your VPS @@ -51,8 +51,11 @@ cat > ~/.env.app << 'EOF' PORT=3000 # Add your production environment variables EOF +chmod 600 ~/.env.app ``` +The preflight also verifies `~/.env.app` is not world-readable. `chmod 600` is recommended; wider permissions trigger a warning but do not fail the deploy. + ## 6. Deploy **Option A: Manual trigger** @@ -161,6 +164,40 @@ docker compose up -d --wait > **Tip:** GHCR keeps the last 10 versions by default (configured in `cd.yml`). Make sure the version you need hasn't been pruned. +## SSH Key Rotation + +Rotate `VPS_SSH_KEY` on a quarterly cadence, or immediately if you suspect compromise (leaked laptop, ex-contributor access, etc.). The rotation is zero-downtime as long as you keep both keys valid during the switch. + +1. **Generate a new ed25519 keypair** on your local machine (passphrase recommended for the at-rest copy): + + ```bash + ssh-keygen -t ed25519 -f ~/.ssh/deploy_key_new -C "deploy@$(date +%Y-%m)" + ``` + + See [OpenSSH docs](https://www.openssh.com/manual.html) for the underlying options. + +2. **Append the new public key to the VPS** — do *not* remove the old one yet: + + ```bash + ssh-copy-id -i ~/.ssh/deploy_key_new.pub deploy@YOUR_VPS_IP + ``` + +3. **Update the GitHub Secret** `VPS_SSH_KEY` with the contents of the new *private* key (`~/.ssh/deploy_key_new`). Repo → Settings → Secrets and variables → Actions → `VPS_SSH_KEY` → Update. + +4. **Verify CD still works.** Push a trivial commit (or re-run the last Deploy workflow) and confirm it reaches the VPS. If it fails, the old key is still authorized — roll back the secret and investigate. + +5. **Remove the old public key** from `~/.ssh/authorized_keys` on the VPS once the new key is confirmed working: + + ```bash + ssh deploy@YOUR_VPS_IP + # Edit authorized_keys and delete the old line + nano ~/.ssh/authorized_keys + ``` + +6. **Delete the old private key locally** (`rm ~/.ssh/deploy_key`) and document the rotation date. + +> **Locked out?** Use your VPS provider's web console (DigitalOcean, Hetzner, etc. all offer one) to log in, re-add a working public key, and restart. Never store the only copy of a recovery key in GitHub alone. + ## Advanced: Multi-Container Setup The default deployment creates a single-service `docker-compose.yml` on VPS at `~/app/`.