Files
pezkuwi-telegram-miniapp/ops/host/miniapp-site-receive
T
Satoshi Qazi Muhammed f6d53f51b1 ci: deploy over pinned, single-purpose keys; root installs the gate
The deploy reached VPS3 as root with one key, handed to appleboy actions
referenced by tag, and on every run it reinstalled the edge-function
ownership gate and its registry as root. That gate guards every project's
functions in the shared volume, so any change merged here could rewrite the
rule that protects pwap's functions.

Now CI connects as miniapp-deploy with two keys, each pinned on the host
(authorized_keys restrict,command=) to a script in ops/host:

- MINIAPP_SITE_DEPLOY_KEY -> miniapp-site-receive: the site tar on stdin,
  into /var/www/telegram.pezkiwi.app, nothing else.
- MINIAPP_FUNCTIONS_DEPLOY_KEY -> miniapp-functions-ssh: `gate-sha`, or
  `deploy` with functions.tgz on stdin, run through sudo for exactly those
  arguments.

Root installs the gate and registry from ops/; the deploy checks the host's
copies against them and refuses to deploy functions while they differ.

The asset cleanup that ran after each deploy (/opt/cleanup-miniapp.sh, only
on the host) protected lazy chunks by mtime and so removed live ones when a
build was shipped more than two hours after it was made; it took seven pages
down for about two minutes on 2026-10-09 while this was being tested, and
they were restored from the same tar. The receive script now keeps every
file of the new build and removes other assets only after four hours. Its
tests cover that (red with the keep rule removed) and the refusals; CI runs
them with shellcheck, and apply-repo-settings.sh requires the job.

VPS3's host key is pinned. The commented-out VPS1 deploy is gone; that host
no longer exists. Every action is pinned to a commit SHA.
2026-10-09 06:09:22 +04:00

51 lines
2.0 KiB
Bash
Executable File

#!/bin/bash
# Forced command for the miniapp site deploy key. Installed root-owned at
# /usr/local/sbin/miniapp-site-receive; authorized_keys pins the key to it:
#
# restrict,command="/usr/local/sbin/miniapp-site-receive" ssh-ed25519 ...
#
# CI runs `ssh miniapp-deploy@<host> deploy < site.tar`. The key can do
# nothing else: no shell, no other command, no forwarding, one web root.
set -euo pipefail
TARGET=/var/www/telegram.pezkiwi.app
if [ "${SSH_ORIGINAL_COMMAND:-}" != "deploy" ]; then
echo "miniapp-site-receive: the only command is 'deploy', with the site tar on stdin" >&2
exit 2
fi
STAGE=$(mktemp -d)
trap 'rm -rf "$STAGE"' EXIT
# GNU tar drops leading slashes and refuses members containing '..'.
tar -xf - -C "$STAGE" --no-same-owner --no-same-permissions
if [ ! -f "$STAGE/index.html" ] || [ ! -d "$STAGE/assets" ]; then
echo "miniapp-site-receive: not a site build (index.html and assets/ are required)" >&2
exit 1
fi
# -rt copies regular files and directories only: a symlink in the upload is
# skipped. --perms with --chmod sets the modes, so mktemp's 0700 never reaches
# a directory nginx reads. Assets first, index.html last.
rsync -rt --perms --chmod=D755,F644 --exclude=/index.html "$STAGE/" "$TARGET/"
install -m 644 "$STAGE/index.html" "$TARGET/index.html"
# Earlier deploys' assets go once nothing can still ask for them: a file of
# this build is never removed, whatever its timestamp; a file that is not in
# this build stays for four hours, for tabs still open on the previous build.
# (The cleanup this replaces protected lazy chunks by their mtime and so
# removed live ones when a build was shipped more than two hours after it was
# made, which took seven pages down on 2026-10-09.)
removed=0
while IFS= read -r -d '' f; do
name=${f#"$TARGET/"}
[ -e "$STAGE/$name" ] && continue
if [ -z "$(find "$f" -mmin -240)" ]; then
rm -f "$f"
removed=$((removed + 1))
fi
done < <(find "$TARGET/assets" -type f -print0)
echo "miniapp-site-receive: deployed; removed $removed asset(s) of earlier builds"