TL;DR: A self-updating CLI verifies its downloads against a checksum, but a checksum hosted next to the binary only catches accidents. Anyone who compromises the release platform replaces the binary and the checksum in one move. Closing that gap needs a cryptographic signature whose signing key lives somewhere the release platform cannot reach. That key needs a home: a separate security domain, with its own account and its own controls. This is the first post in a series about building that home.

The door the last post left open

A while back I wrote about verifying your own downloads: go-tool-base’s self-update command now checks the SHA-256 of every binary it downloads against the release’s published checksums.txt before installing it.

That post was honest about its own ceiling. A checksum file hosted next to the binary it describes shares a trust root with that binary. Both come from the same release, on the same platform. Corruption, truncation, a CDN serving a stale object: a same-origin checksum catches all of those, because they are accidents and the checksum was not part of the accident. What it cannot catch is an attacker who has compromised the release platform itself. Someone who can replace the binary can replace checksums.txt in the same breath, and the tool will cheerfully verify the malicious download against the malicious checksum and call it good.

The post named the fix and then deferred it: a signature whose trust root sits somewhere the release platform cannot reach. “That’s the next phase of this work.” This series is that phase.

What a signature actually needs

It is worth being precise about why a signature helps where a checksum does not, because it is easy to wave the word “signature” around and assume it settles things.

A signature closes the gap only under two conditions. The verifying key, the public half, must reach the user by a path the release platform does not control. And the signing key, the private half, must live somewhere the release platform cannot reach.

The second condition is the one people skip. If the signing key sits in the same CI system that builds the release, you have gained almost nothing. An attacker who owns the CI owns the key, and a key they own will sign whatever they hand it. The signature verifies perfectly and means nothing. A signature is only worth the distance between the signing key and the thing being signed. Put them in the same place and the distance is zero.

So the signing key has to live in a different security domain from the release pipeline. Not a different folder. A different account, with a different blast radius, that the release platform has no standing access to.

“Just sign the binary” is not a small feature

That reframes a line item that sounds tiny. “Sign the release binary” unpacks into a list:

  • there must be a private signing key;
  • it must live outside the release platform, in its own security domain;
  • it must be access-controlled, audited, and protected from exfiltration;
  • only the release pipeline may ask it to sign, and only by proving a short-lived, federated identity, never by holding a copy of the key.

That is not a feature you bolt onto a CLI. That is infrastructure.

The shape of it: a cloud account, with the key held in a managed key service so the private key material never exists as a file on a disk that anyone, me included, can copy. The release pipeline authenticates to that account as itself, briefly, and asks the key service to produce a signature. The key never moves.

But an account you are going to trust with a signing key is itself something you have to get right first. An account with a weak baseline, no audit trail, and long-lived credentials lying around is not a safe home for the most security-sensitive key in the whole system. Before the key can move in, the house has to be built and the locks have to work.

What this series builds

So this turned into a longer project than “add a signature,” and the series follows it in order.

It starts with bootstrapping a fresh AWS account: the deliberately minimal first tofu apply, and the remote state backend that has a genuine chicken-and-egg problem. Then the credential question, which is the heart of it: how a CI pipeline deploys to AWS with no stored access key at all. Then hardening the account, so it is genuinely safe to hold something valuable. Then the discipline of deploying changes to it: plans reviewed before they are applied. Then the shared tooling that makes all of it repeatable.

Every one of those pieces exists for the same reason. The signing key needs somewhere to live, and somewhere safe is not a default you are handed. It is a thing you build, deliberately, before you have anything worth protecting in it.

The series ends where the verifying-downloads post pointed: a signing service whose key the release platform cannot touch, so a self-updating tool can finally verify that the binary it is about to become is genuinely the one I published.

The upshot

go-tool-base’s self-update verifies downloads against a checksum, and a same-origin checksum stops accidents but not a compromise of the release platform. The fix is a signature, and a signature is only worth the distance between its signing key and the release pipeline.

Holding that key safely means a private key that never leaves a managed key service, in a separate cloud account, reached only by a short-lived federated identity. That is infrastructure, and a safe account is something you build before you trust it with anything. The rest of this series builds it, piece by piece, up to the signing service itself.