What an artifact signature proves, and what it does not
Overview
Signing an artifact is straightforward, but the claims made about it are typically broader than what the practice actually supports. What exists works, though it is narrower than the phrase "supply chain security" implies. The honest picture is more useful than a diagram with green ticks.
1. Sign the JAR and the Image
We must sign both the JAR and the container image, because only one of them gets deployed. The JAR is what the build produces; the image is what actually runs. Signing only the JAR provides proof about an input to a later step but leaves a gap-the thing that reaches the cluster may differ from the signed artifact. Anyone who can influence how the image is assembled sits in that gap, and the signature does not cover it.
| What you sign | What that lets you check | What is still open |
|---|---|---|
| The JAR only | The library came from a trusted build | Nothing about the image that wraps it |
| The image only | What runs was built by a trusted pipeline | Nothing about the code inside it |
| Both | Each handoff is checkable on its own | Whether the build itself did what it claimed |
Why the Second Signature Was Added
The first version signed the JAR at publish time and stopped there. On paper the chain looked finished-signed commits, signed artifact, verification before deploy-but the external question was: what have you proved about the image? The image was built in a separate job from a base image we pulled and a JAR we trusted, and no signature covered the result. Adding a signature to the image closes that step-not the whole chain, but it moves the boundary somewhere defensible.
2. Only the Build Can Sign, and That Is the Whole Control
Access to the signing key is restricted to the runner's role. No developer has the key, no local build can produce a signed artifact, and there is no break-glass path that places the key on a laptop. This restriction is the entire security property. Everything else-the verification step, the metadata, the pipeline configuration-is plumbing that only makes sense because the key is unreachable from anywhere except the build.
The moment a person can sign, a signature stops meaning "this came from the pipeline" and starts meaning "this came from the pipeline, or from someone who was in a hurry." The interesting question is therefore not how we sign, but who can schedule a job on a runner that holds the signing role. That is a CI access question wearing cryptographic clothes, and it is where the real risk lives.
3. Verify at the Point of Use, and Fail the Deploy
Verification happens in the deploy job, before the artifact is pulled down and installed. If the signature does not verify, the deploy fails. There is no warning mode and no override flag.
Two principles matter more than where the check sits:
- Fail closed: A step that logs a warning and continues is documentation, not a control.
- Name the problem explicitly: Messages should say "no signature found" versus "signed with an unknown key," distinguishing two different problems with different fixes. Nobody should need a security runbook to tell them apart.
The most common verification failure is not an attack-it is an artifact that predates the control, or one promoted between environments by a path that did not carry the signature with it. With a generic message both outcomes are identical: a red deploy, a developer who did nothing wrong, and a support request. Both become obvious if the message names the artifact, states whether a signature was absent or unrecognized, and links to the page explaining what to do next. The check is easy; the message decides whether people trust it.
4. A Signature Is Not Provenance
This is the core insight. Alongside each artifact we record the commit and the signature. That is it. There is no signed attestation describing the build, no record of which runner produced it, and no statement of what the pipeline configuration was at the time. The chain supports this claim: this artifact was signed by our build, and it says it came from this commit. It does not support the claim people tend to hear-that this artifact was built from this commit, by this pipeline, with nothing else added. The commit reference is metadata the build wrote about itself. A signature over self-reported metadata proves the signer, not the statement.
That is not a reason to skip signing. It raises the cost of tampering after the build, which is the most likely place for it to happen. It is a reason to be careful about the sentence you put in a compliance document. "Traceable" is true. "Provable" is not, yet.
5. Key Rotation Is the Failure You Will Actually Hit
A lost key is far more common than an attacker scenario. When a developer loses their key-a replaced laptop, usually-they generate a new one, and everything signed with the old key is suddenly signed by something the verifier does not recognize. The signature remains valid; the key it points to is no longer registered.
The fix is to keep old public keys rather than replacing them. The parameter store holds retired keys alongside the current one, so historic signatures continue to verify while new work is signed with the new key. Revocation becomes a deliberate act for a key you believe is compromised, rather than a side effect of someone getting a new machine. This is the single most useful thing to tell anyone rolling out signing of any kind: design for rotation before you design for enforcement. Enforcement is a day of work; rotation is what determines whether the control survives contact with normal life.
In practice, the pattern repeats: someone gets a new laptop, generates a fresh key, and their next few pieces of work fail verification. From their side the control appears broken-they did nothing unusual and the pipeline refuses their change. Keeping retired keys in the store fixes it and costs nothing except deciding once that key history is state you keep rather than state you overwrite. What remains for a human is deciding whether a key was lost or taken. Those require opposite responses-keep the old key so history verifies, or revoke it so it stops-and no automation tells you which one you are looking at.
6. The Part That Is Still Unsolved: Knowing Which Commits Are New
Even with perfect signing, there is a deeper gap: deciding which commits a pipeline run is supposed to check. When a branch is pushed for the first time, the "before" reference the hook receives is all zeros-there is no previous state to compare against. You cannot tell from that alone where the new work starts. If the branch was taken from another branch rather than from the mainline, walking back from the tip picks up commits that belong to whoever wrote them, not to the person pushing now.
The consequences run both ways:
- Too wide: the check covers commits from before the branch point, blocking a developer by history they did not write.
- Too narrow: the range misses commits, and unverified work reaches a protected branch while the pipeline reports green.
Comparing against the mainline merge base seems obvious but is not reliable either-a branch taken from a branch has a merge base that is not where the developer thinks it is, and a rebase moves it after the fact.
How We Live With It Today
We compare against the mainline and accept that a branch taken from another branch is a case we get wrong. When it happens, the developer sees a failure they cannot explain, asks in the channel, and we look at the range by hand. Calling that a solution would be generous-it is a known rough edge with a manual escape, which is the honest description of most controls at this stage.
The real fix looks like recording the verified range rather than recomputing it: storing what has already been checked, so the pipeline asks what is new since the last verification instead of inferring the boundary from the push. I have not built it yet.
Takeaways
- Sign every artifact that gets handed on, not only the first one. Both the JAR and the image are handoffs.
- Keep the signing key reachable only from the build. That restriction is the security property; everything else is plumbing.
- Fail the deploy on a bad signature, and make the message distinguish "no signature" from "unknown key."
- Be precise about what you have. A signature over self-reported metadata gives you traceability, not attested provenance.
- Plan key rotation first. Keep retired public keys so history still verifies, and treat revocation as a deliberate decision.
- Work out how you will identify new commits before you enforce anything on them. It is harder than the signing, and until that is solved the enforcement around it is only as trustworthy as the range it was handed.
Comments
No comments yet. Start the discussion.