Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ images where `/bin/sh` is dash.
Edit `keys.txt` and deploy. That is the entire workflow — nothing else references
the list.

vi keys.txt # add: ssh-ed25519 AAAA... aswin@NewLaptop
vi keys.txt # add: ssh-ed25519 AAAA... aswin@NewLaptop
sh scripts/fingerprints.sh # regenerate fingerprints.txt
npm run deploy
npm run check # verifies production
npm run check # verifies production

Commit `keys.txt` and `fingerprints.txt` together — `validate.sh` refuses to build
while they disagree.

Or edit `keys.txt` on GitHub and let Workers Builds deploy it — useful precisely
because this endpoint exists to set up *other* machines, and you may not be at the
Expand Down Expand Up @@ -75,6 +79,24 @@ the SSH wire format inside it holds just the algorithm name and the raw public
key, with no field for a comment. Renaming one changes nothing about who can log
in; the fingerprint is identical.

### Pinned fingerprints

`fingerprints.txt` lists the SHA256 of every key in `keys.txt`, and `validate.sh`
refuses to build if the two disagree.

This exists because `ssh-keygen -l` validates *structure*, not authenticity. Flip a
single bit in a key's material and the line still parses, still reports
`256 ... (ED25519)`, and still passes every other check — only the fingerprint
moves. A key corrupted that way would deploy, land in a new machine's
`authorized_keys`, and silently not work, because no private key matches it. You
would believe you had four ways into that box and have three.

The pin also makes key changes reviewable. A reviewer skims past a 68-character
base64 blob; a changed `SHA256:` line is legible. Any change to key material has to
appear in `fingerprints.txt` in the same commit or the build fails.

sh scripts/fingerprints.sh # after any change to keys.txt

### A bad key cannot be deployed

`scripts/validate.sh` runs as wrangler's `build.command`, so it fires on
Expand Down Expand Up @@ -151,7 +173,8 @@ offline — no secrets, no Cloudflare token — so there is nothing to gate it o

**Checks**

- `validate.sh` — the same script the deploy runs, but now *before* merge
- `validate.sh` — the same script the deploy runs, but now *before* merge,
including the pinned-fingerprint comparison
- `check-readme.sh` — the README table must match `keys.txt`
- `tsc --noEmit`
- `wrangler deploy --dry-run` — config and bundling, without deploying
Expand Down
15 changes: 15 additions & 0 deletions fingerprints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Fingerprints of every key in keys.txt. Generated -- do not hand-edit.
# Regenerate with: sh scripts/fingerprints.sh
#
# These exist so a corrupted key cannot deploy. ssh-keygen -l validates
# structure, not authenticity: flip one bit in the key material and it still
# parses, still reports 256-bit ED25519, and still passes -- only the
# fingerprint changes. Pinning them turns 'these look like keys' into 'these
# are exactly the keys we intend'.

SHA256:dxiyBcPf+pkSLO/WNrEYnGEKkXpu67jKaS47Ky8ECFs aswin@AswinPC
SHA256:SvP/8B22PR2Q4rCGcQmvF8yUsSGWBcIKAuCD+P6oYDg aswin@Aswin-Laptop
SHA256:cCz8aDx6zovq4vWWcEUEmRzkdf7LLgNbzM3BKUqpeJ4 aswin@Aswins-MacBook-Air.local
SHA256:J0Xs4eAxSVzI/XhTmESrauM48xEMtFmN78GxEc0CkV4 aswin@Aswin-Macbook-Pro
SHA256:ubg9S7gxhwKqTvmgRxS/SRIvfEo1ZayOa+FqfVl+qJ8 mail@ubuntu
SHA256:gWAoI59eLhaNK6FGvQDobXNmxPZUCkf+HUNh329gZAg aswin@truenas-host
27 changes: 27 additions & 0 deletions scripts/fingerprints.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
# Regenerates fingerprints.txt from keys.txt.
#
# sh scripts/fingerprints.sh
#
# Run this after changing keys.txt and commit both files together. validate.sh
# refuses to build while the two disagree, so the fingerprint of any key that
# changed lands in the same diff as the change -- which is the point. A reviewer
# skims past a 68-character base64 blob; a changed SHA256 line is legible.
set -eu
cd "$(dirname "$0")/.."

{
echo "# Fingerprints of every key in keys.txt. Generated -- do not hand-edit."
echo "# Regenerate with: sh scripts/fingerprints.sh"
echo "#"
echo "# These exist so a corrupted key cannot deploy. ssh-keygen -l validates"
echo "# structure, not authenticity: flip one bit in the key material and it still"
echo "# parses, still reports 256-bit ED25519, and still passes -- only the"
echo "# fingerprint changes. Pinning them turns 'these look like keys' into 'these"
echo "# are exactly the keys we intend'."
echo
ssh-keygen -lf keys.txt | awk '{print $2" "$3}'
} > fingerprints.txt

echo "fingerprints.sh: wrote $(grep -v '^#' fingerprints.txt | grep -c .) fingerprints"
grep -v '^#' fingerprints.txt | grep . | sed 's/^/ /'
29 changes: 29 additions & 0 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ if [ -n "$dupes" ]; then
fail=1
fi

# Pinned fingerprints. ssh-keygen -l above proved every line is STRUCTURALLY a
# key; it cannot prove it is the RIGHT key. Flip one bit in the key material and
# the line still parses, still reports 256-bit ED25519 and still passes — only the
# fingerprint moves. A key corrupted that way would deploy, reach a new machine's
# authorized_keys, and silently not work, because no private key matches it.
#
# Comparing against a checked-in list closes that, and also puts any change to key
# material into the diff in a form a reviewer can actually read.
FP=fingerprints.txt
if [ ! -f "$FP" ]; then
echo "validate: $FP is missing — run: sh scripts/fingerprints.sh"
fail=1
else
want=$(grep -v '^#' "$FP" | grep . | awk '{print $1" "$2}' | sort)
got=$(ssh-keygen -lf "$F" 2>/dev/null | awk '{print $2" "$3}' | sort)
if [ "$want" != "$got" ]; then
echo "validate: keys.txt and $FP disagree."
echo
echo " pinned but not in keys.txt:"
printf '%s\n' "$want" | grep -vxF "$(printf '%s' "$got")" 2>/dev/null | grep . | sed 's/^/ /' || echo " (none)"
echo " in keys.txt but not pinned:"
printf '%s\n' "$got" | grep -vxF "$(printf '%s' "$want")" 2>/dev/null | grep . | sed 's/^/ /' || echo " (none)"
echo
echo " If the key change was intended: sh scripts/fingerprints.sh"
echo " and commit fingerprints.txt alongside keys.txt."
fail=1
fi
fi

if [ "$fail" != 0 ]; then
echo
echo "validate: FAILED — not deploying."
Expand Down