<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Key-Rotation on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/key-rotation/</link><description>Recent content in Key-Rotation on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Tue, 23 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/key-rotation/index.xml" rel="self" type="application/rss+xml"/><item><title>Sign your own binaries with go-tool-base, part 7: rotation and break-glass</title><link>https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-7/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-7/</guid><description>&lt;img src="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-7/cover-sign-your-own-binaries-with-go-tool-base-part-7.png" alt="Featured image of post Sign your own binaries with go-tool-base, part 7: rotation and break-glass" /&gt;&lt;p&gt;Most signing guides stop the moment the first release goes out the door,
which is a shame, because the question that keeps you up at night comes later:
what do you do when the key has to change? Keys get rotated on a schedule,
keys get compromised, and one day you&amp;rsquo;ll want to move off RSA onto something
newer. This last part of the &lt;a class="link" href="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base/" &gt;signing series&lt;/a&gt;
covers the bit everyone skips, swapping a signing key out from under a fleet
of installed tools without locking a single one of them out, and the
break-glass key for the day the primary is gone.&lt;/p&gt;
&lt;p&gt;By &lt;a class="link" href="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-6/" &gt;Part 6&lt;/a&gt;
you&amp;rsquo;ve a pipeline signing every tagged release through a KMS key, a public key
published over WKD, and that same key baked into the binary as its trust
anchor. It all works. The catch nobody mentions is that you&amp;rsquo;ve now got a key
embedded in software sitting on other people&amp;rsquo;s machines, and that key is the
one thing in the whole chain you can&amp;rsquo;t quietly change. So let&amp;rsquo;s plan for
changing it properly.&lt;/p&gt;
&lt;h2 id="why-theres-no-auto-rotate-button"&gt;Why there&amp;rsquo;s no auto-rotate button
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;ve used KMS for encryption, you&amp;rsquo;ll know it can rotate keys for you on a
yearly tick. Asymmetric &lt;code&gt;SIGN_VERIFY&lt;/code&gt; keys don&amp;rsquo;t get that, and the
&lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-signing-kms" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-signing-kms&lt;/code&gt;&lt;/a&gt;
module sets &lt;code&gt;enable_key_rotation = false&lt;/code&gt; on purpose. That&amp;rsquo;s not an oversight
to work around; it falls straight out of how the key is built. The private
half of a signing key never leaves the HSM and has no export path at all, so
there&amp;rsquo;s no mechanism by which AWS could hand your verifiers a rotated public
half and keep the old one verifiable. KMS auto-rotation works for symmetric
keys precisely because you never see the key material; the instant you need a
&lt;em&gt;public&lt;/em&gt; key your clients pin against, rotation stops being something a cloud
provider can do behind your back.&lt;/p&gt;
&lt;p&gt;So rotation here is a runbook you maintain, not a checkbox you tick. That
sounds like the worse deal until you weigh it: you&amp;rsquo;ve got a key with no
exfiltration path, in exchange for rotating it by hand on the rare occasions
you must. For a release-signing key that&amp;rsquo;s exactly the right trade. You don&amp;rsquo;t
want this key rotating itself; you want to be standing right there when it
happens.&lt;/p&gt;
&lt;h2 id="rotate-by-minting-a-new-key-never-by-changing-the-old-one"&gt;Rotate by minting a new key, never by changing the old one
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the rule that makes everything else fall into place. You do not edit
the existing key. The module&amp;rsquo;s &lt;code&gt;key_spec&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; are immutable by design,
and that immutability is a feature: the v1 key is a fixed point that stays
verifiable while you stand up its replacement next to it.&lt;/p&gt;
&lt;p&gt;So you mint a &lt;em&gt;second&lt;/em&gt; key. A second module instance, named &lt;code&gt;acme-release-signing-v2&lt;/code&gt;,
with its own alias &lt;code&gt;alias/acme-release-signing-v2&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;signing_kms_v2&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gitlab.com/phpboyscout/signing-kms/aws&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.1.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;acme-release-signing-v2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; oidc_provider_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;gitlab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;arn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; ci_subject_filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;project_path:acme/acme-cli:ref_type:tag:ref:v*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; key_administrator_arns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; automation_role_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;automation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;arn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Apply that and the v1 key carries on signing, undisturbed. Now mint its public
half exactly the way you minted v1 back in Part 4, only the alias changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb keys mint &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --backend aws-kms &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --kms-region eu-west-2 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --key-id alias/acme-release-signing-v2 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --name &lt;span class="s2"&gt;&amp;#34;Acme Releases&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --email release@acme.dev &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --created &lt;span class="s2"&gt;&amp;#34;2026-06-08T00:00:00Z&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --output signing-key-v2.asc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ve now got two real signing keys, both live, neither one a threat to the
other. The whole rotation is the careful business of moving traffic from one to
the other while every installed client keeps verifying.&lt;/p&gt;
&lt;h2 id="the-dual-publish-dual-sign-window"&gt;The dual-publish, dual-sign window
&lt;/h2&gt;&lt;p&gt;This is the part that does the work, and it leans on one fact about how the
verifier treats your keys. The trust anchor isn&amp;rsquo;t a single key, it&amp;rsquo;s a &lt;em&gt;set&lt;/em&gt;:
v1, v2, and the rotation authority all sit in &lt;code&gt;internal/trustkeys/keys/&lt;/code&gt; and
all get embedded together. A release verifies if &lt;strong&gt;any&lt;/strong&gt; key in that set
validates its signature. That&amp;rsquo;s what makes a handover window possible, because
during it a client might hold v1, or v2, or both, and verify happily whichever
it has.&lt;/p&gt;
&lt;p&gt;So you run an overlap. Three moves, in order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First, publish both public keys over WKD.&lt;/strong&gt; Same email bucket, same command
you already know from Part 4, just with v2 added to the file list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb keys wkd &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --domain acme.dev &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --email release@acme.dev &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --output ./wkd-staging &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; signing-key-v1.asc signing-key-v2.asc rotation-authority.asc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both signing keys share &lt;code&gt;release@acme.dev&lt;/code&gt; in their UID, so they land in one
&lt;code&gt;hu/&lt;/code&gt; bucket together, and the rotation authority rides along as it always has.
Deploy that staging tree the way you deployed it before. Now the WKD endpoint
serves the new trust anchor, so a client that fetches keys on its next update
picks v2 up automatically.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Dual-publishing v1, v2 and the rotation authority into one WKD bucket" class="gallery-image" data-flex-basis="400px" data-flex-grow="166" height="720" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-7/demo-rotate-wkd.gif" width="1200"&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second, embed v2 alongside v1 and ship a release.&lt;/strong&gt; Drop &lt;code&gt;signing-key-v2.asc&lt;/code&gt;
into &lt;code&gt;internal/trustkeys/keys/&lt;/code&gt; next to v1, cut a release, and that build now
ships knowing about both keys. Installed clients pick the new trust anchor up
as they update through the window. This is the slow bit, and it should be: you
want the new key spread far and wide &lt;em&gt;before&lt;/em&gt; you stop signing with the old
one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third, sign with v2, wait, then retire v1.&lt;/strong&gt; Point the pipeline&amp;rsquo;s
&lt;code&gt;GTB_SIGNING_KEY_ID&lt;/code&gt; at &lt;code&gt;alias/acme-release-signing-v2&lt;/code&gt; so new releases are signed
by the new key. A client that&amp;rsquo;s already updated verifies against the embedded
v2; one that&amp;rsquo;s lagging still has v1 in its set and, because you&amp;rsquo;re still
publishing v1 over WKD, can still fetch and trust it. Nobody&amp;rsquo;s locked out.&lt;/p&gt;
&lt;p&gt;Leave that overlap running long enough that you&amp;rsquo;re confident the slow movers
have updated, weeks, not hours, depending on how often your users actually run
the thing. Only &lt;em&gt;then&lt;/em&gt; do you retire v1: drop its module instance, pull
&lt;code&gt;signing-key-v1.asc&lt;/code&gt; from the embed directory and the WKD file set, and ship a
release that knows only about v2. The rotation is done, and at no point did an
installed tool see a signature it couldn&amp;rsquo;t check.&lt;/p&gt;
&lt;h2 id="the-break-glass-key-for-when-theres-no-handover"&gt;The break-glass key, for when there&amp;rsquo;s no handover
&lt;/h2&gt;&lt;p&gt;Everything above assumes the old key can hand over to the new one: it&amp;rsquo;s still
there, still able to sign, and you&amp;rsquo;re rotating on your own terms. The
nightmare is the other case. The KMS key is gone, access revoked, or you&amp;rsquo;ve
reason to think it&amp;rsquo;s compromised and you daren&amp;rsquo;t sign anything with it again.
There&amp;rsquo;s no handover, because the thing that would do the handing is exactly
what you&amp;rsquo;ve lost.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what the rotation-authority key is for, and it&amp;rsquo;s why
&lt;a class="link" href="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base-part-4/" &gt;Part 4&lt;/a&gt;
had you mint it the moment everything else was calm. It&amp;rsquo;s an offline ed25519
key, generated once on a trusted machine, and the go-tool-base how-to,
&lt;a class="link" href="https://gtb.phpboyscout.uk/how-to/generate-rotation-key/" target="_blank" rel="noopener"
 &gt;generate the rotation-authority key&lt;/a&gt;,
