opinion
DevSecOps practice notes
Thirty-six field notes on DevSecOps practice: pipeline identity, build provenance, secret blast radius, and vulnerability triage after NVD stopped enriching a third of new CVEs. Each note states the condition under which it stops being true.
These are working notes, not a framework. Each states a claim, the argument for it, and the condition under which it stops being true. They come from watching the same delivery-pipeline failures survive three generations of tooling. Two of them reverse advice I gave publicly, and one rests on a measurement that contradicts what most vulnerability programmes still assume about their reference data.
How to read these notes
Every note has the same shape: a claim, a short argument, and a closing line beginning Stops being true when. That last line is the point of the format. A practice note without a falsification condition is a slogan.
One test runs underneath all thirty-six. When something in a delivery pipeline fails, ask whether the failure would survive replacing the tool involved. Swap the scanner, the gate, the ticketing integration, the policy engine — if the same thing happens six months later, the failure was never in the tool. It was in who was accountable, what authority they had, and how long it took them to find out.
The two categories consume different budgets and have different repair times. A tooling failure is expensive and fast: you can replace a scanner in a quarter. An ownership failure is cheap and slow: it costs nothing to draw a new responsibility line and eighteen months to make it real. Teams reliably misclassify the second as the first, because the first has a vendor attached and therefore has a meeting.
How much of this is measured
The two halves of this article are not equally evidenced, and you should weight them accordingly.
The pipeline and dependency notes rest on primary sources and on measurements whose method I show: a complete census of NVD enrichment status for one quarter, the CISA KEV feed, vendor documentation quoted with dates. If those notes are wrong, they are wrong in a way you can check.
The ownership notes — 1 to 5, 28 to 34, and the anti-patterns table — are argued from mechanism. Their external support amounts to two data points: DORA's 2022 finding that the strongest predictor of application-development security practice is cultural rather than technical, and NIST SSDF RV.2, which supports a narrower claim about what frameworks can assign. Everything else there is my judgment, and the "stops being true when" lines are how you hold me to it. I have not closed that gap and will not dress judgment as evidence to hide it. The most I can offer is circumstantial: two of the strongest controls documented below — SLSA v1.2's Source Track Level 4, requiring two-party review, and npm's staged publishing, requiring a human second factor the pipeline does not hold — are ownership controls written into a specification and a registry. Authority boundaries are being encoded where only tooling used to live.
What this is not
Not a maturity model. The CNCF Platform Engineering Maturity Model exists and is a reasonable document; it is the convention this piece declines, because every level in it is defined relative to the previous level rather than relative to an attacker.
Not platform-neutral. The pipeline and dependency notes are GitHub- and npm-shaped, because that is where the 2026 changes happened and where documentation is specific enough to quote. "Where these notes do not apply" says which arguments transfer elsewhere.
Version numbers, spec revisions and dataset measurements were verified on 2026-08-06. Several will be wrong within a quarter; the final section says which.
Where these notes come from
In July 2018 I gave a talk called "Shifting strategy from DevOps to DevSecOps" and posted an announcement for it, timestamped 2018-07-29T16:21:32+00:00. It is roughly sixty words of prose around an eight-item agenda: introduction; key challenges and recommendations; analysis; core practices; a pipeline for application and infrastructure security; tool selection tips; implementation strategy; and a final checklist.
The substance was in a SlideShare deck. I recovered the post from an Internet Archive snapshot dated 2022-05-19, but not the deck: SlideShare serves a bot-challenge page to automated fetches, and the snapshot embeds it by iframe rather than inlining it. So I have re-read my own post and not my own slides, and nothing here characterises what those slides said.
The agenda still establishes the 2018 frame: organisational strategy, pipeline coverage, tool selection, a checklist. Eight years later one of those four has held completely, one partly, one has been comprehensively invalidated, and one is worth keeping. "What has changed since 2018" scores each. This is a new article under a current date, not an archive migration.
Notes on ownership
1. Security ownership is an allocation problem, not an awareness problem. Almost every remediation programme that stalls, stalls in the same place: a finding exists, several teams agree it is real, and no team's on-call rotation gets worse if it stays open. Fixing it consumes a sprint from a team whose objectives do not mention it. Training does not move this. Changing whose quarterly commitments the fix appears in does.
NIST SP 800-218, the Secure Software Development Framework (v1.1, published 2022-02-03), is honest about this boundary. Its Response and Vulnerability group tells you to help ensure vulnerabilities are remediated in accordance with risk — RV.2. Note the verb. The framework assigns responsibility for ensuring remediation happens; it does not, and cannot, assign the owner. That assignment is yours, and it is what most SSDF gap assessments quietly skip.
Stops being true when: the same team writes the code, operates it, and carries the pager. Then allocation is already solved and awareness genuinely is the constraint. This describes most teams under about fifteen engineers, who should skip to note 6.
2. Remediation ownership follows deploy authority, not code authorship. If team A wrote the library and team B ships the service embedding it, the finding lands on B whether or not B can fix the root cause. Assigning it to A produces a ticket that ages; assigning it to B produces a version bump and a workaround. Both are needed, but only one can be scheduled by the team receiving the ticket. Route findings to whoever can change what is running, and open a separate item against whoever can change the source. Your tracker therefore needs two owner fields. Most have one, which is why the owner field on a dependency finding is usually a lie.
Stops being true when: a central platform team can ship a dependency bump across all consumers without per-service action. Then the two authorities coincide and one field is enough.
3. "The security team owns security findings" guarantees a queue, and the queue length is the real metric. A central team with no deploy authority can triage, contextualise and escalate. It cannot close. Every finding it accepts joins a queue whose service rate is bounded by other teams' willingness, so the queue grows monotonically and is presented quarterly as a resourcing problem. It is a routing problem.
The diagnostic is not how many open findings you have but what fraction are assigned to a team that can close them without a negotiation. Below about half, adding security headcount will not change the trend line. The half is my rule of thumb, not a measured threshold; the durable part is measuring the fraction at all.
Stops being true when: the central team is also the platform team and can ship into every service's base image or dependency set unilaterally.
4. Culture is a real predictor, and citing it is usually an evasion. DORA's 2022 research found the biggest predictor of an organisation's application-development security practices is cultural rather than technical. I think that is correct, and it is also the most-abused sentence in this field, because culture is where an argument goes when nobody wants to name a decision. Culture here is not enthusiasm. It is who may say no, whose deadline slips when a finding is real, and whether a team that stops a release is thanked or investigated — org-design decisions with names and dates. An intervention that changes none of them will not change the outcome. Note the vintage: a 2022 finding, cited as evidence about a durable structural relationship, not a fresh measurement.
Stops being true when: someone produces a cultural intervention with a measurable effect that does not route through an authority change. I have not seen one and would revise this gladly.
5. A security champion without deploy authority is a liaison. Liaisons reduce ambiguity about who to ask. They do not reduce risk, which is what champion programmes are usually funded for. The version that works grants two powers: spending a defined fraction of team capacity on security work without asking, and holding a release. The version that does not grants a Slack channel and a monthly meeting.
Stops being true when: the role is explicitly scoped as translation and triage and funded as such. A well-run liaison function is useful; the failure is in the claimed outcome, not the role.
Notes on the pipeline as production
6. CI is production-adjacent, and it is usually the weakest identity boundary you operate. The runner holds credentials that deploy to production, publish to your registry, and often assume roles across several accounts. It executes code from pull requests and pulls third-party actions at mutable references. It is a machine that runs untrusted input with production authority, and in most organisations it is administered by whoever set it up first. What changed is that the rest of the environment got harder while CI did not: production workloads now run with short-lived workload identity, admission control and network policy, and the build system producing them frequently has none of the three.
Illustrative — trust boundaries in a typical hosted CI pipeline. Adapt zone names to your own platform.
Alt text: a left-to-right flow diagram with three trust zones. Untrusted input, containing a fork pull request and a third-party action at a mutable tag, feeds into the CI trust zone containing the runner, workflow token and OIDC token. The runner's OIDC token crosses into the cloud and artifact zone, reaching a cloud IAM role, an artifact registry and a deployment target. Two edges are labelled as boundaries: untrusted input to runner, and OIDC token to cloud IAM role.
Stops being true when: builds run on ephemeral, network-restricted runners with no standing cloud credentials, and every credential the build obtains is scoped to one artifact and expires within the build. Achievable today; not the default anywhere.
7. OIDC federation did not remove the long-lived credential problem — it relocated it into the trust policy, where it is harder to audit. Replacing a static access key with a federated token is a genuine improvement: no secret sits in a settings page waiting to be exfiltrated. But security now rests entirely on the conditions in the cloud-side trust policy, and those are written once, by whoever set up the first workflow, and never reviewed. A stored key at least appears in a secrets inventory. A permissive sub condition appears nowhere.
GitHub's documentation is direct: you must define at least one condition, so that untrusted repositories cannot request access tokens for your cloud resources. The issuer is https://token.actions.githubusercontent.com. The condition is yours to get right.
Illustrative — AWS IAM trust policy fragments. Account ID redacted. Validate against your own OIDC provider ARN and repository naming before use.
// WRONG: any workflow in any repository in the org, on any ref,
// can assume this role. The wildcard swallows the branch condition.
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:example-org/*" }
}
}
// BETTER: one repository, one ref, exact match.
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example-org/example-repo:ref:refs/heads/main"
}
}
}
Read the second fragment as a shape, not a template. An exact match on refs/heads/main refuses a tag-triggered release, an environment-scoped deployment or a release branch, and the usual response to that breakage is to widen the condition back to a wildcard under time pressure. Enumerate the refs and environments that legitimately deploy before tightening, and add them as separate exact-match entries rather than a pattern.
Stops being true when: trust policies are generated from the same source of truth as the workflows, so a repository rename or a new environment cannot silently widen the condition.
8. The corrected trust policy above is already legacy, and that is the most instructive thing in these notes. GitHub documents that repositories created after 2026-07-15 use an immutable default subject format including numeric owner and repository IDs — repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH rather than the name-based form. The reason is sound: names can be transferred, so a policy keyed on a name is keyed on a mutable identifier. The feature is not available on GitHub Enterprise Server.
A condition many of us have written, reviewed and approved for years is now the legacy format. It is not wrong for existing repositories. It is wrong for new ones, and it fails open in one scenario: if the name-based subject can still be produced by a repository you no longer control. Treat it as a scheduled audit item, and check which format each repository actually emits rather than inferring from its creation date. I have written the name-based form into more documents than I want to count; this is a correction to my own prior advice.
Stops being true when: the immutable format becomes universal and the name-based form is no longer accepted. The note then becomes history.
9. AWS OIDC provider thumbprints are now a fallback, and the rotation runbooks many teams maintain are mostly obsolete — but the field is not optional. AWS documents that it verifies the JWKS endpoint's TLS certificate against its own library of trusted certificate authorities, and uses configured thumbprints only if that fails. Three conditions trigger the fallback: the certificate is not signed by a trusted CA, AWS cannot retrieve the TLS certificate, or TLS v1.3 is required.
Most write-ups get the first consequence and miss the second. The calendar reminder to rotate thumbprints before GitHub's certificate changes is folklore that outlived the behaviour producing it. But an IAM OIDC provider must still have at least one thumbprint and may have up to five, so you cannot delete the value, and the second and third fallback conditions mean a thumbprint can become load-bearing without your provider changing CA. Keep a correct value; retire the ritual.
Stops being true when: you use a self-hosted or non-public identity provider whose chain AWS does not trust. Then the thumbprint is load-bearing continuously and the runbook is real.
10. Pinning actions to a commit SHA is correct, and it delayed a security fix from reaching a large number of repositories in July 2026. GitHub states that pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release. That is the right advice against tag mutation.
Now the other side. actions/checkout v7 went GA on 2026-06-18 with a new default: it refuses to check out fork pull request code under pull_request_target and workflow_run unless explicitly told otherwise. The opt-out input is allow-unsafe-pr-checkout, which GitHub says was intentionally named to be easy to spot in code review and static analysis — good naming, worth copying. On 2026-07-20 that enforcement was backported to all supported major versions except v1.
Read that carefully. A repository using the floating tag actions/checkout@v4 received the backported protection automatically. A repository that had done the diligent thing and pinned to a full-length SHA received nothing, because a SHA is by definition not updated. The hardened configuration was the one that stayed vulnerable.
Illustrative — the pinning trade-off as it appeared in July 2026. Substitute your own pinned digest.
# Pinned to a SHA: immune to tag mutation.
# Did NOT receive the 2026-07-20 backport of the safer fork-PR default.
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3
# Floating major tag: exposed to tag mutation.
# DID receive the 2026-07-20 backport automatically.
- uses: actions/checkout@v4
The resolution is not to unpin. Unpinning trades a rare, high-impact, attacker-controlled failure for a common, low-impact, vendor-controlled one, and the first is the one that ends with your secrets in someone else's log. The resolution is to accept that pinning converts a security property into a maintenance obligation, and to fund it: automated digest bumps with a review step, a named owner for the action inventory, an alert path for upstream security releases. Pinning without a bump process is worse than not pinning, because it produces the paperwork of a control with the freshness of an archive.
Stops being true when: registries offer immutable-by-construction tags with a separate security-patch channel, so a reference can be both immutable and patchable. Container registries are closer to this than action marketplaces.
11. Tag mutability is the mechanism, and there is a documented case with a score attached. NVD's record for CVE-2025-30066 reads: "tj-actions changed-files before 46 allows remote attackers to discover secrets by reading actions logs. (The tags v1 through v45.0.7 were affected on 2025-03-14 and 2025-03-15 because they were modified by a threat actor to point at commit 0e58ed8, which contained malicious updateFeatures code.)" Published 2025-03-15, CVSS 3.1 base score 8.6, vector AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N, NVD as primary scoring source.
Two elements of that vector are worth pointing at where "pin your actions" is being deprioritised. PR:N — no privileges required. S:C — scope changed, meaning impact crossed a security boundary beyond the vulnerable component. That is the formal statement of "the build system reached into things it was not meant to reach into." I cite NVD's description as written and do not speculate about how the tags came to be modified.
Stops being true when: the reference format itself becomes non-mutable across the ecosystems you consume.
12. Self-hosted runners on public repositories are an accepted-risk decision, not a configuration detail. GitHub's wording is unambiguous: self-hosted runners should almost never be used for public repositories, because any user can open pull requests and compromise the environment. Teams do it anyway for GPU access, network position, or licensing — all legitimate needs. The failure is not the decision; it is that the decision gets made in a settings page by one engineer, with no compensating controls and no record.
If you must, the compensating set is: ephemeral runners destroyed after each job, no standing cloud credentials on the host, an egress policy, and required approval before workflows run for first-time contributors. Any of those missing turns the arrangement into arbitrary code execution inside your network with a Git interface.
Stops being true when: the runner is genuinely single-use and network-isolated, in which case "compromise the environment" degrades to "compromise a container about to be deleted" — still bad if secrets are present, survivable if they are not.
13. The platform is hardening faster than the guidance, and the canonical CI/CD threat taxonomy is four years stale. In a ten-week window in 2026, GitHub shipped controls over who and what can trigger workflows (2026-06-18), the safer checkout default above (2026-06-18), read-only Actions cache for untrusted triggers (2026-06-26), and holding potentially malicious workflows for approval (2026-07-28).
Meanwhile the OWASP Top 10 CI/CD Security Risks — the taxonomy most teams cite when scoping a pipeline review — reached stable v1.0 in October 2022 and has not been revised. It remains a good document, written before OIDC federation was ubiquitous, before artifact attestations existed as a product, and before any of the 2026 defaults. Use it as a checklist of failure classes, not a statement of current state. The operational implication is a review step most teams lack: read the platform's changelog for the period since your last review. Defaults now change faster than policies, and a default that changed in your favour is still a change you did not decide.
Stops being true when: the taxonomy is refreshed, or your platform's release cadence slows. Neither looks imminent.
Notes on provenance
14. SLSA v1.1 is retired, v1.2 is current, and v1.2 restructured the spec rather than incrementing it. The v1.1 levels page carries a "Retired" marker pointing to v1.2. The v1.2 specification, status Approved, is organised into two tracks. The Build Track runs L1 to L3. The Source Track runs L1 "Version controlled", L2 "History and provenance", L3 "Continuous technical controls", and L4 "Two-party review", which requires two trusted persons to review all changes to protected branches.
Under v1.0 and v1.1, provenance was a claim about how an artifact was built. Under v1.2 you can also claim how the source going into it was controlled — whether history is protected, whether changes were reviewed, whether controls were continuously enforced rather than asserted. For most organisations the Source Track is closer to where the gap is.
Note where the tracks top out, because it is the reverse of what people assume: Build stops at L3, Source goes to L4. If your documentation says "SLSA Level 3" without naming a track, it is now ambiguous in a way that flatters you — Build L3 is the ceiling, Source L3 is not.
Stops being true when: v1.2 is superseded. The version moved twice recently; check before you cite.
15. Build Level 3 is a claim about the build platform, not about your repository. L2 requires that all build steps ran on a hosted build platform, not on an individual's workstation. L3 requires isolation: an ephemeral environment provisioned per build, no build access to platform secrets such as the provenance signing key, no influence between builds overlapping in time, and no injection of false entries into a cache used by another build.
Those are properties of the service running your build. On a hosted CI platform you cannot reach L3 by changing your own configuration alone; you reach it by choosing a configuration the provider designed to meet it. GitHub documents that its artifact attestations by themselves provide SLSA v1.0 Build Level 2, and that going further means builds using known, vetted instructions — a reusable workflow shared across repositories.
Note the versioning mismatch: that claim is pinned to SLSA v1.0 wording while the spec is at v1.2. Not a criticism of the feature — a caution against transcribing the claim into your own compliance documentation without the qualifier, because only one of those two documents is yours to defend.
Stops being true when: the provider's level claim is updated to current spec wording, or you move to a platform where you control the isolation properties.
16. "Signed" without an identity constraint means "somebody signed it", which is not a security property. Sigstore's keyless flow issues short-lived certificates bound to an OIDC identity, so verification is only meaningful if you assert which identity you expect. cosign requires --certificate-identity and --certificate-oidc-issuer for keyless verification for exactly this reason. There is also a --check-claims=false escape hatch, which still verifies the signature and payload against the supplied key but stops verifying the claims in the payload, including the digest binding. It should never appear in a pipeline. The equivalent for GitHub artifact attestations is that gh attestation verify takes a repository or owner constraint; without one you are verifying that an attestation exists.
Illustrative — verification commands. Replace identities and issuers with your own; do not copy the identity strings.
# Proves only that a signature exists. Not a control.
cosign verify ghcr.io/example-org/example-image:1.4.2
# Proves that a specific workflow in a specific repository signed it.
cosign verify ghcr.io/example-org/example-image:1.4.2 \
--certificate-identity "https://github.com/example-org/example-repo/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
# GitHub artifact attestations, scoped to a repository.
gh attestation verify oci://ghcr.io/example-org/example-image:1.4.2 \
-R example-org/example-repo
The identity format depends on the issuer, and an exact match pinned to one ref has the same brittleness as the trust policy in note 7: a release cut from a tag will not match an identity pinned to refs/heads/main. Decide deliberately whether your policy pins the workflow path, the ref, or both, and check your cosign version's flag list for pattern-matching forms before settling on exact strings.
Stops being true when: verification is expressed declaratively in an admission controller with the identity in policy rather than a command line. The failure mode moves from "forgot the flag" to "policy scope too broad" — different failure, same class.
17. For private repositories, GitHub artifact attestations use GitHub's own Sigstore instance without a transparency log. GitHub's wording: private repositories generating artifact attestations use GitHub's Sigstore instance, which uses the same codebase as the Sigstore Public Good Instance, but does not have a transparency log and only federates with GitHub Actions.
The public-good deployment writes to a public transparency log, which is what makes third-party detection of anomalous signing possible. Private-repository attestations do not get that. They still give you a verifiable statement that a particular workflow produced a particular artifact; they do not give you the append-only public record. If your provenance threat model includes "an insider signs a malicious artifact and we detect it out of band", this configuration does not serve it. If it is "a compromised build elsewhere in the supply chain produces an artifact my deploy accepts", it does.
On log versions: the Sigstore project's post of 2026-06-28 states the sigstore.dev public good instance will continue using Rekor v1 as the default log for the foreseeable future, because Rekor v2 introduces breaking changes in client behaviour — and notes the coming post-quantum cryptography transition will force client updates of its own. Rekor v2 reached general availability on 2025-10-10. Check which log version your clients speak rather than assuming the newest.
Stops being true when: GitHub changes the private-repository configuration, or you run your own transparency log.
18. Provenance you generate and never verify is a build-time cost with no security value. This is the most common state of SLSA adoption I encounter: generation switched on, artifacts signed, nothing in the deployment path that fails when verification fails. It produces a green compliance answer and no risk reduction, because the attacker's path does not pass through the step you added.
The minimum useful configuration is one enforcement point that refuses an artifact lacking a valid attestation from an expected identity. Where it sits — admission controller, deployment pipeline, registry policy — matters less than that it exists and has visibly rejected something. If it never has, you do not know it works, and the cheapest test is to build an artifact deliberately outside the sanctioned path and confirm the gate stops it.
The in-toto Attestation Framework (spec v1.2; Statement type URI https://in-toto.io/Statement/v1) gives you the envelope for carrying further predicates — SBOMs, test results, review records — through the same verification path. Worth building toward, but only after the first enforcement point exists. Predicates nobody evaluates are the same failure at larger scale.
Stops being true when: you generate provenance for a downstream consumer who does verify it. Then the value accrues to them, and generating without verifying is correct for you.
Notes on secrets
19. Rotation cadence is a proxy for blast radius, and a poor one — but it gets funded because it is countable. A ninety-day rotation SLA answers "how long is a leaked credential useful for" with a worst-case number. It says nothing about how much that credential can do, how many principals can mint an equivalent, or how quickly you would notice it being used. Those three determine the cost of an exposure and none appear on a rotation dashboard. Rotation wins the budget argument because it produces a percentage: "94% of credentials rotated within policy" is a slide, whereas "median credential scope reduced from 41 to 9 permitted actions" requires explaining what a permitted action is.
Stops being true when: you genuinely cannot reduce scope. Some legacy systems have one credential with one permission level; there, cadence is the only lever and should be pulled hard.
20. AWS's own guidance ranks rotation below elimination, and the headings say so. The IAM best-practices document's headings include "Require human users to use federation with an identity provider to access AWS using temporary credentials" and "Require workloads to use temporary credentials with IAM roles to access AWS." Rotation appears under a differently-scoped heading: "Update access keys when needed for use cases that require long-term credentials."
Read as a hierarchy that is unambiguous. Rotation is the fallback for the case where you failed to eliminate the long-lived credential, not a practice to apply uniformly. A programme reporting rotation compliance across an estate still holding hundreds of long-lived keys is measuring the fallback and calling it the goal.
Stops being true when: federation is unavailable. Some partner integrations and older SaaS platforms still require a static key — exactly the credentials where cadence matters most, which argues for measuring cadence only on that population.
21. The rotation machinery is itself a privilege-escalation surface, and this is documented rather than theoretical. AWS Secrets Manager's documentation on turning on rotation states you need both iam:CreateRole and iam:AttachRolePolicy, and warns: "Granting these permissions allows an identity to grant themselves any permissions."
Consider what that means. To reduce the impact of a credential compromise you deploy a system that must create roles and attach policies; if that system is compromised, the blast radius exceeds the credential you were protecting. Not an argument against rotation — an argument for treating rotation infrastructure as a tier-zero system with its own review, change control and alerting, the way you treat your identity provider. Most rotation deployments I have reviewed treat the Lambda as application infrastructure.
Stops being true when: rotation is implemented with pre-created roles and the deploying identity does not hold iam:CreateRole. More work to set up, and it removes the escalation path. It should be the default pattern and is not.
22. Measure credential lifetime, scope, and mintability — in that order — and let cadence fall out of it. The three numbers describing exposure to a leaked credential are how long it stays valid, how many distinct actions it authorises, and how many principals can produce an equivalent. A one-hour token scoped to three actions that two workflows can mint is a fundamentally different exposure from a permanent key with an administrative policy that four teams can regenerate, even when both rotate on the same schedule.
GitHub secret scanning is worth running regardless: free on public repositories, it validates detected secrets against partner providers and notifies them so they can revoke. Its remediation guidance leads with rotating the affected credential immediately rather than rewriting history, which is the right priority — the credential is the exposure, the commit is evidence.
Stops being true when: you cannot instrument scope. Some platforms do not expose a machine-readable permission set per credential; then lifetime and mintability are what you have.
Notes on dependency risk
23. The reference data underneath your vulnerability programme changed hands, and most triage processes have not noticed. On 2026-04-15 NIST moved the National Vulnerability Database to risk-based enrichment prioritisation. Priority goes to CVEs in the CISA Known Exploited Vulnerabilities catalogue (target: one business day), CVEs in federal government software, and Executive Order 14028 critical software. Everything else is categorised as "Lowest Priority - not scheduled for immediate enrichment". All backlogged CVEs published before 2026-03-01 were moved to "Not Scheduled".
NIST's stated reason is volume: CVE submissions increased 263% between 2020 and 2025; NVD enriched nearly 42,000 CVEs in 2025, 45% more than any prior year; submissions in the first three months of 2026 ran nearly a third higher than the same period the previous year. I measured publication volume independently against the NVD API on 2026-08-06, reading totalResults over publication-date windows.
| Window | CVEs published | Change |
|---|---|---|
| Q1 2025 | 12,412 | — |
| Q1 2026 | 16,255 | +31.0% year on year |
| Q2 2026 | 20,871 | — |
That corroborates the characterisation. What changes your process is enrichment. On 2026-08-06 I retrieved the vulnStatus of every CVE published in Q2 2026 — all 20,871 records, a complete census of that window rather than a sample.
vulnStatus | Count | Share |
|---|---|---|
| Analyzed | 10,301 | 49.4% |
| Deferred | 7,488 | 35.9% |
| Modified | 1,730 | 8.3% |
| Awaiting Analysis | 1,034 | 5.0% |
| Rejected | 176 | 0.8% |
| Undergoing Analysis | 142 | 0.7% |
Better than a third of one quarter's CVEs carry Deferred, meaning NVD has decided not to enrich them.
The obvious conclusion — that those records have no severity — is wrong, and the truth is more awkward. In the same census, 99.2% of records carry a CVSS vector from some source. Only 22.9% carry a vector attributed to nvd@nist.gov. Among Deferred records that figure is zero, and among Awaiting Analysis records it is also zero; every one carries a vector supplied by the assigning CNA instead.
The number does not go missing. Its provenance changes, silently, in a field most triage pipelines never read. Your scanner still shows a severity, and for a growing majority of recent CVEs that severity is now the assigning vendor's assessment of its own product, with no independent second opinion and nothing surfacing the difference where a human looks. A branch reading severity >= HIGH does not break loudly. It quietly starts consuming a differently-produced input.
Two honesty notes on the measurement. I measured the vulnStatus field and the source field on CVSS metrics; I did not evaluate score quality, and nothing here says CNA scores are worse, only that they are unreviewed by NVD. And Analyzed records without an nvd@nist.gov vector are most likely cases where NVD's analysis produced applicability or reference data rather than a new score — I report the fields rather than infer the workflow behind them.
The practical step is small: make your ingestion record which source produced the score it branches on, and count how many open findings run on an unreviewed vendor score. That number is your exposure to this change.
Stops being true when: consumers begin treating CNA-supplied scores as equivalent in practice and the distinction stops carrying information, or NVD's capacity recovers. Worth re-measuring in a quarter; the method is above.
24. The arithmetic between published CVEs and known-exploited CVEs is the strongest argument against severity-threshold gates. The CISA KEV catalogue, catalogVersion 2026.08.06, contained 1,661 entries across its entire history when I pulled the JSON feed. Additions by year: 2021: 311, 2022: 555, 2023: 187, 2024: 186, 2025: 245, and 177 in 2026 to 6 August — of which 146 fell in the first half of the year.
Set that against publication. Roughly 37,100 CVEs were published in the first half of 2026; in the same window, 146 were added to KEV, about four in every thousand. FIRST's own statement, in an article whose underlying data is dated to March 2022, is that only around 5% of all vulnerabilities are ever exploited in the wild — a different measure over a much longer window, but the same order of magnitude for the point being made.
A gate blocking on "no high or critical severity findings" is therefore blocking on a signal that is, at the population level, overwhelmingly unrelated to exploitation. It will be bypassed. The bypass will be justified. The justification will be correct.
The misreading to avoid is bigger than the argument. This is a statement about the population of published CVEs, not about the vulnerability in front of you. A single unauthenticated remote code execution in your internet-facing edge is not made safe by the fact that most CVEs are never exploited, and nothing here licenses deprioritising a finding because the base rate is low. The arithmetic says severity alone cannot rank a queue this size. It does not say the queue is harmless.
Illustrative — the triage funnel with measured values. Numbers are as of 2026-08-06 and will move.
Alt text: a top-down funnel. About 37,100 CVEs were published in the first half of 2026. A complete census of the second quarter found 49.4% analysed by NVD and 35.9% deferred, with 22.9% carrying a CVSS vector attributed to NVD. In the same half-year, 146 vulnerabilities were added to the CISA Known Exploited Vulnerabilities catalogue. Two further narrowing stages, presence in your running estate and reachability or internet exposure, are labelled as things you must measure yourself.
Stops being true when: KEV inclusion criteria broaden substantially, or you operate in a sector whose exploited set is not represented in KEV. KEV is a US federal catalogue with specific inclusion criteria — a floor, not a census of exploitation, and a lagging indicator by construction.
25. CVSS base score is not risk, and FIRST explicitly says not to multiply it by EPSS. The CVSS v4.0 specification (document version 1.2) defines four nomenclatures — CVSS-B, CVSS-BT, CVSS-BE, CVSS-BTE — and states that the usefulness of a numerical CVSS score is directly proportional to the metrics used to generate it. Nearly every tool reports CVSS-B, the base score alone, and nearly every process treats that number as risk.
EPSS is a different thing again. FIRST describes it as a machine-learning model estimating the probability that a published CVE will be exploited in the wild in the next 30 days, republished daily, and its FAQ is explicit that EPSS does not measure how much damage successful exploitation would cause, or whether it affects your environment. And then, directly: "Multiplying EPSS by a CVSS score does not compute probability × severity and is never a good idea." A great many internal risk-scoring spreadsheets do exactly this. If yours does, the number it produces has no defined meaning, which is worse than having no number, because it survives scrutiny that a missing number would not.
Express triage as a decision, not a score. The decision has inputs you can name and defend individually.
Pseudocode — a triage decision expressed as conjunctions rather than a composite score. Adapt the exposure and reachability predicates to your own inventory.
for each finding:
if not present_in_running_estate(finding):
route -> backlog, no SLA
continue
exposed = internet_reachable(finding.component)
or handles_untrusted_input(finding.component)
reachable = call_path_reaches_vulnerable_symbol(finding) # where the language supports it
exploited = finding.cve in KEV
or epss_probability(finding.cve) >= epss_cutoff
if exploited and (exposed or reachable):
route -> expedited, owner = deploy_owner(finding.component)
elif exposed and reachable:
route -> standard, owner = deploy_owner(finding.component)
elif exploited:
route -> standard, owner = deploy_owner(finding.component)
else:
route -> batched with next dependency update
# Deliberately absent: any multiplication of EPSS by CVSS.
# FIRST states this does not compute probability times severity.
That leaves epss_cutoff, and an argument against thresholds that ends by introducing one owes you a way to set it. Do not pick a round number. Derive it from capacity: rank the findings actually present in your estate by EPSS descending; count how many findings your remediation path has demonstrably closed per month over the last two quarters; the EPSS value at that rank is your cutoff. Beyond it you are writing tickets you have no evidence you can close.
Then check the cutoff rather than trusting it. Of the KEV entries that appeared in your estate over the same period, what fraction would this cutoff have flagged before KEV listed them? If that fraction is near zero, the EPSS term buys nothing KEV membership was not already buying, and the effort belongs in exposure and reachability data instead. This is a method, not a house number, and it yields a different cutoff for every organisation — the honest answer, because the input it depends on is your remediation capacity, not a property of the vulnerability.
Stops being true when: you have environmental CVSS metrics populated for your own estate — CVSS-BE or CVSS-BTE. Then the score does mean something about your risk, because you supplied the missing half. Almost nobody does.
26. Reachability analysis changes the shape of the problem, but only where the tooling can see the call graph. Go's govulncheck documentation describes it as analysing your codebase and surfacing only vulnerabilities that actually affect you, based on which functions in your code transitively call vulnerable functions; the Go team describes the project as a work in progress. Where this works, it removes a large fraction of findings honestly rather than by threshold.
The caveats below are my analysis, not the Go documentation's, and you should state them when presenting the reduced numbers. Reflection, dynamic dispatch, code generation, plugin loading and configuration-driven execution all defeat static reachability. A "not reachable" verdict is a statement about the analysable call graph, not about the program, and in dynamic languages the analysis is substantially weaker. Reachability also says nothing about data: a function you call with input an attacker never controls is reachable and not exploitable, a second filter the tooling does not apply for you.
The honest framing when reporting a filtered number: this is the set we can demonstrate is reachable, plus an unmeasured set we cannot analyse. Presenting the filtered number as the total is how a reachability tool becomes a way of not looking.
Stops being true when: the component is loaded dynamically or invoked through configuration, which describes a great deal of real application behaviour.
27. The registry moved faster than the scanner, and registry-level controls are now doing more work than your SCA is. A set of 2026 changes shifted where dependency risk is addressed:
- Dependabot applies a three-day cooldown to version updates as of 2026-07-23. Security updates are unaffected — GitHub's reasoning is that they still open right away, since a delay would hold back a fix for a flaw that is already public. The rationale came from reviewing 21 widely reported supply-chain incidents between 2018 and 2026 in which malicious versions were each pulled within hours of publication.
- npm 12 changed dependency-source defaults:
allow-gitandallow-remoteboth default to"none". npm's reason: git dependencies rungitagainst a remote repo and may install configuration the project does not control. Current stable per the registry dist-tags endpoint is 12.0.2. - npm trusted publishing uses OIDC and implements the OpenSSF trusted-publishers standard. Configurations created before 2026-05-20 are automatically restricted to
npm publishonly. - npm staged publishing splits release into
npm stage publish, review vianpm stage listandnpm stage view, thennpm stage approve, where the maintainer is prompted for 2FA. Staging needs no second factor; approval does. It requires npm CLI 11.15.0 or later with Node 22.14.0 or later. - High-impact npm accounts enter a 72-hour read-only mode after an email change or use of a 2FA recovery code, introduced in June 2026.
One correction, because it matters for anyone writing this into an internal standard. GitHub's July 2026 supply-chain post states that npm v12 disabled install scripts by default. npm's own v12 configuration documentation shows ignore-scripts defaulting to false with no version-change note, and the v12 lifecycle-scripts page contains no such statement. I re-checked both on 2026-08-06 and could not reconcile them. I report the documentation rather than the blog summary and flag the discrepancy rather than picking a winner silently. Set ignore-scripts explicitly and do not rely on the default in either direction.
The through-line: a cooldown is a security control that trades latency for signal. It detects nothing. It buys time for other people's detection to fire before you consume the artifact — an unusual and underused shape of control, working precisely because the malicious-version lifetime distribution is short. It also fails in a specific way: a cooldown on version updates does nothing about a package already in your lockfile, and nothing about a malicious version nobody reports.
GitHub also announced, on 2025-09-22, an intent to deprecate classic npm tokens, cap granular token lifetime at seven days, and move from TOTP to FIDO for 2FA. No dates were committed. Do not plan against it as a schedule.
Stops being true when: attackers adapt by delaying malicious behaviour past the cooldown period. The control's effectiveness rests on an empirical distribution that the control itself creates pressure to change.
Notes on where checks run
28. Left versus right is the wrong axis. The variable is latency to signal — from the moment a decision is made to the moment the person who made it finds out. "Shift left" encoded a real observation badly. The observation is that the cost of changing a decision rises with time since it was made, mostly because the person who made it has stopped holding the context. A finding arriving four seconds after a keystroke is acted on; the identical finding arriving eleven days later in a ticket queue is a negotiation. Position on a pipeline diagram is a lossy proxy: a pre-commit hook running in ninety seconds has worse latency to signal than a PR check running in twenty, because the developer skips the hook.
Illustrative — the same check at four positions, annotated with time to signal and who receives it.
Alt text: a top-down diagram of four positions for the same security check. Editor or language server, latency in seconds, received by the author with full context, partial coverage. Pre-commit hook, seconds to minutes, author, staged changes only and bypassable. Pull request check, minutes, author and reviewer, full diff and enforceable. Post-deploy scan, hours to days, received by a queue, covering the full running estate. A dashed return edge from the post-deploy scan to the editor is labelled: context has decayed, fix cost is highest.
Stops being true when: the check requires runtime state that only exists after deployment. Dynamic testing and drift detection have an irreducible position and the latency argument does not move them.
29. The same check at four positions is not defence in depth — it is four copies of one finding. The editor sees the file being edited and nothing else; the post-deploy scan sees everything running, including what nobody in the pipeline touched. Running one rule set at every position produces the same finding four times with four identifiers, which is how a deduplication project becomes a security programme's largest line item. Place each check once, at the earliest position where it has enough context to be correct, then place a different, narrower check later to catch what the earlier one structurally cannot see — the dependency that entered through a base image, the configuration that only exists in the deployed manifest.
Stops being true when: the earlier check is advisory and the later one enforcing, and you chose that asymmetry deliberately. A legitimate design; it needs to be a decision rather than an accumulation.
30. A check that runs early and reports to a dashboard nobody owns has unbounded latency to signal, and it is worse than not running it. Not neutral — worse. It consumes build minutes, produces a number that appears in coverage reporting, and creates the belief that the class of issue is handled. The belief is the harm: a team that knows it has no static analysis will reason about that gap; a team whose static analysis reports into an unowned dashboard will not. The test is direct — pick a finding from the dashboard and ask who is expected to act on it and by when. If the answer requires a conversation to construct, the latency is unbounded.
Stops being true when: the dashboard is explicitly scoped as a research corpus rather than a control, feeding a quarterly analysis of finding classes. A real use, and it should be labelled so nobody mistakes it for coverage.
31. Whether to block is a latency decision, not a severity decision. The usual framing — block on critical, warn on high — attaches blocking to the number note 25 argued is unreliable. The better question: if this does not block, what is the realistic time until someone acts? If the answer is "within the day, because the owning team watches this channel", warn. If it is "it becomes a ticket", block, or accept that you have decided not to fix this class of issue. Blocking is expensive and its cost is paid by people who did not decide to impose it, which argues for blocking on a small, stable set — secrets in a diff, an unsigned artifact at a deployment boundary, a policy violation with no exception on file — and warning on everything whose false-positive rate you have not personally measured.
One sequencing trap, because this article makes it easy to fall into: do not remove a severity gate until the exposure and reachability data replacing it is actually flowing. The gate is a bad control; the gap between deleting it and standing up its replacement is worse, and the replacement takes far longer to build than the gate takes to delete.
Stops being true when: a regulatory or contractual obligation requires the block irrespective of the response path. The EU Cyber Resilience Act entered into force on 2024-12-10; reporting obligations apply from 2026-09-11 and main obligations from 2027-12-11. Obligations of that kind convert some warn decisions into block decisions by external force.
Notes on gates, exceptions and metrics
32. A gate that cannot be bypassed will be bypassed — around it, not through it. This is the most reliable prediction in the set. If a required check blocks delivery and there is no legitimate exception path, engineers find a path that does not pass through the check: a different pipeline, a manual deploy, a hotfix branch with relaxed rules, a "temporary" workflow that becomes permanent. The gate's metrics look excellent, because the traffic that would have failed no longer arrives.
The diagnostic is to measure the denominator rather than the pass rate. Count deployments to production from all sources and compare against deployments that passed the gate. A gate covering 70% of deploys with a 100% pass rate is weaker than one covering 100% with an 85% pass rate.
Stops being true when: the gate is structurally unavoidable — enforced at the only point that can produce the effect, such as the registry or the control plane, rather than in the pipeline. Which is the next note.
33. The gates that survive are the ones the pipeline structurally cannot satisfy on its own. npm's staged publishing is the shape. The pipeline runs npm stage publish; the package does not go live until a maintainer approves it with a second factor. No amount of pipeline privilege completes that step, because the factor is not available to the pipeline by construction. That is a different category from a policy check a sufficiently privileged workflow could skip.
The general form: put the control at the boundary where the effect occurs, and require an authentication factor the automated path does not hold. Admission control at the cluster is stronger than a policy check in CI for the same reason — CI can be bypassed, the API server cannot. Kubernetes ValidatingAdmissionPolicy has been stable since v1.30 and is CEL-based and in-process, removing the webhook availability failure mode that made earlier admission control operationally uncomfortable. Its limits belong in a separate piece.
The cost is real. Every such gate puts a human in a path that was automated, and that human becomes a bottleneck and a single point of failure — including at three in the morning during an incident, which is exactly when you will want to bypass the control you built to be unbypassable. Decide the break-glass path in advance and make it noisy rather than absent. Reserve these gates for effects that warrant them: publishing to a public registry, granting production access, changing the identity provider.
Stops being true when: the approval becomes routine enough to be performed without reading. An approval step that is always approved is a delay, not a control, and it is the most common end state of this pattern.
34. An exception without an expiry date and a named owner is not an exception. It is a policy change made by whoever filed it. Exception registers rot predictably: the initial entries have owners and dates, the middle entries have owners, the recent entries have neither, and nobody has read the top of the file in two years. At that point the register describes what the organisation actually permits — fine, except the policy document still says something different and the two are never reconciled.
The minimum viable record: what is excepted, why, who owns it, when it expires, and what would have to be true to close it. The expiry must be enforced by something that fires — a calendar entry belonging to a person, a job that reopens the finding, a policy engine that starts failing. An expiry date in a spreadsheet cell is a wish.
Stops being true when: the exception is permanent by design — a documented architectural decision that a control does not apply to a class of system. Those belong in the policy itself, which is the reconciliation step nobody does.
35. Composite scores are gameable by construction, and the best evidence is the maintainers admitting it. OpenSSF Scorecard (v5.5.0, released 2026-04-23) runs 18 default checks — 20 counting the two experimental ones, SBOM and Webhooks — and produces an aggregate. Its maintainers state that Scorecard is not intended to be a one-size-fits-all solution, that every step of producing the results is opinionated, and that many check scores are aggregated into a single score with multiple ways of arriving at the same score.
That last phrase is the definition of a gameable metric. If two projects with materially different posture can produce the same number, improving the number is not improving the posture, and any target set against it will be met by the cheapest path. This is not a criticism of Scorecard, which is a useful diagnostic and honest about its scope; it is a criticism of using it as a target.
Use the individual check results as a checklist instead of the aggregate as a score. The OpenSSF Security Baseline (v2026.02.19) is designed for this — three levels, and by deliberate construction the controls contain only MUST entries, no SHOULD. Control IDs to anchor a conversation: OSPS-BR-06.01 (release signing), OSPS-VM-01.01 (a disclosure policy with a stated response timeframe), OSPS-AC-01.01 (MFA on sensitive resources), OSPS-QA-06.01 (a CI test suite gating commit acceptance). Binary controls are harder to game because there is no partial credit to optimise toward.
Stops being true when: the composite is used purely for population-level comparison across thousands of projects, where individual gaming averages out. That is close to Scorecard's designed use.
36. DORA is five metrics now, not four, and both changes point at delivery reliability rather than speed. The current set is change lead time, deployment frequency, failed deployment recovery time, change fail rate, and deployment rework rate. "The four keys" is still the phrase everyone uses, including me until recently.
Two entries matter for security work. Mean time to restore was renamed failed deployment recovery time, narrowing it from a general availability measure to a specific question about your ability to undo a bad change — the capability a security incident in the delivery path depends on. And deployment rework rate is new; DORA defines it as the ratio of deployments that are unplanned but happen as a result of an incident in production. That is a precise description of a security hotfix, which makes it the one metric in the standard delivery set that counts your emergency security deploys without anyone having to label them as such.
Neither can be improved by reclassifying findings, the failure mode of every security-specific metric. Both can be improved by deploying less, which is the failure mode to watch instead — read them alongside deployment frequency or not at all.
Stops being true when: the metric set is revised again. It has been revised at least once; assume it will be again.
Principle versus tool
The test for a durable principle is that it can be stated without naming a product and survives the product being replaced. Everything in the left column was true in 2018 and is true now. Everything in the middle two columns has changed at least once.
| Principle | Implemented in 2018 by | Implemented in 2026 by | The principle fails if |
|---|---|---|---|
| Workloads should not hold long-lived credentials | Rotated IAM access keys in CI secrets | OIDC federation to a scoped cloud role | Short-lived tokens become mintable by an untrusted principal, making lifetime irrelevant |
| Build inputs must be identified immutably | Version pins in a lockfile | Commit-SHA pinning plus digest references | Registries offer verifiable immutable tags, making SHA pinning redundant rather than wrong |
| Artifacts must be attributable to a build | Signed release tarballs, manual key management | Sigstore keyless signing, SLSA provenance, in-toto attestations | Attribution stops mattering because artifacts are reproducible and verified by content alone |
| Findings must reach someone who can act | Email from the scanner | Routed issues with a deploy-authority owner | Systems self-remediate without human routing, which for dependency bumps is partly here |
| Policy must be enforced where the effect occurs | Manual review before deploy | Admission control and registry policy | The effect boundary moves somewhere policy cannot observe |
| Untrusted code must not run with production authority | Separate build accounts | Ephemeral runners, restricted tokens, fork-PR defaults | Isolation becomes strong enough that the distinction stops paying for itself |
| Exceptions must expire | A spreadsheet | A spreadsheet, still | Nothing. This one has not improved in eight years |
That last row is not a joke. Exception management is the one practice here whose 2026 implementation is materially the same as its 2018 implementation across most organisations I encounter, and it is the practice most likely to be the proximate cause when a known-and-accepted risk turns into an incident.
Anti-patterns
These rows are engineering judgment, not measurement. The "instead" column is a recommendation, not a finding.
| Anti-pattern | What it looks like | Why it appeals | What it costs | Instead |
|---|---|---|---|---|
| Scanner count as coverage | Six tools, reported as six controls | Countable, procurable, demonstrable to an auditor | Overlapping findings, deduplication overhead, no owner per class | Map each tool to a failure class it uniquely covers; retire overlaps |
| Severity-threshold gates | Block on critical, warn on high | Simple rule, no judgment required | Blocks on a signal largely unrelated to exploitation; teaches bypass | Gate on presence plus exposure plus known exploitation; warn elsewhere |
| Trusting the severity field's provenance | Triage branches on the CVSS score in the record | The field is populated, so nothing looks different | For most recent CVEs the score is now the vendor's own, unreviewed | Record which source produced the score; count findings running on unreviewed vendor scores |
| Rotation-cadence SLAs | 90-day rotation reported as a percentage | Produces a slide | Optimises the fallback control, ignores scope and mintability | Measure lifetime, scope, and how many principals can mint an equivalent |
| Exception register with no expiry | Growing spreadsheet, oldest entries unread | Cheap to add to, nobody has to say no | Becomes the de facto policy, diverging silently from the written one | Mandatory expiry enforced by a job that reopens the finding |
| Every finding is a ticket | One-to-one finding-to-issue automation | Feels like accountability | Queue grows faster than service rate; triage moves to the receiving team | Batch by component and owner; route only actionable findings |
| SHA pinning with no bump process | Pinned digests, last updated 14 months ago | Satisfies the hardening checklist | Misses upstream security fixes, including backported defaults | Automated digest bumps with review; a named owner for the action inventory |
| SBOM generated and never queried | SBOM attached to every release | Satisfies a contractual clause | Build-time cost, no security value | One query path that answers "where is this component" in minutes |
| Provenance generated and never verified | Attestations on every artifact | Green compliance answer | Build-time cost, no enforcement, false confidence | One enforcement point that has demonstrably rejected something |
Implementation checklist
The 2018 agenda ended with "DevSecOps Final Checklist". This is its successor, ordered by what tends to be both under-defended and cheap to change. Each line carries the evidence that demonstrates it is done, because a checklist without evidence requirements is a survey.
Pipeline identity
- Enumerate every cloud role assumable from CI, and the trust condition permitting it. Evidence: the list, with each
subcondition quoted. - Confirm no trust condition uses a wildcard spanning repositories. Evidence: the policy documents.
- Determine which repositories emit the immutable subject format and which the name-based format. Evidence: a decoded token claim from one repository of each vintage.
- Enumerate refs and environments that legitimately deploy, before tightening any
subcondition to an exact match. Evidence: the list, cross-referenced against the conditions written. - Confirm no long-lived cloud access key exists in any CI secret store. Evidence: the secret inventory, with each entry's type.
- Retire OIDC thumbprint rotation runbooks while keeping a valid thumbprint configured. Evidence: the current AWS documentation, dated, plus the provider configuration.
Untrusted input
- List every workflow triggered by
pull_request_targetorworkflow_run. Evidence: the list, with the checkout configuration for each. - Confirm none passes
allow-unsafe-pr-checkoutwithout a recorded decision. Evidence: the grep result, plus the decision record for any hit. - Confirm self-hosted runners are not attached to public repositories, or record the accepted risk with its compensating controls. Evidence: the runner-to-repository mapping.
- Read your CI platform's security changelog for the period since your last review. Evidence: the dated list of changes and their applicability.
Third-party actions and dependencies
- Pin third-party actions to full-length commit SHAs. Evidence: the workflow files.
- Confirm a process bumps those SHAs and has run in the last 60 days. Evidence: the merged bump pull requests.
- Enable a cooldown on non-security version updates; confirm security updates are exempt. Evidence: the configuration.
- Set
ignore-scriptsexplicitly rather than relying on a default in either direction. Evidence: the config file. - Move package publishing to trusted publishing (OIDC) where the registry supports it. Evidence: the publish configuration, and the absence of a publish token.
Provenance
- Identify one enforcement point that rejects artifacts without valid provenance. Evidence: a log line showing a rejection, produced by deliberately building outside the sanctioned path.
- Confirm every verification command or policy asserts an expected identity and issuer. Evidence: the command or policy text.
- Record which SLSA track and level you claim, with the spec version. Evidence: the claim, linked to the version cited.
- If using private-repository attestations, record that there is no transparency log and confirm your threat model does not depend on one. Evidence: the decision record.
Findings and ownership
- For each finding source, name the field holding the deploy-authority owner. Evidence: a sample finding with that field populated.
- Measure what fraction of open findings are assigned to a team that can close them unilaterally. Evidence: the fraction, with the query used.
- Record which source produced the severity your triage branches on, and count findings running on a CNA-supplied score with no NVD review. Evidence: the count, with the query.
- Confirm your triage logic has a defined branch for findings whose severity has no NVD-attributed vector. Evidence: the logic, and a worked example on a deferred CVE.
- Confirm no scoring formula multiplies EPSS by CVSS. Evidence: the formula.
Gates and exceptions
- Measure gate coverage as a fraction of all production deployments, from all paths. Evidence: the two counts and their sources.
- Confirm every exception has an owner, an expiry, and a closing condition. Evidence: the register, with a count of entries missing any field.
- Confirm exception expiry is enforced by something that fires. Evidence: an instance of it firing.
- Confirm every unbypassable human-approval gate has a documented, noisy break-glass path. Evidence: the path, and the alert it raises.
Where these notes do not apply
Every note carries its own falsification line. Four conditions cut across all of them.
Team size. Notes 1 to 5 and 29 to 34 assume enough separate teams that routing is a problem. Below roughly fifteen engineers, code authority and deploy authority are the same people, the two-owner-field argument is overhead, and the honest advice is different: skip the ownership section, spend the effort on notes 6 to 18, revisit when a second team ships independently.
Platform. What transfers to GitLab, Buildkite, Jenkins or self-hosted CI is the structure: CI holds production authority and runs untrusted input (6); federated identity moves the secret into a trust policy nobody reviews (7); pinning creates a maintenance obligation (10); a gate the pipeline can satisfy alone will be satisfied alone (32, 33). What does not transfer is every specific — subject claim formats, backport dates, input names, cooldown defaults. Do not translate a GitHub control into a supposed equivalent elsewhere without reading that platform's documentation, and expect the failure modes to differ rather than only the syntax.
GitHub Enterprise Server. The immutable subject format in note 8 is explicitly unavailable on GHES, and several other 2026 defaults arrive on a different schedule from GitHub.com, if at all. GHES readers should treat notes 8, 10 and 13 as a description of where the platform is going rather than of what they run.
Single account versus organisation. Notes 7 and 8 assume you can enumerate trust policies across accounts. In a single-account estate that audit is an afternoon; across a large organisation it is a discovery problem first, and the practical failure is not a bad condition but an unknown role in an account nobody maps. Start with the inventory — the condition review is worthless without it.
Two further exclusions. This article does not treat runtime or workload security beyond the deployment boundary; everything stops where the artifact is accepted. And it does not address the case where your build system is the product you sell, which changes the threat model enough that notes 10, 12 and 33 should be reconsidered from scratch.
What has changed since 2018
The original agenda scored against current practice. "Held" means the framing was right and remains right. "Reversed" means I would now argue the opposite.
| 2018 agenda item | What it meant then | Verdict now |
|---|---|---|
| DevSecOps Introduction | Establishing that security belongs in the delivery pipeline | Held, and no longer needs arguing. This article has no definition section for that reason |
| Key Challenges, Recommendations | Tooling gaps and integration friction | Partly held. The challenges are now ownership and signal quality; the tooling gaps largely closed |
| DevSecOps Analysis | Assessing current state against a target state | Reversed in form. Maturity assessment produces a plan, not a change. Assess against attacker-reachable paths instead |
| DevSecOps Core Practices | A named set of practices to adopt | Held, with the caveat that the practice list is stable and every implementation has been replaced at least once |
| Pipeline for Application and Infrastructure Security | Getting checks into the build for code and infra | Held and superseded. The pipeline is now itself a high-value target, which the 2018 framing treated as the safe place from which checks are run |
| Security Tools Selection Tips | How to choose scanners | Reversed. Tool selection is the least durable decision in the set. Every tool that would have been recommended in 2018 has been replaced, deprecated, or had its defaults inverted |
| Implementation Strategy | Sequencing an adoption programme | Partly held. Sequencing still matters; the sequence changed. Identity boundaries now come before scanner coverage |
| Final Checklist | A closing artifact to act on | Held. The checklist above is its successor, with evidence requirements added |
The pattern in that table is the thesis. The items that held describe a relationship — between a decision and who finds out, between an artifact and who can vouch for it, between a finding and who can close it. The items that reversed named a category of product.
Editor's note
Dated 2026-08-06.
The 2018 post referenced above was written by the same author. Its article:published_time is 2018-07-29T16:21:32+00:00 and its article:modified_time is 2018-09-12T19:03:21+00:00; direct fetches of the live URL failed, so the text came from the Internet Archive. The embedded SlideShare deck is embed id 107904125. Nothing in this article characterises the contents of that deck.
Carried forward: the subject, the organisational framing, and the four themes implied by the agenda. Deliberately dropped: the maturity-assessment framing, the definitional introduction, and the tool-selection guidance. Added: everything else.
Limitations of these notes
Measured versus asserted. The Q2 2026 NVD figures are a complete census of the 20,871 CVEs published in that window, retrieved from the NVD API on 2026-08-06. The KEV figures come from the JSON feed at catalogVersion 2026.08.06. Notes 1 to 5, 28 to 34 and the anti-patterns table are engineering judgment argued from mechanism, with the external support named in "How much of this is measured" and nowhere else.
Vendor-stated and not independently verified: GitHub's SLSA Build Level 2 claim for artifact attestations, the private-repository transparency-log behaviour, the Dependabot cooldown rationale drawn from 21 incidents, and the npm account read-only behaviour. All documented by the vendor; none tested here.
Unresolved conflict between sources: the npm v12 install-script default. GitHub's blog says scripts were disabled; npm's v12 configuration documentation shows ignore-scripts defaulting to false. Reported as a discrepancy rather than resolved.
Dated: FIRST's "around 5% of all vulnerabilities are ever exploited" figure comes from an article whose underlying data is dated to March 2022, over a much longer window than the half-year arithmetic in note 24. DORA's cultural-predictor finding is from 2022.
Deliberately absent: any characterisation of the 2018 SlideShare deck; any statement of VEX status values or CISA BOD 22-01 deadlines, because those source documents returned HTTP 403 and I will not state them from memory; and any naming of specific 2025–2026 npm worm incidents, because I could not reach a primary source. The registry-hardening discussion describes the pattern without naming incidents, which is the level of confidence the evidence supports.
References
Standards and specifications
- SLSA v1.2 specification — status Approved, two tracks.
- SLSA v1.2 build requirements — Build L1 to L3, hosted and isolation wording, no L4.
- SLSA v1.2 source requirements — Source L1 to L4 including two-party review.
- SLSA v1.2 what's new — changes from v1.1.
- SLSA v1.1 levels, marked retired — retirement notice.
- in-toto Attestation Framework specification — spec v1.2, Statement type URI.
- NIST SP 800-218, SSDF v1.1 — practice groups, published 2022-02-03.
- NIST SP 800-218 full text (PDF) — RV.2 wording.
- CVSS v4.0 specification document — document version 1.2, nomenclature, base-score caveat.
- EPSS overview — the 30-day probability definition, published daily.
- EPSS frequently asked questions — scope limits; the statement against multiplying EPSS by CVSS.
- FIRST on EPSS probability and percentile bins — the "around 5%" figure; underlying data dated March 2022.
- OpenSSF Security Baseline and the 2026-02-19 version — three levels, MUST-only controls, control IDs.
- OWASP Top 10 CI/CD Security Risks — v1.0 stable October 2022, unrevised.
- OWASP Top 10:2025 — current categories, A03 Software Supply Chain Failures.
Government and regulator sources
- NIST National Vulnerability Database programme page — the 2026-04-15 enrichment change, priority categories, backlog handling, volume figures.
- NVD CVE API — publication volumes and the Q2 2026
vulnStatusand CVSS-source census in note 23. Queried 2026-08-06. - CISA Known Exploited Vulnerabilities JSON feed — catalogVersion 2026.08.06, count 1,661, additions by year.
- European Commission on the Cyber Resilience Act — entry into force and obligation dates.
Platform and vendor documentation
- GitHub Actions OIDC reference — issuer, legacy and immutable subject formats, the condition requirement.
- GitHub Actions OpenID Connect concepts — immutable subject applicability and GHES exclusion.
- GitHub Actions secure use reference — SHA pinning,
pull_request_target, self-hosted runner guidance. - GitHub workflow syntax: permissions — permission cascade semantics.
- GitHub changelog: safer pull_request_target defaults for checkout — v7 GA, the 2026-07-20 backport,
allow-unsafe-pr-checkout. - GitHub Actions changelog index — the June and July 2026 changes and their dates.
- GitHub artifact attestations concepts — SLSA level claim, private-repository transparency-log behaviour.
- Using GitHub artifact attestations — verification commands and flags.
- GitHub secret scanning introduction — partner validation, provider notification, remediation guidance.
- GitHub on disrupting supply-chain attacks on npm and GitHub Actions — read-only account mode, staged publishing, the npm v12 summary this article disputes.
- GitHub's plan for a more secure npm supply chain — token and 2FA direction, no committed dates.
- The case for a cooldown: Dependabot version updates — three-day cooldown, security-update exemption, 21-incident rationale.
- npm configuration reference, v12 —
allow-git,allow-remote,ignore-scriptsdefaults. - npm trusted publishers — OIDC publishing, the 2026-05-20 configuration restriction.
- npm staged publishing — stage, review and 2FA approval steps, version requirements.
- Sigstore cosign verification documentation — identity and issuer constraints,
--check-claimsbehaviour. - Sigstore: sigstore.dev and Rekor evolution — 2026-06-28; public good instance remaining on Rekor v1.
- Sigstore: Rekor v2 GA — general availability 2025-10-10.
- AWS IAM security best practices — federation and temporary-credential headings.
- Creating an AWS OIDC identity provider — thumbprints as fallback, the three fallback conditions, the one-to-five limit.
- Turning on AWS Secrets Manager rotation — the
iam:CreateRolewarning. - Go vulnerability management documentation — the reachability model and work-in-progress status.
- Kubernetes ValidatingAdmissionPolicy reference — stable since v1.30, CEL-based, in-process.
- Kubernetes releases — supported versions and end-of-life dates.
Projects and research
- OpenSSF Scorecard repository — v5.5.0, 18 default checks (20 including experimental), the maintainers' scope caveats.
- DORA metrics guide — the five metrics, the recovery-time rename, the rework-rate definition.
- DORA research index — the 2022 cultural-predictor finding.
- CNCF Platform Engineering Maturity Model — the genre convention this piece declines.
The original
- Shifting strategy from DevOps to DevSecOps, 2018 (Internet Archive snapshot) — snapshot dated 2022-05-19.
Every reference above was retrieved and confirmed to support the claim attached to it on 2026-08-06.
Validity and revision
Verification date: 2026-08-06. Every version number, specification revision, documentation quotation and dataset measurement was checked against a primary source on that date. Where a source could not be retrieved, the article says so rather than filling the gap.
| Claim | Why it will age | Review by |
|---|---|---|
| SLSA at v1.2 | The version moved twice in the recent window | 2027-02 |
| GitHub Actions defaults (checkout v7, trigger controls, cache behaviour) | Four material default changes in a ten-week window | 2026-11 |
| npm 12 defaults and stable version (12.0.2) | v12 was days to weeks old at publication | 2026-11 |
| NVD enrichment policy and the Q2 2026 census proportions | Policy changed 2026-04-15; records can also be re-enriched later | 2026-11 |
| The 22.9% carrying an NVD-attributed CVSS vector | Directly downstream of NVD capacity; the most volatile number here | 2026-11 |
| KEV count of 1,661 (catalogVersion 2026.08.06) | Changes continuously | Treat as a dated snapshot |
| Sigstore public-good instance on Rekor v1 | Stated as "for the foreseeable future", not permanently | 2026-12 |
| EU CRA reporting obligations from 2026-09-11 | The date falls five weeks after publication | 2026-10 |
| Kubernetes, OPA, Trivy, OpenTofu versions | Ordinary release cadence; Kubernetes 1.34 reaches end of life 2026-10-27 | 2027-02 |
| npm token and 2FA deprecation direction | Announced without committed dates; must not be treated as scheduled | 2026-12 |
| OWASP Top 10:2025 category numbering | New enough that the numbering is worth re-checking | 2027-06 |
Recommended review date: 2026-11-30. The GitHub Actions defaults, npm 12 behaviour and NVD enrichment proportions will be wrong first. The ownership arguments carry no version dependency and should hold longer than anything else here — which is also why they are the hardest part of this article to check.
- devsecops
- ci-cd-security
- supply-chain-security
- slsa
- vulnerability-management
- secrets
- policy-as-code
- provenance
- pipeline-security
The service behind this work
Security automation and detection engineering
Vendors sell detection. We build fixes — detections as code, AI-assisted triage, and remediation pipelines engineered inside your AWS and Google Cloud accounts, in your repositories, owned by your team when we leave.
Related reading
All articles →Kubernetes guardrails with OPA Gatekeeper
Writing the constraint is the easy part. An operational guide to running Gatekeeper as admission control you can afford to have fail: fail-open defaults, cold caches, audit blind spots, staged rollout, and the silent-drop bugs that make a green CI run mean nothing.
· 63 min read
AI governance for engineers, not lawyers
Translate AI governance into systems you can build: inventory reconciled against runtime telemetry, deployment gates that block, human oversight you can measure, and evidence that survives review. With EU AI Act dates as amended in July 2026.
· 77 min read
Automated remediation on AWS
An engineering guide to automated security remediation on AWS: when to automate, why an organization trail plus a service control policy removes most of the CloudTrail-disruption problem before any code runs, and how to build a loop that is idempotent, bounded, reversible and switchable-off.
· 55 min read