Skip to content

fix(lib): pin TLS on vendor installer fetches; drop apt-key - #47

Merged
WomB0ComB0 merged 3 commits into
mainfrom
fix/harden-vendor-installer-fetches
Aug 11, 2026
Merged

fix(lib): pin TLS on vendor installer fetches; drop apt-key#47
WomB0ComB0 merged 3 commits into
mainfrom
fix/harden-vendor-installer-fetches

Conversation

@WomB0ComB0

Copy link
Copy Markdown
Member

Second half of the audit, kept separate from #46 so that one stays reviewable as a security fix on its own.

install.sh pins --proto '=https' --tlsv1.2 for its own vendor installer. scripts/lib did not — so the two halves of the repo disagreed about how much to trust the network.

Two were more than inconsistency

nix.sh had no -f. Without it, curl emits the response body on a 4xx/5xx and that HTML is piped straight into sh. With -L there was also an unconstrained redirect chain.

bun.sh used irm bun.sh/install.ps1 — no scheme. A bare host lets the request begin as plaintext HTTP, for a response piped directly into iex.

apt-key

docker.sh used apt-key add, which installs into the global trusted set: the Docker key could then sign packages for every repository on the system. Deprecated for exactly that reason.

Replaced with a keyring file scoped by signed-by, plus an explicit sources.list.d entry instead of add-apt-repository:

