CI Security Validation
Your CI pipeline is a security boundary. Code running during builds and tests can make network requests—to package registries, external APIs, analytics endpoints, or worse. Without visibility into this traffic, you can't detect supply chain attacks, accidental credential exfiltration, or compliance violations before they reach production.
This guide shows how to use Qtap in GitHub Actions to observe ALL network traffic during CI, enforce network allow-lists, and fail builds when unexpected hosts are contacted.
Why This Matters
The Problem
Your CI environment makes network calls you don't know about:
Supply chain attacks - A compromised npm/pip package phones home during
npm installorpip installAccidental production calls - Integration tests hit production APIs instead of staging
Secrets exfiltration - Malicious code in dependencies POSTs environment variables to attacker-controlled servers
Compliance violations - HIPAA/SOC2/PCI require proving no data leaves approved networks
Dependency drift - New transitive dependencies fetch from unexpected registries
Traditional approaches don't help:
Network policies - Not available in most CI runners (GitHub Actions, CircleCI, etc.)
egress proxies - Require app code changes and don't catch kernel-level traffic
Log scraping - Only shows what apps choose to log, misses silent exfiltration
The Solution
Qtap captures ALL network traffic at the Linux kernel level using eBPF:
Zero code changes - Observes traffic passively, no proxy configuration needed
Complete visibility - Sees inside TLS/HTTPS before encryption happens
Process attribution - Know exactly which command made each request
Enforcement - Fail builds automatically when disallowed hosts are contacted
Use Cases
This guide demonstrates the pattern with a simple curl example, but the same approach works for:
Package manager validation - Ensure
npm install,pip install,go getonly fetch from approved registriesIntegration test guardrails - Verify your app only calls staging/test APIs, never production
Docker build security - Confirm
apt-get,curlin Dockerfiles only reach approved mirrorsCompliance auditing - Generate tamper-proof logs of all CI network activity for security reviews
Secrets leak prevention - Detect when dependencies try to exfiltrate AWS keys, tokens, etc.
Prerequisites
Repository uses GitHub Actions with Docker available on runners.
Basic familiarity with
docker compose.Python 3 (for the helper script).
Quick Start: Basic Pattern
This section demonstrates the core pattern using a simple curl command. Once you understand the mechanics, see the production examples below for real-world use cases.
Directory Layout
The snippets below assume this directory layout:
Step 1 – Qtap Configuration (qtap-ci.yaml)
qtap-ci.yaml)We're using format: json to make downstream parsing easy.
Step 2 – Compose File (docker-compose.ci.yaml)
docker-compose.ci.yaml)The GitHub runner host can reach approved-api on localhost:8000, and Qtap observes both the host traffic and container cross-talk.
Step 3 – Verification Script (scripts/ci/verify_allowed_hosts.py)
scripts/ci/verify_allowed_hosts.py)The script downloads the entire Qtap log, saves it as an artifact, and fails if any host falls outside the allowed set.
Step 4 – GitHub Actions Workflow (.github/workflows/qtap-security.yml)
.github/workflows/qtap-security.yml)Set QTAP_NEGATIVE_TEST=true in workflow dispatch to watch the job fail on purpose. In normal runs you’d omit that step, keeping the allow-list tight.
Understanding the Qtap Output
Qtap emits one JSON object per line for each HTTP transaction. Here's what the output looks like:
Allowed Host Example
Traffic to localhost:8000 (on the allow-list):
Notice the "authority": "localhost:8000" field—the verification script extracts this to confirm it's on the allow-list. This request shows summary level capture (no headers or body fields) because it doesn't match the "Disallowed host" rule.
Disallowed Host Example
Traffic to example.com (NOT on the allow-list) - captured at full level because it triggers the "Disallowed host" rule:
Here "authority": "example.com" triggers the verification script to fail because example.com is not in the ALLOWED set.
Key Fields to Note
metadata.process_exe– Identifies which process made the request (/usr/bin/curlin these examples)request.authority– The host:port being accessed (what the verification script checks)direction–egress-externalmeans outbound to external hostsrequest.headers– Only present atlevel: full(disallowed hosts in this example)response.headers– Only present atlevel: fullresponse.body– Only present atlevel: full(base64-encoded)
Capture Levels:
Summary level (allowed hosts): Basic metadata only - no headers or bodies
Full level (disallowed hosts): Complete request/response including headers and bodies
If the job fails, download qtap-logs from the Actions run to inspect exactly which host triggered the alert.
Production Examples
Now that you understand the basic pattern, here's how to apply it to real-world scenarios.
Example 1: Validate npm install
Scenario: Ensure npm install only fetches packages from your approved registries (npmjs.com, your internal registry).
Complete Qtap configuration (qtap-npm.yaml):
Workflow changes:
What this catches:
Malicious packages that phone home during install scripts
Typosquatting packages from unexpected registries
Dependency confusion attacks using public registries when internal ones expected
Example 2: Validate Integration Tests
Scenario: Your app has integration tests that should only call staging APIs, never production.
Complete Qtap configuration (qtap-integration-tests.yaml):
Workflow changes:
What this catches:
Hardcoded production URLs accidentally committed
Environment variable misconfigurations
Tests that modify production data
Example 3: Validate Docker Build
Scenario: Your Dockerfile runs apt-get, curl, wget - ensure they only fetch from approved mirrors.
Complete Qtap configuration (qtap-docker-build.yaml):
Workflow changes:
What this catches:
Malicious RUN commands in Dockerfile
Compromised base images with modified package sources
wget/curl commands fetching from unexpected locations
Adapting to Your Environment
Update the Allow-List
The verification script accepts environment variables for flexibility:
Add More Rulekit Rules
Beyond host validation, you can flag other suspicious patterns:
Rulekit Limitation: The not operator does not currently work in rule expressions or macros. Use explicit negation with != and && operators instead.
For example, instead of:
Use:
Archive for Compliance
For HIPAA/SOC2/PCI compliance, archive Qtap logs permanently:
Summary
With this pattern, network policy drift becomes visible during every pull request—long before changes reach production. You can:
Prevent supply chain attacks by validating package manager traffic
Enforce environment boundaries by blocking accidental production calls
Detect secrets exfiltration by flagging unexpected outbound requests
Generate compliance evidence with complete network audit trails
Continuous Integration now enforces both functional AND security expectations.
Last updated