Repository navigation
feat: install script and nodkey update - #95
Merged
Merged
Conversation
This was referenced Oct 17, 2026
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.
What changed
install.sh(new, at the root of the repository). It installs thenewest release, or
--version X.Y.Z, as root on the host itself: thebroker host or an agent machine. It is POSIX sh and runs in dash. Options:
--version X.Y.Z,--bin-dir DIR(default/usr/local/bin),--gh PATH,--reinstall,--help. Exit codes: 0 done or nothing to do, 1 failure,64 wrong usage.
First it makes its environment safe. It removes
TAR_OPTIONS,GZIP,POSIXLY_CORRECT,CURL_HOME,CURL_CA_BUNDLE,SSL_CERT_FILE,SSL_CERT_DIR,SSLKEYLOGFILE,OPENSSL_CONF,OPENSSL_MODULES,OPENSSL_ENGINES,LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,GCONV_PATH,CDPATHandIFS, and setsLC_ALL=C. Asroot it sets
PATH=/usr/sbin:/usr/bin:/sbin:/bin. A comment in the scriptsays why each variable goes. Then:
and writes to: the bin directory, the old
nodkey, the gh of--gh,and
TMPDIR(or/tmp). Each of them, and each directory above it upto
/, must be owned by root and not writable by group or others. Adirectory above them, and
TMPDIRitself, may be writable by othersonly with the sticky bit, as
/tmp. Links in these paths are resolvedonce (
cd -P,pwd -P), and the real path is used from then on. Theold file and the gh must be regular files, not links. When the script
does not run as root, the user who runs it may own these paths too
(this is how the tests run it).
--version, ortag_nameofhttps://api.github.com/repos/code6x/nodkey/releases/latest, read withsed. Only
vand three numbers without leading zeros count.checked, exit 0, unless
--reinstall. An older version than theinstalled one only with
--version. A build from the source (dev,or not
X.Y.Z) is "unknown version" and is replaced.SHA256SUMSandnodkey_<version>.sigstore.json(ci: publish the attestation bundle with each release #87) into a new
mktemp -ddirectory. curl gets-q(no.curlrc),--proto '=https' --proto-redir '=https' --tlsv1.2 --fail, 3 retries,30 s to connect, 600 s for each download, and
--max-filesize: 64 MiBfor the tarballs (nodkey and gh), 1 MiB for the API answer,
SHA256SUMSand the bundle.SHA256SUMS.for each CPU. Or
--gh PATH. Either must be 2.93.0 or newer; the firstline of
gh --versionmust be exactlygh version X.Y.Zup to aspace, so
2.93.0-rc.1or2.102.0+ds1do not count. A comment notesthat the newer advisories GHSA-mm27-mwq9-fr5g, GHSA-wjmr-j3rp-mh2g and
GHSA-4mq3-hpgx-9cx8 are about
--signer-repo,--signer-workflowand--source-ref, which the script does not use.gh attestation verify <tarball> --bundle <bundle> --repo code6x/nodkey --cert-identity https://github.com/code6x/nodkey/.github/workflows/release.yml@refs/heads/main --deny-self-hosted-runners, with no token or host from theenvironment,
HOME,GH_CONFIG_DIRand theXDG_*directories in thetemporary directory, and no update notices, prompts or telemetry.
nodkeyout of the tarball, copies it into the bin directoryunder a temporary name, and checks that its
nodkey versionprintsexactly
nodkey <version>. This stops an attested older build under anewer name: the attestation proves the workflow, not the version (found
in the review of ci: publish the attestation bundle with each release #87). The new nodkey runs only after the attestation
passed.
mv -fover the old file: a rename in one directory.which ends all grants, and each
nodkey ssh-agent). It restartsnothing.
not renamed yet, also after a failure or a signal.
All work is in functions, and
main "$@"is the last line. Every failureends with
nodkey: <reason>; nothing was installed (exit 1).install.go(new): packagenodkeyat the root of the module, whichembeds
install.shasInstallScript.nodkey update [--version X.Y.Z] [--gh PATH] [--reinstall](
cmd/nodkey/update.go): only as root; resolves its own file withos.Executableandfilepath.EvalSymlinksand refuses a file that is notnamed
nodkey; runs/bin/sh -c <embedded script> nodkey-install --bin-dir <dir> [flags]with stdin from/dev/null, and passes stdout,stderr and the exit code through. The script gets a fixed environment:
PATH=/usr/sbin:/usr/bin:/sbin:/bin,LC_ALL=C,HOME=/root, and onlyHTTPS_PROXY,https_proxy,NO_PROXYandno_proxyif set. So withsudo -Eor aftersuwithout-, another user'sTAR_OPTIONS,LD_PRELOADorTMPDIRdo not reach root's programs.CI: the
lintjob downloads ShellCheck v0.11.0, checks its SHA-256 inthe step, and runs
shellcheck --shell=sh install.sh.Docs:
docs/setup.mdsections 1, 2, 13 and 14;README.mdquickstart;
DESIGN.mdsections 4, 15 and 18;THREAT-MODEL.mdT20 andsection 7;
AGENTS.mdlayout. Section 2 and the README now say where theroot shell should come from: on the broker host its console or a trusted
computer, never an agent machine; on an agent machine its console or SSH
as an admin user from a trusted computer, not
sudoin the agent'sterminal (the agent could put its own
sudofirst inPATH, or type intothe terminal). They also state the limit: on an agent machine that the
agent controls, nodkey cannot keep its client safe, but the broker trusts
nothing from the client, so a changed client gets no more than the phone
approves. Section 13 sets
VERSIONonce, runsnodkey update --version "$VERSION"(nosudoon the broker host) andfetches the unit of the same version, with no
# commentafter a command(that breaks in interactive zsh). Section 14 says what to do when the
installer refuses a path (for example an old Debian with
/usr/local/binwritable by
staff; Debian 12, Debian 13 and Ubuntu 24.04 ship it755 root:root, checked in their official images).How the gh pin was checked
On 2026-10-17, with gh 2.96.0, logged in:
gh release view --repo cli/cli --json tagName,publishedAt,isImmutable:v2.102.0, published 2026-09-30, immutable; the newest release.sha256sumofgh_2.102.0_linux_amd64.tar.gzandgh_2.102.0_linux_arm64.tar.gzequals their lines ingh_2.102.0_checksums.txtand the asset digests on GitHub:bb766f710eef8ede859c18578c72c327597cd4c8a85b06001b1f3843c60193867862c86c72f43df3a2d93ddde6f473285b4e2af61b494849846827e513ef6484gh attestation verify <tarball> --repo cli/cli,--owner cli, and with--cert-identity https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk --deny-self-hosted-runners:exit 0 for both. The attestation names the same two SHA-256s.
gh attestation verify --owner cli. It cannot: cli/cli does not attestthat file (
Error: no attestations found). The tarballs were checkedinstead, which is stronger.
attestations, fixed in 2.67.0) and GHSA-8xvp-7hj6-mcj9 (fixed in 2.93.0).
With the real pinned gh, no login and an empty environment, against the
real 0.1.0 tarball and its provenance bundle,
install.sh(with a fakecurl that served these real files):
--version 0.1.0: installed;the version check refused it;
SHA256SUMS: ghrefused it (
verifying with issuer "sigstore.dev").TAR_OPTIONSwith a--checkpoint-action=execcommand was set in allthree runs, and the command never ran.
Tests
No test uses the network. As root, the script runs only the programs of
the system, so the tests of the script skip as root (the fakes in
PATHwould not count, and curl would use the network).
install_linux_test.gorunsinstall.shwith/bin/sh(dash on Debianand Ubuntu) against a fake
curlfirst inPATH(it serves files of thetest, enforces
--max-filesize, and writes down each call), a fakeuname, and a fake gh given with--gh(it writes down its arguments andenvironment). The fake release is built in the test.
dev, a commit, afile that does not run),
--reinstallof the same version,--versionto an older one, arm64,
uname -mamd64, gh 2.93.0 exactly, aTMPDIRwith the sticky bit, a bin directory in a sticky directory, abin directory behind a link, and the downloaded gh (a copy of the script
with the SHA-256 of a fake gh tarball).
--version; a newer version without--version, also with--reinstall. The text says that the installed file was not checked.directory is gone): another system or CPU; no bin directory; a bin
directory that cannot be written, or that others, its group, or all
with the sticky bit can write to, or under a directory that others can
write to; an old file that others can write to, that is a link, a
directory or a pipe; a
TMPDIRthat others can write to, or under sucha directory, or missing; a
--ghthat others can write to, in such adirectory, a link, or missing (for all of these: no download, and no
nodkey or gh ran); no API answer, or one that is too big; bad tags;
missing tarball,
SHA256SUMSor bundle; aSHA256SUMSthat is too big;bad checksum, missing line, a line for a longer name, two lines; failed
verify; a gh that is too old, of a pre-release, with a suffix, of no
version, or whose
--versionfails; an attested older build under anewer name; a new nodkey of another version, with more output, with a
v, or that does not run; a tarball withoutnodkey; a downloaded ghwith another SHA-256; no gh to download. A new nodkey never runs when
the attestation was not checked, and not when it fails.
--help.gh attestation verify, that gh checked the veryfile of the release, and its environment: no token, no host, none of the
unsafe variables,
LC_ALL=C, and HOME, the gh config and the XDGdirectories in the temporary directory.
-qand has the HTTPS, TLS,--fail,retry, time and size flags, with 64 MiB for tarballs and 1 MiB else.
TAR_OPTIONSwith an exec checkpoint action never runs.gone.
uid=0:files of the test are refused ("must be owned by root"),
/tmppassesas a shared directory and fails as one that must be private, and
/usr/bin,/tmpand/pass.clean_envremoves the variables andsets
LC_ALL=C; with a fakeidthat says 0, it sets the fixedPATH.cmd/nodkey/update_test.go: wrong usage (also--ghnot absolute orgiven twice, and
--reinstall=maybe), the refusal as another user, thearguments of the script (behind a link, with
--version,--gh,--reinstall), the fixed environment (withTAR_OPTIONS,LD_PRELOAD,TMPDIR,PATH,HOMEandGH_TOKENset for nodkey; only the proxyvariables pass), that the script is
install.shbyte for byte, the exitcode, the refusal of a file that is not named
nodkeyor cannot be found,and the real run with
/bin/sh($0, arguments, only the givenenvironment, stdin from
/dev/null, stdout, stderr, exit code, a signal).Results:
CI=true go test -race ./...(Go 1.27.1)CI=true go test -race ./...(Go 1.26.0)go vet ./...(both)gofmt -l .go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 run --build-tags e2eshellcheck --shell=sh install.sh(v0.11.0, SHA-256 checked)-count=20 -cpu 1,2,4The end-to-end test was not run: this change does not touch what it
covers.
Left out, and why
docs/releasing.mdis not changed here. ci: publish the attestation bundle with each release #87 (merged in ci: publish the attestation bundle with each release #91) made"Verify a download" three steps; T20 and setup.md refer to them.
no GitHub login) waits for the next release. 0.1.0 has no bundle, so the
installer refuses it with a clear message, and the docs say to install
0.1.0 by hand. 0.1.0 has no
nodkey update, so the first update of a0.1.0 host uses the installer.
functions of the script with
uid=0, and through a hook for the userID in Go.
nodkey updatedoes not passTMPDIR. On a host where/tmpdoes notallow programs to run, the downloaded gh cannot run; the message then
says to use
--gh, or to runinstall.shwithTMPDIRset to adirectory that only root can write, such as
/root.to update them by hand.
The security review (
.claude/agents/security-reviewer.md) found nocritical or high finding. All medium, low and nit findings of the first
review are fixed in this change. The review of those fixes found one
medium point:
OPENSSL_CONF,OPENSSL_MODULESandOPENSSL_ENGINESwere not removed, and an OpenSSL config can make curl load any library.
The script now removes them too, and the test checks them. Its low points
are in #94.
Closes #88