#!/usr/bin/env bash # Create the non-root, forced-command deploy user for the dashboard host # (audit 2026-10-09, finding M3). Run ONCE on the droplet as root; idempotent, # safe to re-run. # # sudo bash deploy/setup-deploy-user.sh "" # # Before this, GitHub Actions deployed as root with an unrestricted shell # (deploy.yml: SERVER_USER=root, git reset --hard, sudo systemctl restart). # A leaked DEPLOY_KEY therefore meant root on the host that serves the wallet # and /downloads. After this script: # # * SSH as `deploy` can ONLY run /usr/local/bin/deploy-dashboard.sh # (authorized_keys forced command; no pty, no port/agent/X11 forwarding). # * deploy-dashboard.sh fetches origin, hard-resets /opt/moonbite-dashboard to # the exact SHA CI validated, and restarts the service. # * sudo for `deploy` is limited to `systemctl restart moonbite-dashboard` # (and `status`, read-only). # * The old Actions key is removed from /root/.ssh/authorized_keys (matched by # its comment "moonbite-dashboard-deploy"). # # Then set the repo's deploy workflow to SERVER_USER=deploy (already done in # .github/workflows/deploy.yml) and keep the same DEPLOY_KEY secret -- the key # pair does not change, only which account accepts it and what it may run. set -euo pipefail [ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; } DEPLOY_USER="deploy" PROJECT_PATH="/opt/moonbite-dashboard" SERVICE="moonbite-dashboard" KEY_COMMENT="moonbite-dashboard-deploy" FORCED_CMD="/usr/local/bin/deploy-dashboard.sh" PUBKEY="${1:-}" if [ -z "$PUBKEY" ]; then # Fall back to lifting the existing Actions key out of root's authorized_keys. PUBKEY="$(grep -F "$KEY_COMMENT" /root/.ssh/authorized_keys 2>/dev/null | head -1 || true)" [ -n "$PUBKEY" ] || { echo "usage: $0 '' (no key with comment '$KEY_COMMENT' found in /root/.ssh/authorized_keys)" >&2; exit 1; } fi # Strip any options already on the key; we add our own. PUBKEY="$(printf '%s\n' "$PUBKEY" | sed -E 's/^.*(ssh-(ed25519|rsa)|ecdsa-sha2-nistp[0-9]+|sk-[a-z0-9-]+@openssh\.com) /\1 /')" case "$PUBKEY" in ssh-*|ecdsa-*|sk-*) ;; *) echo "not an ssh public key: $PUBKEY" >&2; exit 1;; esac # 1. user if ! id -u "$DEPLOY_USER" >/dev/null 2>&1; then useradd --system --create-home --home-dir "/home/$DEPLOY_USER" --shell /bin/bash "$DEPLOY_USER" echo "created user $DEPLOY_USER" fi # 2. project checkout owned by deploy (the service itself may run as another # user; that is fine, it only needs read access to the tree) [ -d "$PROJECT_PATH/.git" ] || { echo "$PROJECT_PATH is not a git checkout" >&2; exit 1; } chown -R "$DEPLOY_USER:$DEPLOY_USER" "$PROJECT_PATH" sudo -u "$DEPLOY_USER" git -C "$PROJECT_PATH" config --global --add safe.directory "$PROJECT_PATH" # 3. forced command cat > "$FORCED_CMD" <&2 exit 64 fi cd "\$PROJECT_PATH" echo "Fetching origin..." git fetch --quiet origin echo "Checking out \$SHA..." git reset --hard --quiet "\$SHA" echo "Restarting \$SERVICE..." sudo -n /bin/systemctl restart "\$SERVICE" sudo -n /bin/systemctl status "\$SERVICE" --no-pager || true echo "Deployment complete: \$(git rev-parse --short HEAD)" EOF chmod 755 "$FORCED_CMD" chown root:root "$FORCED_CMD" # 4. authorized_keys with forced command and no shell escape install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "/home/$DEPLOY_USER/.ssh" AK="/home/$DEPLOY_USER/.ssh/authorized_keys" LINE="command=\"$FORCED_CMD\",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding,restrict $PUBKEY $KEY_COMMENT" touch "$AK" grep -vF "$KEY_COMMENT" "$AK" > "$AK.tmp" || true printf '%s\n' "$LINE" >> "$AK.tmp" mv "$AK.tmp" "$AK" chmod 600 "$AK"; chown "$DEPLOY_USER:$DEPLOY_USER" "$AK" # 5. sudoers: only the restart (and read-only status) cat > /etc/sudoers.d/moonbite-deploy < /root/.ssh/authorized_keys.tmp || true mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys echo "removed '$KEY_COMMENT' key from /root/.ssh/authorized_keys" fi echo echo "Done. Test from the runner's key:" echo " ssh -i deploy_key deploy@ \$(git rev-parse HEAD) # deploys that SHA" echo " ssh -i deploy_key deploy@ bash # must be refused" echo "Then ensure .github/workflows/deploy.yml uses SERVER_USER: deploy."