<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>CI/lock blog</title>
        <link>https://cilock.dev/blog</link>
        <description>CI/lock Blog</description>
        <lastBuildDate>Tue, 09 Jun 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[The signed record we didn't have in March]]></title>
            <link>https://cilock.dev/blog/signed-record-we-didnt-have-in-march</link>
            <guid>https://cilock.dev/blog/signed-record-we-didnt-have-in-march</guid>
            <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Two March attacks had the same shape — CI ran code it shouldn't have, with credentials it shouldn't have had, and nobody could prove what executed. That's the gap CI/lock closes.]]></description>
            <content:encoded><![CDATA[<p>I've spent a decade on this problem. I helped build Witness, we donated it to the CNCF and in-toto, and I helped write the reference architecture people point at when they talk about securing the software supply chain. The good news is the rest of the industry is converging on the premise: provenance and attestation are where software trust is heading. The harder part is getting there. So when I tell you the tooling still wasn't good enough, I'm including my own.</p>
<p>In March, two attacks landed within days of each other.</p>
<p>The first hit <code>aquasecurity/trivy-action</code>. An attacker force-pushed 75 of 76 version tags — rewrote the history under tags people had already pinned. If your pipeline referenced one of those tags, and most did, your next run pulled credential-harvesting code. It read secrets out of <code>/proc/&lt;pid&gt;/environ</code>, encrypted them, and sent them to a typosquat domain. The advice we'd all given — "pin to a tag, don't float on latest" — is exactly what got people hit.</p>
<p>The second was <code>litellm</code>. Two malicious releases on PyPI carried a stealer in a <code>.pth</code> file, which Python executes on interpreter startup. You didn't have to import anything. If the package was ever on the machine, the code already ran.</p>
<p>Two different ecosystems, one shape: CI executed code it had no reason to trust, holding credentials it had no reason to hold, and afterward nobody could produce a signed record of what actually ran. You could read the workflow file. You couldn't prove what executed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-same-gap-now-with-agents">The same gap, now with agents<a href="https://cilock.dev/blog/signed-record-we-didnt-have-in-march#the-same-gap-now-with-agents" class="hash-link" aria-label="Direct link to The same gap, now with agents" title="Direct link to The same gap, now with agents" translate="no">​</a></h2>
<p>If that threat model feels abstract, look at how code gets written this week. An AI agent writes it. The agent edits the workflow. The agent opens the PR and, in plenty of shops, the agent triggers the release. The human in the loop is skimming a diff late at night, or there's no human at all — just another agent reviewing the first one.</p>
<p>I'm not anti-agent. We build with them every day. But "the agent did it" is not provenance, and the permissions you hand an agent are the permissions a poisoned dependency inherits. The fix isn't to slow the agent down. It's to put a gate at the end that the agent cannot open by itself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-cilock-actually-does">What CI/lock actually does<a href="https://cilock.dev/blog/signed-record-we-didnt-have-in-march#what-cilock-actually-does" class="hash-link" aria-label="Direct link to What CI/lock actually does" title="Direct link to What CI/lock actually does" translate="no">​</a></h2>
<p>Wrap any command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">cilock run -- go build </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> app ./</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.</span><br></div></code></pre></div></div>
<p>CI/lock records what ran: the command, the inputs it read, the environment, the artifacts it produced. It signs that record — keyless, so there are no long-lived keys to leak — and you get your first signed attestation in about 60 seconds.</p>
<p>Then you gate on it:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">cilock verify ./app </span><span class="token parameter variable" style="color:#36acaa">--policy</span><span class="token plain"> release.policy</span><br></div></code></pre></div></div>
<p>The policy is signed by a human, with their key. It says what's allowed to ship. The agent can do everything else — run the build, gather the evidence, draft the release — but it can't sign the policy, so it can't decide what ships. That line, between "did the work" and "decided what ships," is the whole point.</p>
<p>It's Apache 2.0, and it speaks Witness in both directions, so it drops into what you already have instead of asking you to rip anything out. It runs where your agent already runs: Claude Code, Codex, Cursor.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="built-for-how-we-ship-now">Built for how we ship now<a href="https://cilock.dev/blog/signed-record-we-didnt-have-in-march#built-for-how-we-ship-now" class="hash-link" aria-label="Direct link to Built for how we ship now" title="Direct link to Built for how we ship now" translate="no">​</a></h2>
<p>The tooling we built to secure the supply chain assumed a human doing the setup by hand, at human speed. An agent is already three commits deep before you've finished reading the docs. CI/lock is what Witness taught me, rebuilt for that: same lineage, with the bug fixes we found auditing the upstream code. What's new is who it's for. The first-class user is your agent.</p>
<p>Point your agent at a goal like "get this build to SLSA Level 3" and it can take you there. CI/lock is the engine: it emits SLSA Provenance and in-toto evidence at every step, signs it, and verifies it against Rego policy you write. The evidence then flows into the <a href="https://testifysec.com/" target="_blank" rel="noopener noreferrer" class="">TestifySec platform</a>, which maps it onto the frameworks you answer to — FedRAMP, SOC 2, NIST 800-53. A compliance report stops being a project you dread and becomes a read of evidence you already have.</p>
<p>And the part people dread most is already done: you don't stand up Fulcio, a timestamp authority, or any Sigstore plumbing. The platform hosts it. In CI you don't even log in — signing uses your runner's ambient OIDC token, keyless, no secrets. <code>cilock login</code> only matters when you want attestations stored on the platform. No CA to operate, no keys to rotate.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="if-youre-already-running-witness">If you're already running Witness<a href="https://cilock.dev/blog/signed-record-we-didnt-have-in-march#if-youre-already-running-witness" class="hash-link" aria-label="Direct link to If you're already running Witness" title="Direct link to If you're already running Witness" translate="no">​</a></h2>
<p>You don't have to switch tools to get any of this. CI/lock is the in-tree continuation of Witness: anything you produced with Witness verifies under CI/lock unchanged, and CI/lock's shared-format attestations verify back under Witness. Here's what the next iteration adds.</p>
<table><thead><tr><th>What it does</th><th>Witness</th><th>CI/lock (in-tree continuation)</th></tr></thead><tbody><tr><td>Attestation format</td><td>The donated in-toto/Witness project and the reference implementation. The DSSE/in-toto format the rest of the ecosystem reads.</td><td>The same format. Anything Witness produced verifies under CI/lock, with legacy type aliases registered at startup, and CI/lock's shared attestors verify back under Witness.</td></tr><tr><td>Trust setup (Fulcio, TSA, Archivista, keyless CI)</td><td>Ships all of it as first-class, including a GitHub Actions OIDC path. You point each endpoint at its host with its own flag.</td><td>Same endpoints, same flags, all still overridable. Adds one <code>--platform-url</code> that derives the hosted Archivista, Fulcio, TSA, and OIDC audience, and in GitHub Actions it signs keylessly off the runner's ambient OIDC token, with no login and no stored secret.</td></tr><tr><td>Capturing what actually ran</td><td>The <code>commandrun</code> attestor traces the wrapped process with ptrace. The portable, root-free path.</td><td>Keeps that exact ptrace path as a first-class mode, and adds an eBPF kprobe backend that traces at the kernel boundary. Default <code>auto</code>: probe eBPF, fall back to ptrace, record which backend ran.</td></tr><tr><td>Integrity over the build's files</td><td>The <code>product</code> and <code>material</code> attestors record each file as its own in-toto subject with a digest set. Fully policy-actionable.</td><td>The same per-file digests still flow through, and the file set is additionally committed to an RFC 6962 Merkle root, so one artifact gets a verifiable inclusion proof and a 29,000-file <code>npm install</code> doesn't balloon into a 10 MB envelope. Older envelopes stay verifiable.</td></tr><tr><td>Support and backing</td><td>A CNCF and in-toto project, maintained by a global open-source community.</td><td>Open source as well, with a commercial SLA from TestifySec behind it, a US company, for teams that need a vendor and a support contract on the hook for their build tooling.</td></tr></tbody></table>
<p>None of this is a break from Witness. Same lineage, same envelopes, same policy model, moving forward for a world where an agent is the one wrapping the build at 2am. We contribute fixes back upstream where they apply, because a stronger Witness is good for everyone. If you're running Witness today, CI/lock drops in next to it and reads what you already have.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-on-your-next-build">Try it on your next build<a href="https://cilock.dev/blog/signed-record-we-didnt-have-in-march#try-it-on-your-next-build" class="hash-link" aria-label="Direct link to Try it on your next build" title="Direct link to Try it on your next build" translate="no">​</a></h2>
<p>Most security engineers I talk to aren't missing conviction. They're missing a first step. "Secure your supply chain" is a mandate, not an instruction, and the world of SBOMs and signing and provenance is hard to walk into.</p>
<p>So here's the first step: wrap one command, sign one build, read the record it leaves behind. Once you have a signed account of what ran, everything downstream — policy, gating, the audit evidence — has something real to stand on. You don't need any of it to start.</p>
<p>You need one signed build. <a class="" href="https://cilock.dev/getting-started/installation">Get started</a>.</p>]]></content:encoded>
            <category>supply-chain</category>
            <category>cilock</category>
            <category>ai-agents</category>
        </item>
        <item>
            <title><![CDATA[We took a real project to SLSA Level 3 in 75 minutes. This post is the build log]]></title>
            <link>https://cilock.dev/blog/slsa-level-3-in-75-minutes</link>
            <guid>https://cilock.dev/blog/slsa-level-3-in-75-minutes</guid>
            <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A real project, forked and hardened to a gated SLSA Level 3 release in about an hour, with the write-up coming out of the same session. The screenshots are timestamped from the build.]]></description>
            <content:encoded><![CDATA[<p>Most write-ups about supply-chain hardening are composed weeks after the fact, by someone who was not in the terminal when it happened. This one was written in the terminal, while it happened. The screenshots are timestamped from the build. If that sounds like a strong claim, good, because the entire point of attestation is that claims should be checkable. So here is the clock.</p>
<table><thead><tr><th style="text-align:right">Clock</th><th style="text-align:center">Time (CDT)</th><th style="text-align:left">What happened</th></tr></thead><tbody><tr><td style="text-align:right">+0</td><td style="text-align:center">09:16</td><td style="text-align:left">Downloaded CI/lock and verified its own provenance</td></tr><tr><td style="text-align:right">+5m</td><td style="text-align:center">09:21</td><td style="text-align:left">Offline attested pipeline ran; tamper test rejected a forged binary</td></tr><tr><td style="text-align:right">+21m</td><td style="text-align:center">09:37</td><td style="text-align:left">Authenticated to the platform (screenshots captured live)</td></tr><tr><td style="text-align:right">+25m</td><td style="text-align:center">09:41</td><td style="text-align:left">The runbook you are reading was created, mid-build</td></tr><tr><td style="text-align:right">+28m</td><td style="text-align:center">09:44</td><td style="text-align:left">Public repo live; offline tier complete</td></tr><tr><td style="text-align:right">+37m</td><td style="text-align:center">09:53</td><td style="text-align:left">Keyless Level 3 build, green in CI</td></tr><tr><td style="text-align:right">+62m</td><td style="text-align:center">10:18</td><td style="text-align:left">Fail-closed verification gate, green in CI</td></tr><tr><td style="text-align:right">+74m</td><td style="text-align:center">10:30</td><td style="text-align:left">Illustrated runbook finished</td></tr></tbody></table>
<p>Everything after the 62 minute mark was writing what you are reading. The security work landed in about an hour, and the documentation accreted as a byproduct of doing it. The runbook was created at the 25 minute mark, while the gate it documents did not yet exist. Here is how the hour went, and why it went that fast.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="act-1-the-climb">Act 1: The climb<a href="https://cilock.dev/blog/slsa-level-3-in-75-minutes#act-1-the-climb" class="hash-link" aria-label="Direct link to Act 1: The climb" title="Direct link to Act 1: The climb" translate="no">​</a></h2>
<p>We started by forking Hugo, a static site generator that real people ship real sites with, not a hello-world repo built to make a point. The first tier of hardening needs no account and no network trust at all. One script wraps every build stage in a CI/lock attestation. It pins the source commit, records the build, produces a CycloneDX software bill of materials, runs a vulnerability scan, and signs a policy that gates the binary. All of it offline, with a local key.</p>
<p>That earns SLSA Build Level 1, plus the cryptographic substance that Level 2 also asks for: real signatures over real provenance. It is honest to call it exactly that, and nothing more. A local key sitting on the same machine as the build is forgeable, which means it is not Level 2 and not Level 3, and we did not dress it up as either.</p>
<p>Reaching Level 3 takes no extra YAML. It is a change of where the build runs and who holds the key. Move the same steps onto an ephemeral GitHub Actions runner, give the job an OIDC token, and the signer becomes a short-lived Fulcio certificate bound to the workflow's own identity. The build steps never touch a private key, because there is no longer a private key for them to touch. That isolation, a builder you do not operate and a key the build cannot reach, is what Level 3 actually means. The certificate on our CI attestation states it in plain text: issued by the TestifySec Platform Fulcio CA, subject set to the exact workflow file at github.com/testifysec/hugo, runner environment recorded as github-hosted. None of that is a name a human typed. It is the pipeline vouching for itself.</p>
<p><img decoding="async" loading="lazy" alt="The CI/lock platform authorize screen" src="https://cilock.dev/assets/images/01-cilock-authorize-page-427affbc9b1fd8dbde40c369f937c7ef.png" width="897" height="959" class="img_ev3q"></p>
<p><em>Authenticating the build to the platform, captured live at the 21 minute mark.</em></p>
<p><img decoding="async" loading="lazy" alt="Authorized" src="https://cilock.dev/assets/images/02-cilock-authorized-9107953a925144bae73050a60b627dee.png" width="574" height="586" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="act-2-the-gate">Act 2: The gate<a href="https://cilock.dev/blog/slsa-level-3-in-75-minutes#act-2-the-gate" class="hash-link" aria-label="Direct link to Act 2: The gate" title="Direct link to Act 2: The gate" translate="no">​</a></h2>
<p>Generating provenance is the easy 80 percent, and it is where a lot of the industry stops. An attestation nobody checks is a sticker. The question that matters is whether your pipeline will refuse to ship when the evidence is wrong, and the only way to know is to try to break it.</p>
<p>So we tried. We took the built binary, appended a single byte, and ran verification again. It failed, for the correct reason: the tampered digest is not a subject of any attestation signed by the trusted workflow identity. The honest binary verifies and the altered one is rejected, and the build stops there. The policy doing the enforcing is itself signed, by the offline release key, and it trusts exactly one signer for the build step: the keyless CI identity from Act 1. The operator signs the rule, the build signs the evidence, and the two are checked against each other. That separation is the whole game.</p>
<p><img decoding="async" loading="lazy" alt="A green secure-release CI run" src="https://cilock.dev/assets/images/03-green-secure-release-ci-run-cdc8fc40ab0618d1dfa14ed74523e1a7.png" width="1280" height="840" class="img_ev3q"></p>
<p><em>The gate, green in CI: the honest build verifies and ships.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="act-3-why-it-was-fast">Act 3: Why it was fast<a href="https://cilock.dev/blog/slsa-level-3-in-75-minutes#act-3-why-it-was-fast" class="hash-link" aria-label="Direct link to Act 3: Why it was fast" title="Direct link to Act 3: Why it was fast" translate="no">​</a></h2>
<p>Here is the part that matters if you are the one paying for it. Standing up Level 3 with policy-gated releases is normally scoped as a multi-week project for a platform team, because the list of moving parts is long: a Fulcio certificate authority, a timestamp authority, an attestation store, a policy engine, and the glue holding them together. We ran none of it. The TestifySec platform brings that trust plane and you operate exactly none of it. Our side of the contract was one workflow permission, id-token: write, and four words of attestor configuration. The platform URL, the keyless signing, and the attestation storage all came from defaults.</p>
<p>That is the real reason the clock reads an hour and not a quarter. The undifferentiated work, the part every company would otherwise rebuild and rebuild badly, is already done and already running. You bring a build command and an identity.</p>
<p>There is a second multiplier, and leaving it out would be dishonest, because half this story is that the session was driven by an AI coding agent rather than typed by hand. The agent read the release manifest and declined to pipe the installer into a shell. It decoded the signing certificate to confirm the identity was what we expected. It walked into a genuine wall, a Go build that is not bit-for-bit reproducible across machines, worked out why the gate verifies the attested artifact instead of a local rebuild, and kept going. A human approved the browser login and owned the judgment calls about visibility and naming. The agent did the rest, and it wrote the runbook as it worked. A CLI built to be driven, and an agent able to drive it; neither half alone gets you to an hour.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="act-4-the-payoff-you-did-not-have-to-ask-for">Act 4: The payoff you did not have to ask for<a href="https://cilock.dev/blog/slsa-level-3-in-75-minutes#act-4-the-payoff-you-did-not-have-to-ask-for" class="hash-link" aria-label="Direct link to Act 4: The payoff you did not have to ask for" title="Direct link to Act 4: The payoff you did not have to ask for" translate="no">​</a></h2>
<p>None of the evidence we just produced is disposable. It is signed, timestamped, and durable, and it maps to the compliance frameworks you will be asked about, on the day you decide you are ready to be asked. The most pressing of those today is the EU Cyber Resilience Act.</p>
<p>CRA is not a voluntary badge you chase to close a deal. It is law, with obligations phasing in toward 2027 and vulnerability-reporting duties landing earlier, and it gates access to the EU market with real penalties behind it. For products with digital elements it requires a machine-readable bill of materials covering at least your top-level dependencies, documented vulnerability handling, evidence that the product was built and shipped with integrity, and technical documentation that demonstrates all of it. Read that list again against what the build already emitted.</p>
<table><thead><tr><th style="text-align:left">CRA expectation</th><th style="text-align:left">What this build already produced</th></tr></thead><tbody><tr><td style="text-align:left">Machine-readable SBOM, top-level dependencies at minimum</td><td style="text-align:left">A full transitive CycloneDX SBOM, signed as an attestation</td></tr><tr><td style="text-align:left">Documented vulnerability handling</td><td style="text-align:left">An attested vulnerability scan, traceable to components through the SBOM</td></tr><tr><td style="text-align:left">Product integrity, secure by design</td><td style="text-align:left">SLSA provenance plus a gate proving the shipped binary is the one built from attested source</td></tr><tr><td style="text-align:left">Technical documentation for conformity</td><td style="text-align:left">A signed, timestamped evidence trail, plus a generated SSDF and SLSA mapping</td></tr><tr><td style="text-align:left">Due diligence on third-party and open-source components</td><td style="text-align:left">Lockfile and material attestations that pin the dependency graph</td></tr></tbody></table>
<p>A careful line is owed here, because this is legal ground and overclaiming on it is its own kind of insecurity. The platform produces and maps the technical evidence that underpins these requirements. It does not file your conformity assessment, affix a CE mark, or run your disclosure process. Those are organizational obligations, and your counsel owns the exact dates and the scope. What the platform takes off your plate is the part engineers actually dread, which is assembling the evidence after the fact, by hand, under a deadline. You generated it as a side effect of a build you were going to run regardless.</p>
<p>The same evidence answers more than CRA. Point it at NIST's Secure Software Development Framework, at SOC 2, at FedRAMP, and it is the same signed attestations read through a different control mapping. You do the security engineering once. The audit artifacts are there when you need them and not a minute sooner, which is precisely how a security team prefers it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-receipt">The receipt<a href="https://cilock.dev/blog/slsa-level-3-in-75-minutes#the-receipt" class="hash-link" aria-label="Direct link to The receipt" title="Direct link to The receipt" translate="no">​</a></h2>
<p>That is the whole story. A real project, forked and hardened to a Level 3 release with a gate that fails closed, in about an hour, with the write-up coming out of the same session. The speed holds up because the trust plane was already running and the agent driving the CLI knew what it was doing. The compliance mapping, CRA included, is not a second project you signed up for later. It is the same evidence, waiting.</p>
<p>The screenshots above are from that session, timestamped from the build. Everything here verifies.</p>]]></content:encoded>
            <category>supply-chain</category>
            <category>cilock</category>
            <category>slsa</category>
            <category>ci</category>
        </item>
    </channel>
</rss>