Public Access
Site: published by CI after a push to main and after a release (#79)
The Site workflow's last step, and the release workflow after publishing, ask the web server over SSH to rebuild the site. The key CI holds is tied on the server to one forced command (restrict,command=...), so CI sends no command and a leaked key can only refresh the site. The server, the user, the key and the server's host key are Gitea secrets; with none of them set the step does nothing. scripts/site_refresh.sh is what both workflows run; scripts/site_deploy_keygen.sh makes the key and prints where each half goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Creates the key CI uses to ask the web server for a site refresh, once (issue #79,
|
||||
# docs/milestones/W1.md), and says where each half goes. The private key stays in
|
||||
# ~/.config/roro9stack/ until it is pasted into the Gitea secret; it is never committed and this
|
||||
# script doesn't print it.
|
||||
#
|
||||
# scripts/site_deploy_keygen.sh [/full/path/to/rororefresh.sh] [the runner's address]
|
||||
set -euo pipefail
|
||||
KEY="${RORO_SITE_DEPLOY_KEY:-$HOME/.config/roro9stack/site-deploy-key}"
|
||||
COMMAND="${1:-/full/path/to/rororefresh.sh}"
|
||||
FROM="${2:-}"
|
||||
|
||||
if [ -e "$KEY" ]; then
|
||||
echo "A site deploy key already exists at $KEY; not overwriting it." >&2
|
||||
else
|
||||
mkdir -p "$(dirname "$KEY")"
|
||||
( umask 077; ssh-keygen -q -t ed25519 -N "" -C roro9stack-ci-site-refresh -f "$KEY" )
|
||||
fi
|
||||
|
||||
options="restrict,command=\"$COMMAND\""
|
||||
[ -z "$FROM" ] || options="from=\"$FROM\",$options"
|
||||
|
||||
cat <<TEXT
|
||||
|
||||
1. On the web server, as the user that runs the refresh, add this one line to ~/.ssh/authorized_keys:
|
||||
|
||||
$options $(cat "$KEY.pub")
|
||||
|
||||
restrict: no terminal, no forwarding of any kind. command=: whatever the client asks for, this
|
||||
runs instead.$([ -n "$FROM" ] || printf '\n Give the runner'"'"'s address as the second argument to add from="...": the key then works from there only.')
|
||||
|
||||
2. In Gitea, the repository's Settings > Actions > Secrets:
|
||||
|
||||
SITE_DEPLOY_KEY the whole of $KEY (the private key, with its BEGIN and END lines)
|
||||
SITE_DEPLOY_HOST the server's address as the runner reaches it, or address:port
|
||||
SITE_DEPLOY_USER that user's name
|
||||
SITE_DEPLOY_KNOWN_HOSTS the server's host key, one line, from a machine you trust the network of:
|
||||
ssh-keyscan -t ed25519 <address> (or: -p <port> <address>)
|
||||
and compare it with the server's own:
|
||||
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub (on the server)
|
||||
ssh-keyscan -t ed25519 <address> | ssh-keygen -lf - (here)
|
||||
|
||||
3. Try it, from here, with the same four values in the environment:
|
||||
|
||||
SITE_DEPLOY_KEY="\$(cat $KEY)" SITE_DEPLOY_HOST=... SITE_DEPLOY_USER=... \\
|
||||
SITE_DEPLOY_KNOWN_HOSTS="\$(ssh-keyscan -t ed25519 ... 2>/dev/null)" scripts/site_refresh.sh
|
||||
|
||||
(with from= set, this works from the runner's address only.) Then, to see that the key can do
|
||||
nothing else: ssh -i $KEY <user>@<address> id must run the refresh, not \`id\`.
|
||||
|
||||
Once the secret is in Gitea, the copy at $KEY can be deleted.
|
||||
TEXT
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Asks the web server to rebuild the site (issue #79, docs/milestones/W1.md). Run by CI after a push
|
||||
# to main that changed the site, and after a release is published (the home page and Downloads
|
||||
# name the latest release when they are built).
|
||||
#
|
||||
# It only connects: the server's authorized_keys line forces the one command this key may run, so
|
||||
# nothing sent from here chooses what happens there. From the environment (Gitea secrets):
|
||||
# SITE_DEPLOY_KEY the private key (scripts/site_deploy_keygen.sh makes it)
|
||||
# SITE_DEPLOY_HOST the server, or server:port
|
||||
# SITE_DEPLOY_USER the user there
|
||||
# SITE_DEPLOY_KNOWN_HOSTS the server's host key, as a known_hosts line: nothing else is trusted
|
||||
# With none of them set it does nothing (a fork, or before the key is installed); with only some, it fails.
|
||||
set -euo pipefail
|
||||
|
||||
set_count=0
|
||||
for v in SITE_DEPLOY_KEY SITE_DEPLOY_HOST SITE_DEPLOY_USER SITE_DEPLOY_KNOWN_HOSTS; do
|
||||
[ -z "${!v:-}" ] || set_count=$((set_count + 1))
|
||||
done
|
||||
if [ "$set_count" = 0 ]; then
|
||||
echo "site refresh: no SITE_DEPLOY_* secrets here, nothing done"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$set_count" != 4 ]; then
|
||||
echo "site refresh: SITE_DEPLOY_KEY, _HOST, _USER and _KNOWN_HOSTS are needed, and only $set_count of them are set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ssh >/dev/null; then
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends openssh-client >/dev/null
|
||||
fi
|
||||
|
||||
host="$SITE_DEPLOY_HOST" port=22
|
||||
case "$host" in
|
||||
*:*) port="${host##*:}" host="${host%:*}" ;;
|
||||
esac
|
||||
|
||||
# The key and the host key exist as files only while this runs, in a container that goes with the job.
|
||||
umask 077
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
printf '%s\n' "$SITE_DEPLOY_KEY" > "$tmp/key"
|
||||
printf '%s\n' "$SITE_DEPLOY_KNOWN_HOSTS" > "$tmp/known_hosts"
|
||||
|
||||
# -F none: no configuration but this line. -T and no command: the server's forced command runs.
|
||||
ssh -F none -T -p "$port" -i "$tmp/key" \
|
||||
-o IdentitiesOnly=yes -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=yes -o UserKnownHostsFile="$tmp/known_hosts" -o GlobalKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=20 -o ServerAliveInterval=15 -o ServerAliveCountMax=8 \
|
||||
"$SITE_DEPLOY_USER@$host"
|
||||
echo "site refresh: done"
|
||||
Reference in New Issue
Block a user