Verify the explore setup script and every download it makes - #37
Merged
Merged
Conversation
setup-explore.sh runs as root, but it installed Rust by piping sh.rustup.rs into sh, Node.js by piping the NodeSource setup script into sudo -E bash, and imported Docker's apt key with no fingerprint check. Each step trusted whatever the server returned over TLS. - Wrap the body in main() and call it on the last line, under set -euo pipefail, so a download that is cut short runs nothing. - Check that the domain is a plain hostname and the email a plain address before any command runs. Both end up in nginx server_name lines and certbot arguments; certbot now takes them as an array. - Install Rust from Ubuntu's rustup package, which apt verifies against the archive key, then `rustup default stable`. Where the archive has no rustup package, fetch rustup-init 1.29.1 for the host architecture from rustup's versioned archive and refuse to run it unless it matches the SHA-256 pinned in the script. - Put ~/.cargo/bin on PATH for login shells, since the distro rustup keeps cargo in /usr/bin but `cargo install` still writes there. - Stop installing Node.js. Only the optional did-hosting-ui source build needs it, and the guide now installs Ubuntu's nodejs at that step. - Stop installing Docker and adding the user to the docker group. Nothing in the explore stream uses Docker, and group membership is equivalent to root. Steps renumber from 10 to 8. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The guide had operators fetch setup-explore.sh from the main branch with no way to check what they were about to run as root, and the repo had no CI. - release.yml: on a v* tag, attach setup-explore.sh and SHA256SUMS to a GitHub Release and record a build provenance attestation for both, so `gh attestation verify` can tie a download to this workflow. Tagged commits must be on main. Attesting (id-token, attestations) and publishing (contents: write, `release` environment) run as separate jobs, and the publish job rebuilds the asset and checks it against the attested digest. - lint.yml: run shellcheck, plus a guard that fails on downloads piped into a shell and on raw fetches from this repository's main branch. - dependabot.yml: keep the action SHA pins current, with a 7-day cooldown on new releases. - RELEASING.md: how to pin the hash in the guide, tag, and verify the published assets. Every action is pinned to a full commit SHA, and no checkout keeps credentials. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Step 3 piped setup-explore.sh from the main branch straight into bash, so a changed branch or a truncated download ran as root unchecked. - sysop/explore/01-server-setup.md, Step 3: download setup-explore.sh and SHA256SUMS from a tagged release, check the script against the SHA-256 pinned on the page for that tag, check SHA256SUMS, verify the build provenance attestation (on a workstation if the server's gh is older than 2.49), then run `sudo bash setup-explore.sh <domain>`. The pin is v1.0.0; RELEASING.md covers updating it. - The step list drops Node.js and Docker. Option B installs Ubuntu's nodejs and npm right before the did-hosting-ui build, with a note that the UI declares Node.js >= 24.3.0 while Ubuntu 26.04 ships 22. - Step 4 puts ~/.cargo/bin on PATH instead of sourcing ~/.cargo/env, which the distro rustup does not create. - Option A says download.firstperson.dev publishes no checksums, and the main-branch binaries are labelled unverified, for throwaway hosts only. - The debconf troubleshooting section no longer downloads from main. - sysop/README.md: the Explore stream no longer lists Node or Docker as installed. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scripts/setup-explore.shruns as root, but the guide told operators to pipe it from the mutablemainbranch intobash. The script then did the same thing again: rustup viash.rustup.rs | sh, Node.js via NodeSource'ssetup_22.x | sudo -E bash -, and Docker's apt key imported with no fingerprint check. Every step trusted whatever a server returned over TLS, and a truncated download could run part of a script.This PR makes the script and its downloads verifiable, publishes the script as an attested GitHub Release, adds CI to stop the pattern from coming back, and rewrites the guide to download, verify, then run.
What the script installs now, and how each download is checked
curl raw.githubusercontent.com/.../main/... | bashv*tagSHA256SUMS, build provenance attestationcurl sh.rustup.rs | shrustuppackage (1.27.1 in 26.04 universe), thenrustup default stablerustup-init1.29.1 fromstatic.rust-lang.org/rustup/archive/1.29.1/<triple>/for x86_64 or aarch64setup_22.x | sudo -E bash -in the base scriptnodejs npmright before thedid-hosting-uibuild, the only step that uses itdockergroupdockergroup membership is root-equivalentOther script changes:
set -euo pipefail, with the body inmain() { ... }called on the last line, so a truncated download runs nothing.DOMAINandEMAILare validated (hostname and simple address regexes, length limits, at most two arguments) before any command runs. certbot arguments are an array, not a word-split string.cargoin/usr/binand creates no~/.cargo/env, so the script puts~/.cargo/binonPATHin~/.profileand~/.bashrc(idempotently) forcargo installoutput. Step 4 of the guide now saysexport PATH="$HOME/.cargo/bin:$PATH".Why Node.js falls back to the distro package
NodeSource's official material (the
nodesource/distributionsREADME and DEV_README, the "Repository Manual Installation" wiki page, and nodesource.com/products/distributions) does not publish a fingerprint fornodesource-repo.gpg.key. The key currently served has primary fingerprint6F71F525282841EEDAF851B42F59B5F99B1BE0B4. That value only appears in user-pasted apt errors on NodeSource issues (#1908, #1920) and in third-party material, not in a statement from NodeSource. Pinning a fingerprint read off the same TLS endpoint would be trust-on-first-use with extra steps, so the guide uses Ubuntu'snodejsinstead.Caveat:
did-hosting-ui/package.jsondeclares"node": ">=24.3.0", and Ubuntu 26.04 ships Node.js 22.22.1. The guide says so and points at the pre-builtdid-hosting-daemonif the UI build fails. See open questions.Release and CI
.github/workflows/release.yml(tag pushv*):attest(contents: read,id-token: write,attestations: write) requires the tagged commit to be onmain, buildsdist/setup-explore.shanddist/SHA256SUMS, and runsactions/attest-build-provenanceover both.publish(contents: write,environment: release) rebuilds the asset, checks it against the attested digest, and runsgh release create --verify-tagwith the hash and thegh attestation verifycommand in the notes.${{ }}expressions insiderun:blocks..github/workflows/lint.yml(contents: read):shellcheck scripts/*.sh .github/scripts/*.sh(preinstalled onubuntu-24.04), and.github/scripts/guard-remote-exec.sh, which fails oncurl/wgetpiped into(sudo [-E]) (ba|z|da)sh, onsh <(curl ...), and onraw.githubusercontent.com/OpenVTC/vti-setup/main/inscripts/,sysop/,developer/,community-manager/andREADME.md..github/dependabot.yml:github-actions, weekly,cooldown: default-days: 7, grouped.gh api repos/<owner>/<repo>/git/ref/tags/<tag>(both are lightweight tags pointing at commits):actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2(as of v4 a wrapper overactions/attest; Dependabot will track it)persist-credentials: false.How the guide pins the hash
The release hash covers only
scripts/setup-explore.sh, not the guide, so the hash is known before the tag exists and can be committed ahead of it. Step 3 ofsysop/explore/01-server-setup.mdnow has:VER=v1.0.0andSHA256=c59f53c74f423327a233bb3178651bf7958449efd88df05df8c2f8d179db1340, which is the script in this PR.setup-explore.shandSHA256SUMSfrom that release.echo "${SHA256} setup-explore.sh" | sha256sum -c -. This is the out-of-band anchor, because the hash comes from the reviewed guide, not from the release.sha256sum -c SHA256SUMS, a consistency check only.gh attestation verify setup-explore.sh --repo OpenVTC/vti-setup --signer-workflow OpenVTC/vti-setup/.github/workflows/release.yml. This needs gh >= 2.49 andgh auth login. Ubuntu 26.04'sghis 2.46, so the guide says to run it on a workstation and compare the hash.sudo bash setup-explore.sh <domain> [email]RELEASING.mddocuments the routine for later releases: pinVERandSHA256in a PR, merge, tag that commit, approve the environment, then verify the assets.Maintainer steps after merge
Before tagging: create the
releaseenvironment with required reviewers (otherwise GitHub creates it unprotected on first use), and add a tag ruleset restrictingv*to maintainers.Confirm the merge commit did not change the script:
sha256sum scripts/setup-explore.shmust printc59f53c74f423327a233bb3178651bf7958449efd88df05df8c2f8d179db1340. If it doesn't, re-pin in a follow-up PR first.Tag the merge commit and push the tag right away, because the guide already points at
v1.0.0:Approve the
releasedeployment.Verify the published assets exactly as Step 3 of the guide does, including
gh attestation verify.If any check fails, delete the release and tag, fix, and release a new version. Never move a published tag.
Follow-up outside this repo: binary checksums
sysop/explore/01-server-setup.mdStep 5 (anddeveloper/02-openvtc-tui.mdforopenvtc) still downloads service binaries fromdownload.firstperson.devinto/usr/local/binwith no integrity check:vta/latest/vta.sha256andvta/latest/SHA256SUMSreturn 404. This PR only labels Option A as not integrity-checked and the/main/channel as unverified, throwaway hosts only. The fix belongs to the binary release pipeline: publishSHA256SUMSper release plus a signature (minisign, or cosign / GitHub attestations). The guide can then addsha256sum -c --ignore-missing SHA256SUMSbefore eachinstall, and the lint guard can require it.Validation
bash -npasses onscripts/setup-explore.shand.github/scripts/guard-remote-exec.sh.shellcheck0.11.0 reports nothing on either file. CI uses the runner's 0.9.0.main(the old one-liners).apt-get,apt-cache,curl,snap,sudo,systemctl,certbot,rustupand others stubbed. 25/25 checks passed:x.com;id,$(id).x.com,example.com -d attacker.com,-d, an embedded newline, a single label, a bad email, no args and three args all exit 1 before any stubbed command runs.Open questions
v1.0.0is proposed. The repo's existing tag isBanyan, and VTI usesVTI-Dogwood.did-hosting-ui: the UI wants >= 24.3.0 but Ubuntu 26.04 has 22. Options:engines)SHASUMS256.txt.ascwith the Node release keys listed in the nodejs/node READMESHA256equalsscripts/setup-explore.shonmain? That would force a release for every script change.