walks the storage in detail. The short version: the private half never lives
on a networked box. You print a paper backup with &lt;code&gt;paperkey&lt;/code&gt;, write it to an
encrypted USB stick, and the two go in a safe; the local copy gets shredded.
The how-to even has you type the paper backup &lt;em&gt;back in&lt;/em&gt; once before you walk
away, because discovering your printer ate a stripe of pixels is a problem you
want now, not eighteen months from now when the building&amp;rsquo;s on fire.&lt;/p&gt;
&lt;p&gt;The public half, though, has been in your trust set the whole time, embedded
in the binary and served over WKD right alongside the signing keys. That&amp;rsquo;s the
trick. Because every installed client already trusts the rotation authority, it
can vouch for a brand-new signing key &lt;em&gt;outside&lt;/em&gt; the normal sign-with-the-old-key
path. You bring the private half out of the safe, use it to authorise the new
key, ship that, and installed tools adopt the replacement on their next update,
all without the dead primary key ever having to sign a thing.&lt;/p&gt;
&lt;p&gt;It is, deliberately, a key you hope never to touch. But a break-glass key you
forgot to cut is just a pane of glass.&lt;/p&gt;
&lt;h2 id="the-whole-chain-end-to-end"&gt;The whole chain, end to end
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the series. Step back and the shape is one clean line: a signing key is
&lt;em&gt;born&lt;/em&gt; inside KMS and never leaves it (Part 2); its public half is &lt;em&gt;minted&lt;/em&gt; out
of the HSM without the private bytes ever surfacing (Part 4); that public key is
&lt;em&gt;published&lt;/em&gt; off-platform over WKD, somewhere your release host can&amp;rsquo;t quietly
rewrite (Part 4), and &lt;em&gt;embedded&lt;/em&gt; into the binary as a required trust anchor
(Part 5); every tagged release is &lt;em&gt;signed&lt;/em&gt; through the key over short-lived OIDC
credentials with no stored secrets (Parts 3 and 6); a stranger&amp;rsquo;s copy of your
tool &lt;em&gt;verifies&lt;/em&gt; its own updates against that anchor before trusting a byte
(&lt;a class="link" href="https://blog-570662.gitlab.io/a-signature-the-platform-cant-forge/" &gt;the cross-check&lt;/a&gt;);
and when the day comes, the whole thing is &lt;em&gt;rotatable&lt;/em&gt; without locking anyone
out. A key that can&amp;rsquo;t be stolen, can&amp;rsquo;t be forged, and can still be replaced.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve followed all seven parts, you&amp;rsquo;ve built that. If you&amp;rsquo;ve dipped in for
one piece, the &lt;a class="link" href="https://blog-570662.gitlab.io/sign-your-own-binaries-with-go-tool-base/" &gt;pillar&lt;/a&gt;
ties the lot together and points at the deep-dives behind the &lt;em&gt;why&lt;/em&gt;. Either
way, your users are getting updates they can actually trust, which was the
whole point. Go and leave your supply chain better than you found it.&lt;/p&gt;</description></item></channel></rss>