Defending Kubernetes CI/CD pipelines from self-replicating npm worms requires a four-layer approach: disable preinstall hooks with ignore-scripts=true in CI runner .npmrc files, deploy Falco runtime detection rules targeting binary execution and metadata endpoint access, enforce image signing with Kyverno admission policies, and apply NetworkPolicies blocking the cloud metadata endpoint at 169.254.169.254.
Two active npm supply chain campaigns are targeting Kubernetes CI/CD pipelines right now, and your production cluster is not the primary objective. Your build infrastructure is. The credentials that live in GitHub Actions environments, IMDS endpoints, and CI runner pods are worth more to attackers than any production workload they could reach after a perimeter breach.
Miasma compromised 32 packages across 90+ malicious versions in the @redhat-cloud-services scope by stealing OIDC tokens directly from GitHub Actions workflows. Those tokens let it publish poisoned packages carrying what appear to be valid SLSA provenance attestations - because the build pipeline that signed them was already compromised. IronWorm followed a different approach: a 976 KB Rust binary inside a preinstall hook deploys a full eBPF kernel rootkit, establishes Tor-based C2, and hides itself from every container security scanner on the market.
This guide is not another attack anatomy. It is a four-layer defense implementation for platform engineers who need to protect Kubernetes CI/CD pipelines from both of these campaigns and from the next ones.
Why Is CI/CD the Real Target Instead of Your Production Cluster?
The question platform engineers ask after a supply chain compromise is “how did they get to production?” The better question is “why did they bother?”
A compromised CI runner typically holds GitHub PATs, npm publish tokens, AWS IMDS credentials, and kubeconfig files scoped to multiple environments. Miasma demonstrated this: within 49 seconds of initial compromise, scripted sweeps used stolen GitHub tokens to inject malicious commits into five separate repositories. The attacker did not need a foothold in production. The build environment handed over everything.
IronWorm sweeps 86 environment variables and more than 20 credential file paths on every compromised machine, covering AWS, GCP, Azure, Vault, Kubernetes, npm, Docker, GitHub, and AI provider keys from all seven major providers. It takes all of them whether or not they are immediately useful.
The credential scope that AI coding agents and CI runners share is a compounding factor. See The AI Agent Credential Crisis: IAM Attack Surface in Kubernetes for how this exposure extends across the full agent infrastructure stack.
The attack surface on a typical CI runner - permissive OIDC scopes, access to the cloud metadata endpoint at 169.254.169.254, long-lived npm tokens, unrestricted egress - is significantly larger than most production workloads that sit behind network policies and admission controls.
How Do Miasma and IronWorm Attack Kubernetes CI/CD Pipelines?
Miasma: OIDC Token Theft and Self-Replication
Miasma exploits GitHub Actions workflows with id-token: write permission. The compromised workflow fires on push to any branch, executes the malicious preinstall hook, and uses the obtained OIDC token to authenticate to npm’s Trusted Publishing API. The resulting published packages carry attestations signed by the compromised workflow’s own OIDC identity - the provenance is technically real, because the signing entity was the compromised pipeline, not a separate attacker key.
Propagation is bidirectional. Miasma enumerates repositories and organizations via stolen GitHub tokens, exchanges OIDC tokens for npm publish rights, and republishes poisoned packages. It injects payloads as .github/setup.js into non-protected branches using the Git Data API with a spoofed commit author (github-actions@github.com) and commit message chore: update dependencies [skip ci] to suppress CI notifications.
IronWorm: eBPF Rootkit from a Preinstall Hook
IronWorm executes before dependency resolution completes. A 976 KB Rust ELF binary at tools/setup fires automatically during npm install. The binary uses a custom UPX stub with the UPX! magic signature overwritten to defeat signature-based detection, and encrypts every internal string with a unique per-call-site key.
The embedded eBPF rootkit provides three stealth capabilities:
- Process hiding: Rewrites
/procdirectory listings in-kernel, removing hidden PIDs before userland tools can read them.ps,top, andls /procall report clean. - Network hiding: Filters
/proc/net/tcprows and netlink responses to remove implant connections. Tor C2 traffic disappears fromssandnetstat. - Anti-debugging: Any
ptracecall against protected processes returns SIGKILL, killing the calling shell.
These capabilities depend on bpf_probe_write_user. On systems with kernel lockdown mode enabled, they fail silently - which is exactly the defense Layer 4 below leverages.
AI Coding Agent Config Injection: The New Vector
Miasma introduces an attack surface that no existing Kubernetes CI/CD security guide addresses. It plants malicious configuration files that auto-execute when developers open compromised repositories in AI-assisted IDEs:
.claude/settings.json- SessionStart hook executingnode .github/setup.js.gemini/settings.json- identical SessionStart structure.cursor/rules/setup.mdc- prompt injection withalwaysApply: true.vscode/tasks.json-runOptions.runOn: "folderOpen"triggerpackage.json- hijackedtestscript
Any developer who clones a compromised repository and opens it in a targeted IDE detonates the payload without performing any deliberate action.
sequenceDiagram
participant A as Attacker
participant G as Git Data API
participant D as Developer Machine
participant N as npm Registry
participant R as Next Repo
A->>G: POST /repos/{owner}/{repo}/contents/.claude/settings.json
Note over G: Commit: "chore: update dependencies [skip ci]"
Note over G: Author: github-actions@github.com
D->>G: git clone compromised-repo
D->>D: Opens project in Claude Code
Note over D: SessionStart hook fires
D->>D: node .github/setup.js executes
Note over D: AWS, GCP, GitHub PAT, npm tokens stolen
D->>N: OIDC token exchanged for npm publish
N-->>D: Publish rights granted
D->>N: Poisoned package published with valid provenance
D->>R: GitHub PAT used across 5 repos (49 seconds)
R-->>A: Cycle repeats
Miasma’s propagation from a single compromised repository through an AI coding agent into the npm registry and adjacent repositories - all within 49 seconds.
The Four-Layer Defense Model
graph LR
subgraph L1["Layer 1: npm Entry Point"]
A[--ignore-scripts]
B[Staged publish + 2FA]
C[Install controls]
end
subgraph L2["Layer 2: Falco Runtime Detection"]
D[Binary exec from node_modules]
E[Metadata endpoint access]
F[Tor bootstrap detection]
end
subgraph L3["Layer 3: Kyverno Admission"]
G[Image signature verification]
H[SLSA provenance attestation]
I[Namespace-scoped policies]
end
subgraph L4["Layer 4: Kernel + Network Isolation"]
J[NetworkPolicy: block 169.254.169.254]
K[Egress allowlisting]
L[Kernel lockdown mode]
M[gVisor / Kata RuntimeClass]
end
L1 -->|blocks| N[Hook Execution]
L2 -->|detects| O[Payload Activity]
L3 -->|enforces| P[Image Integrity]
L4 -->|contains| Q[Lateral Movement]
Each layer targets a different phase of the attack chain. Layers 1 and 4 are deployable today without infrastructure changes.
Layer 1: Block the Entry Point
Disable Lifecycle Scripts in CI Runners
Both Miasma and IronWorm detonate through npm lifecycle hooks. Disabling them in CI eliminates the primary entry point.
Add to your CI runner’s .npmrc:
# .npmrc for CI/CD runners
ignore-scripts=true
audit=true
fund=false
allow-file=false
allow-remote=false
allow-directory=false
The allow-file, allow-remote, and allow-directory flags shipped in npm 11.15.0 and restrict install sources in addition to blocking scripts.
The practical problem: packages like esbuild, playwright, and bcrypt require install scripts for native compilation. For these, use LavaMoat’s @lavamoat/allow-scripts to maintain an explicit allowlist:
npx --yes @lavamoat/allow-scripts setup
# Generates .lavamoat/allow-scripts.yml with an explicit
# allowlist of packages permitted to run install scripts
npx allow-scripts run
Any package not in the allowlist runs with scripts disabled. Any new package attempting to run a script fails the build with an explicit error, prompting a human review before the allowlist is updated.
Enable Staged Publishing with 2FA
npm’s staged publishing (generally available since May 22, 2026) is the single most effective defense against OIDC-token-based package hijacking. Instead of npm publish pushing directly to the registry, npm stage publish places the tarball in a staging queue. A human maintainer must authenticate with 2FA and explicitly approve via npm stage approve before the version becomes installable.
The critical property: npm stage approve requires interactive authentication that OIDC tokens and granular access tokens cannot satisfy. Miasma’s entire propagation chain breaks here - the stolen OIDC token can run npm stage publish, but it cannot run npm stage approve.
Configure your package for stage-only mode by setting your trusted publisher configuration to reject direct publishes:
# CI can stage but not publish directly
npm stage publish
# A human maintainer approves from their local machine
npm stage list # Review pending staged versions
npm stage view <id> # Inspect the tarball before approval
npm stage approve <id> # Requires interactive 2FA
Layer 2: Detect at Runtime
Falco 0.43.0 ships a default rule - “Network Tool Executed During NPM Package Install” - that detects network tools launched when npm installs packages. Extend it with campaign-specific rules for Miasma and IronWorm behavior.
Detect binary execution from node_modules:
- rule: Suspicious Binary in node_modules
desc: Alert when an ELF binary executes from node_modules directory
condition: >
spawned_process and
container and
(proc.exepath contains "node_modules" or
proc.exepath startswith "/tmp/") and
not proc.name in (node, npm, npx, yarn, pnpm, bun)
output: >
Suspicious binary executed during package install
(user=%user.name container=%container.name proc=%proc.name
exe=%proc.exepath cmdline=%proc.cmdline)
priority: CRITICAL
Detect cloud metadata access from build pods:
- rule: CI Runner Metadata Access
desc: Detect access to cloud metadata from CI/CD containers
condition: >
evt.type = connect and
container and
fd.sip = "169.254.169.254" and
proc.pname in (node, npm, bun)
output: >
Cloud metadata access from npm process in CI runner
(container=%container.name proc=%proc.name parent=%proc.pname)
priority: CRITICAL
Detect Tor bootstrap in build containers:
- rule: Tor Daemon in CI Container
desc: Detect Tor process starting in build containers
condition: >
spawned_process and
container and
proc.name = "tor"
output: >
Tor daemon started in container (container=%container.name
image=%container.image cmdline=%proc.cmdline)
priority: CRITICAL
Detect process memory access from package manager processes:
- rule: Process Memory Dump in CI Runner
desc: Detect attempts to read /proc/*/mem or environ from npm processes
condition: >
(evt.type = open or evt.type = openat) and
container and
(fd.name contains "/proc/" and
(fd.name contains "/mem" or fd.name contains "/environ")) and
proc.pname in (node, npm, bun, yarn, pnpm)
output: >
Process memory access from package manager process
(proc=%proc.name file=%fd.name container=%container.name
image=%container.image)
priority: CRITICAL
The Tor bootstrap rule and metadata access rule together detect IronWorm’s C2 establishment and credential theft phases. The binary execution rule catches the initial Rust binary detonation before the rootkit loads.
Layer 3: Enforce at Admission
Kyverno’s ImageValidatingPolicy prevents attackers from deploying tampered runner images with weakened security controls. Apply this policy to your CI/CD namespace:
apiVersion: policies.kyverno.io/v1
kind: NamespacedImageValidatingPolicy
metadata:
name: enforce-signed-runner-images
namespace: ci-cd-runners
spec:
matchConstraints:
resourceRules:
- apiGroups: ['']
apiVersions: ['v1']
operations: ['CREATE', 'UPDATE']
resources: ['pods']
matchImageReferences:
- glob: 'ghcr.io/myorg/runner-*'
attestors:
- name: ci-pipeline
cosign:
keyless:
identities:
- subject: 'https://github.com/myorg/runners/.github/workflows/*'
issuer: 'https://token.actions.githubusercontent.com'
ctlog:
url: 'https://rekor.sigstore.dev'
validationConfigurations:
required: true
mutateDigest: true
validations:
- message: 'Only CI/CD pipeline-signed runner images permitted'
expression: >-
images.containers.map(image, verifyImageSignatures(image,
[attestors.ci-pipeline])).all(result, result > 0)
Pair this with SLSA provenance attestation verification to confirm that runner images were built by your trusted pipeline:
attestations:
- name: slsa-provenance
intoto:
type: https://slsa.dev/provenance/v1
validations:
- expression: >-
images.containers.map(image, verifyAttestationSignatures(image,
attestations.slsa-provenance, [attestors.ci-pipeline])).all(e, e > 0)
message: 'SLSA provenance attestation verification failed'
This does not prevent a compromised pipeline from producing malicious packages with valid attestations - Miasma demonstrated that limit. It does prevent an attacker from deploying arbitrary runner images into your CI namespace after compromising a developer credential.
Layer 4: Contain the Blast Radius
Block Metadata Endpoints and Restrict Egress
Both campaigns depend on reaching 169.254.169.254 for IMDS token theft. A NetworkPolicy blocking this endpoint from CI namespaces is deployable in under five minutes:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata-endpoint
namespace: ci-runners
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
For Calico environments, apply cluster-wide:
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: block-cloud-metadata
spec:
namespaceSelector: block-metadata == "true"
selector: '!has(metadata-access)'
order: 100
types:
- Egress
egress:
- action: Deny
protocol: TCP
destination:
nets:
- 169.254.169.254/32
- action: Allow
Extend the egress policy to deny-all with an explicit allowlist covering only package registries (registry.npmjs.org), container registries (ghcr.io, docker.io), and internal services. IronWorm’s Tor C2 traffic has nowhere to go if the runner cannot reach arbitrary outbound endpoints.
Kernel Lockdown Mode and Container Isolation
IronWorm’s eBPF rootkit uses bpf_probe_write_user to modify /proc listings in-kernel. Linux kernel lockdown mode, available since kernel 5.4, restricts this call for all users including root. The rootkit’s stealth capabilities fail silently. Your detection tools continue working.
graph TB
subgraph Default["Default Kernel"]
A1[npm install fires preinstall] --> B1[Rust binary executes]
B1 --> C1[eBPF program loads]
C1 --> D1[bpf_probe_write_user succeeds]
D1 --> E1[/proc listings rewritten]
E1 --> F1[Process + network hidden from defenders]
end
subgraph Lockdown["Lockdown-Enabled Kernel"]
A2[npm install fires preinstall] --> B2[Rust binary executes]
B2 --> C2[eBPF program loads]
C2 --> D2[bpf_probe_write_user DENIED]
D2 --> E2[Rootkit fails silently]
E2 --> F2[ps / ss / netstat report normally]
F2 --> G2[Falco rules detect Tor + binary exec]
end
style F1 fill:#ef4444,color:#fff
style G2 fill:#22c55e,color:#fff
style D2 fill:#3b82f6,color:#fff
Kernel lockdown mode neutralizes IronWorm’s stealth layer. The rootkit loads but cannot hide itself, making all Layer 2 detection rules effective.
Enable on CI nodes:
# /etc/default/grub - add lockdown=confidentiality to GRUB_CMDLINE_LINUX
echo 'kernel.unprivileged_bpf_disabled=1' >> /etc/sysctl.d/99-security.conf
sysctl -p /etc/sysctl.d/99-security.conf
For build pod isolation, use gVisor’s runsc RuntimeClass. gVisor intercepts syscalls at the userspace level and never exposes the host kernel’s eBPF subsystem to untrusted workloads:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
name: ci-runner
namespace: ci-runners
spec:
runtimeClassName: gvisor
containers:
- name: build
image: ghcr.io/myorg/runner-node:20
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
Reserve Kata Containers for pipelines handling production secrets or executing untrusted third-party code. Each Kata pod gets its own kernel, providing complete isolation from the host eBPF subsystem, at the cost of 100-300ms additional boot time.
Layer 5: Harden the AI Agent Attack Surface
The .claude/, .gemini/, .cursor/, and .vscode/ directories in a repository are now a live attack surface that CI/CD security guides have not addressed. Add a pre-clone scan to your pipelines:
#!/bin/bash
# Pre-clone hook: scan for malicious AI agent config files
SUSPICIOUS_FILES=(
".claude/settings.json"
".gemini/settings.json"
".cursor/rules/setup.mdc"
".vscode/tasks.json"
".github/setup.js"
)
for f in "${SUSPICIOUS_FILES[@]}"; do
if [ -f "$f" ]; then
echo "WARNING: AI agent config detected: $f"
if grep -q "SessionStart\|folderOpen\|alwaysApply" "$f" 2>/dev/null; then
echo "CRITICAL: Auto-execution config found in $f - quarantining"
mv "$f" "$f.quarantined"
exit 1
fi
fi
done
Enable branch protection rules on all repositories in your organization to prevent force-pushes and require signed commits. Miasma uses the Git Data API to inject commits to non-protected branches without going through a pull request. Branch protection rules at the organization level close this path.
For a broader look at how AI coding agents are becoming a privileged infrastructure attack surface across Kubernetes deployments, see Securing AI Coding Agent Infrastructure Access.
Add commit signing enforcement to your CI workflows. Miasma spoofs github-actions@github.com as the commit author. Requiring signed commits with verified identities makes unsigned injections visible in the commit log and rejectable at the repository level.
Implementation Priority Matrix
| Defense | Effort | Coverage | Deploy Window |
|---|---|---|---|
ignore-scripts=true in CI .npmrc | Low | Blocks Miasma + IronWorm hook execution | Today |
| NetworkPolicy blocking 169.254.169.254 | Low | Blocks IMDS credential theft | Today |
| Pre-clone AI config scanner | Low | Blocks IDE-based propagation | Today |
| Falco runtime rules (binary + metadata + Tor) | Medium | Detects active IronWorm activity | This sprint |
| npm staged publishing with 2FA | Medium | Blocks OIDC token publish hijacking | This sprint |
LavaMoat @lavamoat/allow-scripts | Medium | Selective script execution for native packages | This sprint |
| Branch protection + commit signing | Medium | Blocks Git Data API injection | This sprint |
kernel.unprivileged_bpf_disabled=1 | Low | Degrades IronWorm stealth on existing nodes | This sprint |
| Kernel lockdown mode | Medium | Neutralizes eBPF rootkit on new nodes | This quarter |
| gVisor RuntimeClass for build pods | Medium | Syscall-level isolation for CI runners | This quarter |
| Kyverno image signing policy | High | Blocks tampered runner image deployment | This quarter |
| Egress allowlist for registries only | Medium | Blocks Tor C2 and arbitrary exfiltration | This quarter |
Start with the three “Today” rows. They are configuration changes with no infrastructure dependencies, they block the primary entry points for both campaigns, and they take less than an hour to implement across a typical CI/CD namespace.
Frequently Asked Questions
Can SLSA provenance alone protect against these npm worms?
No. Miasma produces SLSA attestations that appear valid because it compromises the CI pipeline itself, obtaining a legitimate OIDC token from the GitHub Actions runner. SLSA protects against binary substitution - an attacker swapping a built artifact for a different one - not against a compromised build system producing malicious artifacts with real provenance. Defense requires multiple layers: staged publishing with human 2FA approval, runtime detection for anomalous behavior, and network isolation to contain credential theft.
Does npm —ignore-scripts completely prevent these worms from executing?
Yes for the primary execution vector. Both Miasma and IronWorm rely on preinstall hooks to fire their payloads, and --ignore-scripts blocks all lifecycle hooks including preinstall, postinstall, and prepare. The practical exception: packages like esbuild, playwright, and bcrypt require install scripts for native binary compilation. Use LavaMoat’s @lavamoat/allow-scripts to selectively enable scripts only for audited packages, or run npm rebuild <package> for specific trusted packages after the initial install completes with scripts disabled.
How does kernel lockdown mode affect legitimate eBPF tools like Falco?
Kernel lockdown restricts dangerous eBPF helpers like bpf_probe_write_user - the specific helper IronWorm uses to modify process memory and hide itself from /proc. It does not prevent read-only tracing operations. Falco uses bpf_probe_read, perf events, and similar read-only paths that remain fully functional under lockdown. The rootkit’s stealth capabilities break while Falco’s detection capabilities continue working normally.
Should I use gVisor or Kata Containers for CI/CD build isolation?
gVisor provides strong syscall-level isolation with minimal overhead and no host kernel eBPF exposure. It works well for standard Node.js, Python, and Go builds. Kata Containers provides full VM-level isolation where each runner pod gets its own kernel - complete isolation from the host eBPF subsystem - but adds 100-300ms boot time per pod. Use gVisor for high-volume build jobs where boot latency matters. Reserve Kata for pipelines that execute untrusted third-party code or that have access to production credentials where the higher isolation guarantee justifies the overhead.
What is the minimum set of defenses I should deploy immediately?
Three changes deployable today with no infrastructure upgrades: (1) Add ignore-scripts=true to your CI runner .npmrc files - this blocks the preinstall hook execution that both Miasma and IronWorm depend on. (2) Apply a NetworkPolicy blocking egress to 169.254.169.254/32 in CI namespaces - this prevents IMDS token theft that funds further propagation. (3) Add a pre-clone step scanning for .claude/, .gemini/, .cursor/, and .vscode/tasks.json in repository checkouts - this blocks the IDE auto-execution vector Miasma uses to spread through developer machines. These three controls address the primary entry point, the primary credential theft vector, and the new AI agent propagation vector that existing guides miss.