deb [arch=<arch> signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu <codename> stable

Also

misc.sh's release lookup gets -f and the same flags. It reads JSON rather than feeding a shell, but without -f an error body is piped into the parser and a GitHub error message can be mistaken for a tag name.

Result

All six executed curl invocations under scripts/lib now carry --proto '=https' --proto-redir '=https' --tlsv1.2; the PowerShell mirrors match. Remaining unpinned hits in the tree are comments and user-facing usage text, not executed code.

What this does not do: these are still unverified third-party installers. The vendors publish no digests to check against, so this constrains the transport, not the content. Unlike our own artifacts — which are pinned and hash-verified end to end — a compromise of bun.sh or get.docker.com would still land. Worth being clear about, since the rest of this work sets a higher bar.

shellcheck clean, all libs parse, stamp --check in sync, 48/48 worker tests.

🤖 Generated with Claude Code

WomB0ComB0 and others added 2 commits August 11, 2026 03:25
Found in a security audit of the finished work. install.sh verifies
install-hooks.sh before executing it — and install-hooks.sh then downloaded six
files from resq-software/crates@master, a mutable branch, with no verification,
chmod +x'd them, and pointed core.hooksPath at them. Those run on every commit
and push. The chain of custody terminated one link too early.

It was the default path, not an edge case: main() runs post_clone_setup before
install_resq_cli, so a fresh machine never has resq on PATH at that point and
always took the raw-fetch branch. And RESQ_CRATES_REF is an environment
variable, so the source of those executables was caller-controlled.

Now pinned to a commit with per-hook SHA-256 digests, failing closed. Hooks are
staged in a temp dir and published only after all six verify, so a mismatch on
the last file cannot leave the first five installed and active. Overriding the
ref still works but requires RESQ_ALLOW_UNVERIFIED=1 — the same contract
install-resq.sh already used. The fetch also pins --proto '=https' --tlsv1.2,
which it did not before.

install-hooks.ps1 had the identical flaw and gets the identical treatment.

required.yml now re-checks the pins against the live endpoint. Pins rot
silently: if the templates change, the installers would refuse good hooks and
every onboard would fail with a checksum mismatch. Better a red check than a
broken install.

Verified: correct pin installs six executable hooks and sets core.hooksPath;
an overridden ref without opt-in installs nothing; a tampered digest installs
nothing. bin/stamp.sh propagated the new installer digests into install.{sh,ps1}
on its own, which is what that machinery is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second half of the security audit. install.sh pins --proto '=https' --tlsv1.2
for its own vendor installer; scripts/lib did not, so the two halves of the
repo disagreed about how much to trust the network.

Two of these were more than inconsistency:

  nix.sh had no -f. Without it curl emits the response body on a 4xx/5xx and
  that HTML is piped straight into sh. -L meant a redirect chain, unconstrained.

  bun.sh invoked `irm bun.sh/install.ps1` with no scheme, so the request could
  begin as plaintext http — for a response piped directly into iex.

docker.sh used `apt-key add`, which installs into the global trusted set: the
Docker key could then sign packages for every repository on the system. It is
deprecated for that reason. Replaced with a keyring file scoped by signed-by,
and an explicit sources.list.d entry rather than add-apt-repository.

misc.sh's release lookup gets -f and the same proto flags. It reads JSON rather
than feeding a shell, but without -f an error body is piped into the parser and
a GitHub error message can be mistaken for a tag name.

All six executed curl invocations under scripts/lib now carry
--proto '=https' --proto-redir '=https' --tlsv1.2. The PowerShell mirrors match.
These remain unverified third-party installers — the vendors publish no digests
to check against — so this constrains the transport, not the content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@WomB0ComB0
WomB0ComB0 requested a review from a team as a code owner August 11, 2026 07:38
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
get-resq-software 4a2b696 Aug 11 2026, 08:22 AM

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@WomB0ComB0, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01e7d261-0cea-4c17-8e27-45f6388eb7dd

📥 Commits

Reviewing files that changed from the base of the PR and between bdfe97f and 4a2b696.

📒 Files selected for processing (7)
  • scripts/lib/bun.ps1
  • scripts/lib/bun.sh
  • scripts/lib/docker.ps1
  • scripts/lib/docker.sh
  • scripts/lib/misc.sh
  • scripts/lib/nix.ps1
  • scripts/lib/nix.sh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@WomB0ComB0
WomB0ComB0 merged commit 9083744 into main Aug 11, 2026
22 checks passed
@WomB0ComB0
WomB0ComB0 deleted the fix/harden-vendor-installer-fetches branch August 11, 2026 08:25
@WomB0ComB0 WomB0ComB0 mentioned this pull request Aug 11, 2026
WomB0ComB0 added a commit that referenced this pull request Aug 11, 2026
Ships the security fixes from #46 and #47, which are on main but not yet
reaching anyone: get.resq.software serves what PINS says, and PINS still points
at v0.4.0. Until this releases and the pin bump merges, every
`curl -fsSL https://get.resq.software | sh` still installs the hook installer
that fetched executable git hooks from a mutable branch without verifying them.

  main install-hooks.sh  3b3e67197ffe...  pinned + verified
  served (v0.4.0)        24bd874dd27f...  unverified

Merging this is the release: release.yml validates VERSION, creates the tag,
publishes the Release and SHA256SUMS, and opens the pin-bump PR. Merging that
is what changes the served bytes.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
WomB0ComB0 added a commit that referenced this pull request Aug 11, 2026
…po (#50)

* fix(lib): verify the Nix installer; install Docker from its signed repo

Items 1 and 2 from the vendor-installer discussion. #47 pinned the transport
for these; this verifies the content, where the vendors make that possible.

Nix. install.determinate.systems/nix is a redirect to a versioned URL —
/nix/tag/v3.21.9 — which returns identical bytes across fetches. So it can be
pinned and digest-checked, and now is: downloaded to a temp dir, SHA-256
compared, executed only on match, in all four places that installed Nix
(install.sh, install.ps1, and both libs). scripts/lib/nix.sh additionally
switches from nixos.org's rolling installer to the same Determinate one; two
different installers for one tool in a single repo was its own problem.

Docker. scripts/lib/docker.ps1 still piped get.docker.com to sh while
docker.sh installed from Docker's apt repository, which apt verifies by GPG
signature on every package. Only one of them was verified. get.docker.com is
rolling, unsigned and unversioned — there is nothing to pin it against, so the
fix is to stop using it rather than to constrain how it is fetched. Non-apt
Linux under PowerShell now reports what to run instead of silently taking the
weaker path.

required.yml re-checks the Nix pin against the live URL and asserts all four
copies agree, for the same reason it checks the hook digests: a stale pin would
otherwise fail every user's install rather than one CI run.

What this does not do: Determinate publish no digest of their own, so this pins
what we execute, not what they wrote. It closes "silently different tomorrow",
not "already compromised when we pinned it". Bun is unchanged — its install
script is not covered by the signed SHASUMS256.txt, and fixing that properly
means replacing the script with a download-and-verify against the signed
manifest, which is a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: handle installer failures; correct Debian vs Ubuntu docker repo

Review findings on #50, all valid.

Silent failures. Verifying a download says the script is authentic, not that it
worked. install.sh set NIX_JUST_INSTALLED after the installer ran regardless of
its exit status, so a failed install printed "Nix was just installed" and a PATH
hint for a Nix that was not there. The two PowerShell paths were worse: a native
command's failure does not throw, so Install-Nix and Install-Docker fell through
to their success returns and setup.ps1 carried on. All three now check.

Debian. Both docker paths hardcoded linux/ubuntu with `lsb_release -cs`. On
Debian that yields e.g. "bookworm", which the ubuntu suite does not publish, so
apt errors or silently finds no candidate. Now reads ID/ID_LIKE from
/etc/os-release and picks the matching suite, per Docker's own documentation.
Derivatives resolve through ID_LIKE with UBUNTU_CODENAME/DEBIAN_CODENAME, which
matters for Kali: its VERSION_CODENAME is "kali-rolling", not a Docker suite.
Exercised across ubuntu/debian/mint/pop/kali/raspbian, with fedora correctly
rejected to a manual install.

The Nix pin check covered install.sh and both libs but not install.ps1, which
declares its own $NixInstallUrl and $NixInstallerSha256. It could have drifted
while the job passed — meaning Windows users would get a different installer
than everyone else, which is the failure the check exists to prevent. Now all
four are compared.

Two findings not taken, with reasons in the review thread: the RETURN traps do
fire (verified on normal return, early failure return, and under set -e), and
the missing Apache headers are a pre-existing repo-wide gap — all 20 files in
scripts/lib lack one, so fixing two of them makes the directory less consistent,
not more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(lib): confine the installer cleanup trap to a subshell

The review was right and I tested the wrong thing first. I checked whether a
RETURN trap set inside a function fires; the finding was that it does not stay
inside that function.

Verified, all three claims:

  without set -T          scoped to the function
  with set -T             PERSISTS - fired on an unrelated later return
  nested functions        silently replaced the caller's own RETURN trap
  under set -Tu           "unbound variable", status 1, once the local is gone

These are library functions meant to be sourced, so a caller enabling functrace
is a realistic way to break them, and the failure would surface far from here.

Download, verify and run now happen in a subshell with an EXIT trap, which
cannot escape and takes the temp directory with it. Exit status still
propagates, so a failed install is still reported. Removed the vestigial
`if true` the restructure left behind, along with its unreachable else.

scripts/lib/bun.sh has the same pattern and is fixed on the #51 branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): use an explicit if instead of A && B || C

SC2015, which actionlint fails the build on. The behaviour was correct here —
C should run when either test fails — but the pattern also runs C when A
succeeds and B fails, which is the trap shellcheck is pointing at. An explicit
if says what is meant without relying on the reader knowing that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@WomB0ComB0 WomB0ComB0 mentioned this pull request Aug 11, 2026
WomB0ComB0 added a commit that referenced this pull request Aug 11, 2026
Ships the security audit: #46, #47, #50 and #51. All four are on main and none
are reaching anyone — the endpoint serves what PINS says, and PINS still points
at v0.4.1, which predates every one of them.

  install.sh on main   f38612c071bcd6d4...
  served (v0.4.1)      639b4167471082c6...

What this releases:

  #46  install-hooks.{sh,ps1} fetched six executables from a mutable branch
       with no verification, chmod +x'd them, and pointed core.hooksPath at
       them — on every fresh onboard, since resq is installed after hooks run.
       Now pinned and digest-checked, failing closed.
  #47  TLS pinning across scripts/lib; nix.sh piped 4xx bodies into sh;
       bun.sh fetched schemelessly; docker.sh used apt-key, which grants the
       Docker key authority over every repository on the system.
  #50  Nix installer pinned to a versioned URL and verified; Docker installed
       from its GPG-signed apt repo rather than get.docker.com; installer
       failures no longer report success; Debian and Ubuntu suites resolved
       correctly.
  #51  Bun's installer pinned to a tagged copy and verified.

Merging this is the release. Merging the pin-bump PR that follows is what
changes the served bytes.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants