Safeguard
Best Practices

Defense-in-depth for a modern cloud-native application stack

Log4Shell and the XZ backdoor were caught two different ways — one by patching, one by a developer noticing 500ms of extra SSH latency. Neither alone is a strategy.

Safeguard Research Team
Research
6 min read

On November 24, 2021, Chen Zhaojun of Alibaba Cloud's security team privately disclosed a remote-code-execution flaw in Apache Log4j's JNDI lookup feature — a bug that had sat latent in the library since 2013. The CVE, assigned CVE-2021-44228 on December 10, 2021, became one of the most widely exploited vulnerabilities in internet history within days, because a single patch was never going to be enough: the flaw lived in application code, but stopping it required dependency scanning, web application firewalls, and egress filtering working together, since patch rollout across a global dependency tree simply couldn't happen fast enough. Two and a half years later, a completely different kind of layer caught a completely different kind of attack: on March 29, 2024, PostgreSQL developer and Microsoft engineer Andres Freund reported to Openwall that he'd traced a ~500-millisecond SSH login slowdown to a deliberately planted backdoor in xz/liblzma versions 5.6.0 and 5.6.1 (CVE-2024-3094, CVSS 10.0), inserted by a contributor using the handle "Jia Tan." No scanner caught that one — a human noticed a runtime anomaly. Defense-in-depth exists precisely because those two failure modes are different, and no single layer of a cloud-native stack catches both. This post walks the stack layer by layer, from source code to running workload.

Why doesn't scanning source code catch everything?

Source-code scanning — SAST, linting, secret detection — only sees what's written in your own repository, and Log4Shell is the textbook case for why that's not enough. The vulnerable code wasn't in anyone's application; it was three layers down, in a logging library's JNDI lookup feature, pulled in transitively by countless Java projects that never directly imported it. A SAST tool scanning your own controller code would never flag it, because the dangerous line lived in someone else's dependency tree. This is why source-layer controls have to be paired with software composition analysis (SCA) that resolves the full transitive dependency graph, not just direct imports. Static analysis of your own code answers "did we write a bug," not "did we import one," and Log4Shell's blast radius — affecting an enormous share of enterprise Java applications, per contemporaneous vendor telemetry from Cloudflare and others — was a direct consequence of how deep in the dependency graph the vulnerable code sat.

Can the build pipeline itself be the attack, not just a scanning gap?

Yes — the XZ Utils backdoor proved the build pipeline is itself an attack surface, not just a place to run scans. The malicious code wasn't a vulnerability an attacker found; it was inserted directly into the release tarballs of xz/liblzma 5.6.0 and 5.6.1 by a trusted-looking maintainer over roughly two years of social-engineering the project's actual maintainer into handing over commit access, according to the public timeline reconstructed after Freund's disclosure. The payload specifically targeted OpenSSH via a systemd/liblzma linkage and was obfuscated across build scripts and test binary blobs — designed to evade both code review and static analysis. This is why supply-chain provenance frameworks like SLSA, which attest to how and from what source an artifact was built, are a distinct layer from vulnerability scanning: a build-provenance check can flag "this binary doesn't match a reproducible build from the tagged source," which is a different signal than "this code contains a known-bad pattern."

Why do artifact registries and image signing matter as their own layer?

Registries and signing matter as their own layer because everything upstream of them — code review, CI scanning, build provenance — is worthless if an unsigned or unverified artifact can still be pulled into production. A registry-layer control (Cosign or Notation signature verification, SBOM attestation checks) is the last software-side checkpoint before an artifact becomes a running workload, and it's designed to fail closed: if a signature is missing or a required SBOM attestation wasn't generated, the pull is blocked regardless of what any earlier scan said. This layer also catches drift — a "hotfix" pushed straight to a registry outside the normal CI path, bypassing every upstream control. Treating the registry as a policy gate, not just storage, is what makes "we scanned it in CI" mean something at deploy time instead of being a stale, disconnected fact.

What does the Kubernetes admission layer actually stop that earlier layers miss?

The admission layer stops the gap between "this image was approved at some point" and "this exact image, right now, still meets policy" — because vulnerability databases change continuously and an image that was clean at build time can become non-compliant hours later when a new CVE or CISA Known Exploited Vulnerabilities (KEV) entry lands. An admission controller (Kyverno, Sigstore's policy-controller, or similar) re-evaluates the image digest against current policy at the moment a pod is scheduled, not at the moment it was built. This is a materially different check than CI gating: CI answers "was this compliant when we built it," admission answers "is this still compliant right now." A policy like "block any image containing a KEV-listed critical CVE" only does its job if it's enforced at admission, since KEV additions happen on an ongoing basis, independent of your build schedule.

Why is runtime observation the layer that catches what static analysis structurally cannot?

Runtime observation is the layer that catches what static analysis structurally cannot, because some attacks only manifest as behavior — and the XZ backdoor is the clearest recent proof. No static scanner flagged CVE-2024-3094 before disclosure; it was caught because a human noticed an anomalous timing signature during actual program execution, not because a rule matched a pattern in source. Runtime tooling — eBPF-based collectors that watch loaded libraries, process trees, and egress destinations on running hosts — exists to generalize that kind of catch beyond one unusually attentive developer. It also catches an entirely different, more mundane class of drift: a debug tool baked into a production image by accident, or a container running code that doesn't match any known SBOM at all. Runtime is the layer that assumes every upstream layer, including human code review, can and sometimes will fail.

How do these layers work together instead of duplicating effort?

These layers work together because each one is deliberately built to catch what its neighbors structurally cannot, so overlap is intentional rather than wasteful redundancy. Safeguard's own guardrails documentation lays this out as six concrete enforcement points — IDE, commit, CI, registry, admission, and runtime — each backed by a different mechanism (a CI gate step, a registry webhook, a Kubernetes admission controller, an eBPF runtime collector) and each producing a signed audit record so a security team can show exactly which layer evaluated a given risk and when. Safeguard's continuous-scanning approach reinforces this by re-scoring assets the moment a new CVE or KEV entry appears — CISA KEV additions are reflected in scored findings in under 60 seconds by the platform's own reported latency figures — closing the gap between "scanned once at build" and "still compliant now" that a build-only model leaves open. None of this replaces vigilance like Andres Freund's; it exists to catch the much larger number of cases where nobody happens to be watching a login timer.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.