Chapter 3 · Part I: Post-Quantum Cryptography and Zero-Trust Security
Software Supply Chain and Zero-Trust Security Architecture
An attacker who compromises a dependency, a build system, or a registry reaches every downstream consumer at once. This chapter covers the inventory that makes a vulnerability a query, the provenance that ties an artifact to its source, the kernel controls that assume a compromise already happened, and the identity system that removes standing credentials.
In this chapter
3.1 Software Bill of Materials and Governance
A software bill of materials, or SBOM, is a machine-readable inventory of everything in an artifact: components, versions, suppliers, licenses, and the relationships between them. Its value is operational rather than theoretical. When a critical vulnerability is announced on a Friday evening, an organization with SBOMs runs a query and an organization without them starts an investigation.
The regulatory pressure is now real. The EU Cyber Resilience Act and several United States federal requirements make a bill of materials a condition of supply rather than a good practice. The conversation in most organizations moved from whether to produce one to how.
The two formats
CycloneDX came from the OWASP community and was designed for security use. It is compact, it has first-class support for vulnerabilities, for attestations, and for service dependencies, and it extends to hardware and to machine learning models. SPDX came from the Linux Foundation and was designed for license compliance, and it became an ISO standard. Version 3 broadened it substantially.
Both are adequate. Choose by consumer: your customers and your tooling determine which one you need to emit, and emitting both is cheap because converters exist. The formats are not where the difficulty lies.
Where the difficulty lies
Generating a bill of materials is easy. Generating an accurate one is not, and four problems account for most inaccuracy.
The first is where it is generated. A bill of materials produced from a manifest file lists what the developer declared. One produced from a lock file lists what the resolver chose. One produced from the built artifact lists what is actually present, including statically linked libraries, vendored code, and files copied in by a build script. The three differ, and only the third describes what ships.
The second is transitive depth. A direct dependency list is a small fraction of the real graph. A typical application has tens of direct dependencies and hundreds to thousands of transitive ones, and most vulnerabilities arrive through the transitive set.
The third is the container. An application bill of materials covers the application. The image also contains a base operating system with its own packages, and a vulnerability in the base image is just as exploitable. A complete picture needs both, assembled into one document.
The fourth is drift. A bill of materials describes a moment. The artifact does not change and the vulnerability data does, so a document generated at build time is a snapshot whose risk assessment is stale within days.
SOURCE (manifest) BUILD (lock file) ARTIFACT (binary scan)
express: ^4.18.0 express: 4.18.2 express 4.18.2
lodash: ^4.17.0 lodash: 4.17.21 lodash 4.17.21
+ 847 transitive + 847 transitive
+ libssl 3.0.13 (base image)
+ zlib 1.2.13 (static link)
+ vendored xz 5.4 (copied in)
+ a JAR shaded (relocated)
12 entries 859 entries 1,240 entries
Only the third column describes what an attacker can reach.
The vendored and statically linked entries are the ones that no
manifest-based tool reports, and they are where the incidents come from.
Figure 3.1 Where the bill of materials comes from changes what it says.
Making it useful
An inventory without analysis is a file nobody opens. Four capabilities turn it into a control.
Continuous re-evaluation matters more than generation. Store the bills of materials in a queryable database keyed by artifact digest and by deployed environment, and re-run the vulnerability match whenever the advisory data updates rather than only at build time. The question to answer in seconds is "which running services contain this component at this version".
Reachability analysis is what makes the output actionable. A vulnerability in a function the application never calls is not exploitable, and the industry average is that a large majority of reported vulnerabilities in dependencies are unreachable. Static reachability analysis distinguishes them, which turns a list of four hundred findings into a list of a dozen that matter.
Exploitability exchange documents the judgment. The VEX format lets a supplier state, per vulnerability per product, whether it is affected and why. That is how "we ship this library, the vulnerable code path is unreachable, here is the justification" becomes a machine-readable statement rather than an email.
Policy gates make the analysis binding. A build fails, or a deployment is refused, when the artifact contains a component above a severity threshold with a reachable path and no accepted exception. The gate must have an exception mechanism with an owner and an expiry, because a gate with no exception path gets disabled the first time it blocks a release.
SBOM practices that survive contact with a release schedule
Generate from the built artifact, not from the manifest, and include the base image. Attach the document to the artifact as a signed attestation, so it travels with the digest rather than living in a separate system that drifts. Re-scan continuously against current advisory data rather than only at build. Add reachability analysis before you add more scanners, because the second scanner adds findings and reachability removes the ones that do not matter. And set the policy gate at a threshold the organization will actually hold, because a gate that is routinely overridden trains everyone that gates are advisory.
3.2 Cryptographic Artifact Provenance
A bill of materials says what is in an artifact. Provenance says where the artifact came from: which source commit, which build system, which configuration, and which inputs. Without it, an attacker who compromises the build system produces an artifact that passes every content check, because the content is exactly what the attacker intended.
SLSA levels
Supply-chain Levels for Software Artifacts defines a graduated set of requirements for build integrity. The value of the framework is that it separates the steps, so an organization can be at a defensible level rather than at "we tried".
Table 3.1: SLSA build track levels
| Level | Requirement | Prevents |
|---|---|---|
| 0 | nothing | nothing |
| 1 | provenance exists and describes the build | mistakes, and nothing adversarial |
| 2 | provenance is signed by a hosted build service | forgery by a developer or a laptop |
| 3 | the build service is hardened, builds are isolated, provenance is unforgeable by the build itself | a compromised build step tampering with its own provenance |
| 4 | under development | hermetic and reproducible builds, two-party review |
The jump from level 2 to level 3 is where the real security is. At level 2, a build step that the attacker controls can influence the provenance, because the build and the provenance generation share a trust domain. At level 3 the provenance is produced by the platform outside the build's reach, so a compromised build step can produce a malicious artifact and cannot produce a provenance statement that claims it came from a different source.
Provenance in practice
Provenance is expressed as an in-toto attestation: a statement about a subject, identified by digest, with a predicate describing the claim. A DSSE envelope wraps and signs it.
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "ghcr.io/example/api",
"digest": {"sha256": "3f8a...c41d"}
}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/heads/main",
"repository": "https://github.com/example/api",
"path": ".github/workflows/release.yml"
}
},
"resolvedDependencies": [
{"uri": "git+https://github.com/example/api@refs/heads/main",
"digest": {"gitCommit": "a41b9e2c..."}}
]
},
"runDetails": {
"builder": {"id": "https://github.com/actions/runner/github-hosted"},
"metadata": {
"invocationId": "https://github.com/example/api/actions/runs/1839",
"startedOn": "2026-09-14T10:22:31Z"
}
}
}
}
The consumer-side policy is the half that matters, and it is short. Before admitting an artifact, require five things. A provenance statement exists for its exact digest. The signature is valid. The builder identity is one of an expected set. The source repository and branch are expected. And the build configuration path is the expected one.
That last clause catches a real attack. An attacker with write access to a repository adds a new workflow file that builds a malicious artifact from the legitimate source. The provenance is genuine, the repository is correct, the builder is correct, and the workflow path is not the one the policy expects.
Keyless signing
Traditional signing needs a key, which needs storage, rotation, distribution, and revocation, and which leaks. Sigstore removes the long-lived key entirely.
The flow uses a short-lived certificate bound to an identity. The signer authenticates with an OpenID Connect token, which in a continuous integration system is the workload identity the platform issues, not a human credential. A certificate authority issues a certificate valid for about ten minutes, with the identity in the subject alternative name. The signer signs, and the certificate, the signature, and the artifact digest go into an append-only transparency log. The private key is discarded.
CI workflow (no stored secret)
| 1. request an OIDC token from the CI platform
| identity: repo:example/api, workflow: release.yml, ref: main
v
Fulcio (certificate authority)
| 2. validate the token, issue a ~10 minute certificate
| SAN = the workload identity from the token
v
signer
| 3. sign the artifact digest with the ephemeral private key
| 4. discard the private key
v
Rekor (transparency log, append-only, Merkle tree)
| 5. record: digest, signature, certificate, timestamp
| returns an inclusion proof
v
VERIFIER, later
6. certificate chains to the Fulcio root
7. certificate SAN matches the expected identity policy
8. signature is valid over the digest
9. entry is in the log, and the signature time falls inside
the certificate's short validity window
Step 9 is what makes a ten-minute certificate usable forever: the log
proves the signature happened while the certificate was valid.
Step 7 is the security: without an identity policy, any valid
Sigstore signature from anyone would pass.
Figure 3.2 Keyless signing, and what each component contributes.
The transparency log carries a second benefit beyond making short certificates work. It is public and append-only, so an attacker who obtains the ability to sign cannot do so invisibly. Monitoring your own identities in the log detects a compromise that no amount of key protection prevents.
A signature without an identity policy proves nothing useful
The most common Sigstore mistake is checking that a signature is valid and stopping there. Any signature produced by any identity through the public infrastructure is valid. The control is the identity policy: this artifact must be signed by this repository, this workflow file, and this branch. Without those clauses, an attacker signs a malicious image with their own account and it passes. Encode the policy in the admission controller, and test it by attempting to admit an artifact signed by the wrong identity.
Reproducible builds
A reproducible build produces bit-identical output from the same inputs. It is the strongest available answer to a compromised builder, because an independent rebuild by a second party detects tampering that no signature can.
Four sources of non-determinism account for nearly all failures: embedded timestamps, embedded build paths, non-deterministic ordering from file system iteration or map traversal, and embedded environment such as hostname, user, or locale. The fixes are mechanical. Honor the standard reproducible-build timestamp variable, strip or normalize paths, sort every list before emitting it, and build in a container with a pinned base and a fixed environment.
Section 2.2 gave the other reason to care. A confidential computing measurement is a hash of the built image, and an expected-value policy is only maintainable when the build is reproducible. The two chapters meet here: the attestation policy that gates key release depends on the build system that produces predictable measurements.
3.3 eBPF-Enforced Kernel Microsegmentation
Provenance and attestation defend the path into production. Runtime controls assume that something got in anyway, and they limit what it can do next.
eBPF is the mechanism that made those controls practical. Volume I covers the verifier, the maps, and the attach points. This section covers what to attach and why.
Segmentation by identity, not by address
A network policy written in addresses is a policy about nothing in a cluster that recycles addresses every few minutes. An address that belonged to a payments pod at noon belongs to a batch job at ten past.
An eBPF datapath assigns a numeric identity derived from workload labels, distributes the label-to-identity mapping, and carries or resolves the identity per packet. A policy decision becomes a lookup on an identity pair in a hash map, which is constant time and which stays correct when an address is reused.
Egress policy matters more than ingress, and teams consistently invert that. Ingress policy limits who can reach a compromised workload. Egress policy limits what the compromised workload can reach, which is the cloud metadata service, the internal database, the secret manager, and the attacker's command and control endpoint. Default-deny egress with an explicit allowlist is the single highest-value control per hour of work in a container platform.
Blocking at the system call boundary
A seccomp filter restricts which system calls a process can make. It is evaluated on every call, it is cheap, and it is static: the policy is installed at process start and cannot consider context.
An eBPF LSM program attaches to kernel security hooks and can consider context: which container, which file, which network peer, what the process did previously. It also loads and unloads at runtime, so a policy changes without restarting the workload.
The two compose rather than compete. Use seccomp for the broad reduction, which is removing the 250 or so system calls that a normal service never makes. Use eBPF LSM for the specific rules that need context, which is a small number of high-value cases.
/* Refuse to execute anything from a writable directory inside a
container. A large share of container attacks drop a binary into
/tmp and run it, and this one rule stops that whole class. */
SEC("lsm/bprm_check_security")
int BPF_PROG(block_exec_from_tmp, struct linux_binprm *bprm, int ret)
{
if (ret != 0)
return ret; /* an earlier LSM already refused */
struct task_struct *task = (void *)bpf_get_current_task_btf();
u64 cgid = bpf_get_current_cgroup_id();
/* Only enforce inside cgroups that the control plane marked. */
u8 *enforced = bpf_map_lookup_elem(&enforced_cgroups, &cgid);
if (!enforced)
return 0;
char path[64];
bpf_probe_read_kernel_str(path, sizeof(path),
bprm->filename);
if (starts_with(path, "/tmp/") || starts_with(path, "/dev/shm/")) {
bpf_printk("blocked exec from writable dir: %s", path);
return -EPERM; /* the exec fails */
}
return 0;
}
Detection alongside enforcement
Enforcement blocks known-bad actions. Detection notices unexpected ones, and it is what catches the attack that no rule anticipated.
Four signals carry most of the value. A shell spawning inside a container is almost never legitimate in a production service. An outbound connection to an address outside the policy is either a misconfiguration or an exfiltration attempt. A write to a path that is meant to be read-only indicates persistence. And a process reading the cloud metadata service or a credential file that it never read before indicates credential theft.
The operational discipline matters more than the rule set. Start in detection mode, collect for a full business cycle including month-end and any seasonal pattern, tune the rules that fire on legitimate behavior, and only then enable blocking. A rule set deployed in enforcing mode on day one blocks a legitimate operation, and the report arrives as an outage.
Table 3.2: runtime controls, cost, and what each one stops
| Control | Overhead | Stops | Misses |
|---|---|---|---|
| Seccomp profile | 1 to 5 percent | most of the kernel surface | anything using an allowed call |
| Capability drop | 0 | privileged operations | anything not needing a capability |
| eBPF LSM policy | 1 to 3 percent | context-dependent actions | what the policy did not anticipate |
| Identity network policy | under 1 percent | lateral movement, exfiltration | in-policy abuse |
| eBPF runtime detection | 1 to 3 percent | unexpected behavior, after the fact | anything that looks normal |
| Read-only root file system | 0 | persistence on disk | in-memory only techniques |
| Non-root plus no_new_privs | 0 | privilege escalation by setuid | escalation by kernel bug |
The last column is the honest one. Every control has a gap, which is why they are layered. The combination in that table turns a straightforward container compromise into a difficult one, and it does not turn it into an impossible one.
3.4 Ephemeral Identity Systems
A static credential is the most reliable attack path in a modern system, because it is long-lived, it gets copied into configuration, it appears in logs, and a leak grants access until somebody notices.
Why credentials leak
They leak because of where they have to live. Environment variables appear in process listings, crash dumps, container inspection output, and continuous integration logs. Configuration files get committed. Secret managers solve the storage problem and not the distribution problem, because the application still needs a credential to authenticate to the secret manager.
That last point is the bootstrap problem, and it is the one that workload identity solves. Every secret management scheme needs a first secret, and the only way out is a credential that derives from what the workload is rather than from something it holds.
Attestation-derived identity
SPIFFE defines the naming and the document format. A SPIFFE ID is a URI such
as spiffe://example.com/ns/payments/sa/charge-service. A SPIFFE Verifiable
Identity Document, or SVID, carries that identity in an X.509 certificate or a
JWT. SPIRE is the reference implementation, and both are CNCF projects.
The interesting part is attestation, and it happens at two levels.
Node attestation proves the machine. A SPIRE agent presents evidence to the server: a cloud instance identity document signed by the provider, a TPM quote, or a Kubernetes projected service account token. The server checks it against a registration policy and issues the agent an identity.
Workload attestation proves the process. The agent receives a request over a Unix domain socket and reads the peer credentials to learn the calling process identifier. It then inspects that process: its cgroup, its container image digest, its Kubernetes service account, its binary path, and its parent. It matches those selectors against registration entries and issues the matching identity.
SPIRE SERVER (holds the registration entries and the CA)
^ node attestation: instance identity document, TPM quote,
| or a projected service account token
|
SPIRE AGENT (one per node, DaemonSet)
^ workload attestation over a Unix socket:
| peer credentials give the PID
| the agent reads /proc/<pid>: cgroup -> container -> pod
| selectors: k8s:ns:payments, k8s:sa:charge,
| k8s:container-image:sha256:9f2a...
|
WORKLOAD calls the SPIFFE Workload API, presents no credential
|
v receives an X.509-SVID:
SAN = spiffe://example.com/ns/payments/sa/charge-service
TTL = 1 hour, rotated at ~30 minutes
plus the trust bundle for validating peers
No secret is transmitted, stored, or configured anywhere in this
picture. A stolen SVID expires within the hour, with no revocation
infrastructure needed.
Figure 3.3 Identity derived from what a workload is.
The security property is worth stating precisely. A stolen credential normally grants access until somebody notices and revokes it, which is measured in days or in never. A stolen SVID grants access for at most the remaining lifetime, which is under an hour. That collapse in blast radius is the entire argument, and it holds without any revocation infrastructure, which is the part that traditional public key infrastructure never made work at scale.
Extending it beyond the mesh
The same identity works for more than service-to-service authentication, and each extension removes another standing credential.
Database access uses a short-lived credential issued against the workload identity, or a certificate that the database accepts directly. The static database password disappears.
Cloud resource access uses identity federation: the workload presents a JWT SVID, the cloud provider's identity service exchanges it for short-lived cloud credentials under a trust policy. The static access key disappears.
Continuous integration uses the platform's workload identity to obtain deployment credentials and signing certificates, which is what makes the keyless signing in Section 3.2 possible. The stored deployment secret disappears.
Secret manager access uses the identity as the authentication factor, which removes the bootstrap secret and closes the loop that opened this section.
A credential elimination programme, in the order that works
Start with an inventory of standing credentials: every static key, token, password, and certificate, with its owner, its scope, and its last rotation date. Sort by blast radius rather than by count. Deploy workload identity to one non-critical service first and run both paths in parallel until the new one is proven. Migrate by blast radius: the credential that grants the most access goes first. Alert on any use of a credential that is due for retirement, because that is how you find the caller nobody documented. And treat the last 10 percent as the hard part, because it is the systems that cannot be changed, and each one needs a specific decision rather than a general policy.
What remains hard
Three things resist this model and deserve honesty.
A legacy system that only accepts a static password cannot be given a workload identity. The mitigation is a proxy that holds the static credential inside a tight boundary and authenticates its callers by identity, which does not eliminate the credential and does reduce the number of places it exists.
An external third-party API issues its own long-lived key and does not federate. The mitigation is the same proxy pattern plus aggressive rotation, and the credential remains a standing one.
A short lifetime raises the cost of an availability failure in the identity infrastructure. If SPIRE is unavailable for longer than the SVID lifetime, every workload loses its identity at once and the whole system stops. That is a correctness-preserving failure and it is still an outage. Run the identity control plane with the availability of a critical dependency, keep the lifetime long enough to absorb a realistic recovery time, and test the failure deliberately rather than discovering it.
Sources and further reading
The CycloneDX and SPDX specifications, and the OpenVEX and CSAF VEX documentation for exploitability statements. The SLSA specification, version 1.1, particularly the build track requirements and the threats it enumerates. The in-toto attestation specification and the DSSE envelope format. The Sigstore documentation for Fulcio, Rekor, and Cosign, and the keyless signing flow. The Reproducible Builds project documentation for the sources of non-determinism. The SPIFFE and SPIRE documentation, particularly node and workload attestation. The Cilium and Tetragon documentation for eBPF policy and runtime detection, and the Falco rule set as a starting point for detection rules.
What this chapter established
- A bill of materials generated from a manifest lists what a developer declared. Only one generated from the built artifact lists what an attacker can reach.
- Re-evaluate continuously against current advisory data. The artifact does not change and the vulnerability data does.
- Reachability analysis turns hundreds of findings into the dozen that are exploitable. Add it before adding a second scanner.
- Provenance says where an artifact came from. Content checks cannot detect a compromised build system, because the content is exactly what the attacker intended.
- The jump from SLSA level 2 to level 3 is where the security is: provenance becomes unforgeable by the build steps themselves.
- Check the workflow path in the provenance policy, not only the repository, because an attacker with write access adds a new workflow that builds from legitimate source.
- Keyless signing replaces a long-lived key with a ten-minute certificate bound to a workload identity, made durable by a transparency log entry.
- A valid Sigstore signature proves someone signed it. Only an identity policy proves the right party did.
- Reproducible builds let an independent party detect builder tampering, and they are what makes a confidential computing measurement allowlist maintainable.
- Network policy by identity stays correct when addresses are recycled. Egress policy limits what a compromised workload reaches, which is the control that matters most.
- Seccomp gives broad static reduction and eBPF LSM gives narrow context-aware rules. They compose.
- Deploy detection before enforcement, for a full business cycle, or the first report of a blocking rule arrives as an outage.
- Every secret management scheme needs a first secret. Workload identity derived from attestation is the only way out of that bootstrap.
- A stolen static credential grants access until somebody notices. A stolen one-hour SVID grants it for under an hour, with no revocation infrastructure.
- Short-lived identity makes the identity control plane a critical dependency. Size the lifetime to absorb a realistic recovery, and test the failure on purpose.