Complete Guide - From Hello World to Production
This guide takes you from a simple "hello world" setup to a production-ready configuration, with every example validated and tested. Follow along at your own pace - each level builds on the previous one.
What you'll learn:
Level 1: Dead simple setup - verify qtap is working
Level 2: Basic filtering and selective capture
Level 3: Conditional capture with rulekit expressions
Level 4: Production storage with S3
Prerequisites:
Docker installed and running
Basic familiarity with YAML
curl or similar HTTP client
Level 1: Dead Simple - Verify It's Working
Goal: Get qtap running and see it capture HTTPS traffic in under 5 minutes.
What you'll learn:
Basic qtap configuration
How to verify qtap sees inside HTTPS
Understanding qtap output
Step 1: Create Your First Config
Create a file called qtap-level1.yaml
:
version: 2
# Where to send captured data (stdout = your terminal)
services:
event_stores:
- type: stdout
object_stores:
- type: stdout
# What to do with captured traffic
stacks:
basic_stack:
plugins:
- type: http_capture
config:
level: full # (none|summary|details|full) - Capture everything
format: text # (json|text) - Human-readable output
# What traffic to capture
tap:
direction: egress # (egress|egress-external|egress-internal|ingress|all) - Outbound traffic only
ignore_loopback: true # (true|false) - Skip localhost traffic
audit_include_dns: false # (true|false) - Don't capture DNS queries
http:
stack: basic_stack # Use the stack we defined above
What this does:
Captures all outbound HTTP/HTTPS traffic
Shows full request/response details in your terminal
No filters - captures everything (except localhost)
Step 2: Start Qtap
docker run -d \
--name qtap-level1 \
--user 0:0 \
--privileged \
--cap-add CAP_BPF \
--cap-add CAP_SYS_ADMIN \
--pid=host \
--network=host \
-v /sys:/sys \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/qtap-level1.yaml:/app/config/qtap.yaml" \
-e TINI_SUBREAPER=1 \
--ulimit=memlock=-1 \
us-docker.pkg.dev/qpoint-edge/public/qtap:v0 \
--log-level=info \
--log-encoding=console \
--config="/app/config/qtap.yaml"
Wait for qtap to initialize (important!):
sleep 6
Step 3: Generate Test Traffic
docker run --rm curlimages/curl -s https://httpbin.org/get
Step 4: See What Qtap Captured
docker logs qtap-level1 2>&1 | grep -A 30 "httpbin.org"
What you should see:
URL: https://httpbin.org/get
Method: GET
Protocol: http2
Request Headers:
Host: httpbin.org
User-Agent: curl/8.12.1
Accept: */*
Response Headers:
HTTP/2 200 OK
Content-Type: application/json
Body:
{
"args": {},
"headers": {
"Host": "httpbin.org",
"User-Agent": "curl/8.12.1"
},
"url": "https://httpbin.org/get"
}
🎉 Success indicators:
✅ You see the full URL despite HTTPS encryption
✅ All request and response headers are visible
✅ The response body (JSON) is captured
✅ Protocol detected as
http2
Understanding the Output
Let's break down what qtap showed you:
Process Information:
"exe": "/usr/bin/curl"
- Qtap knows which process made the request
Connection Details:
"protocol": "http2"
- Detected HTTP/2"is_tls": true
- Detected TLS encryption"tlsProbeTypesDetected": ["openssl"]
- Identified OpenSSL library
HTTP Transaction:
Full URL, method, headers captured
Complete request and response body
Response status code (200)
How does qtap see inside HTTPS?
Qtap uses eBPF to hook into system calls before TLS encryption happens. It doesn't break TLS or act as a proxy - it just observes what your application sends before OpenSSL encrypts it.
Troubleshooting
Don't see any output?
Make sure qtap was running BEFORE you generated traffic
Wait 6 seconds after starting qtap before testing
Check qtap is running:
docker ps | grep qtap-level1
See connection info but no HTTP details?
Look for
"l7Protocol": "other"
- means HTTP parsing failedCheck if you're looking at the right traffic (search for your test domain)
Clean up when done:
docker rm -f qtap-level1
What's Next?
✅ Level 1 Complete! You now know:
How to configure qtap basics
How to verify qtap is capturing traffic
That qtap can see inside HTTPS
Next up: Level 2 - Basic Filtering and Selective Capture
Filter out noisy processes
Apply different capture levels by domain
Use multiple stacks for different traffic types
Level 2: Basic Filtering and Selective Capture
Goal: Control what traffic gets captured and apply different capture levels to different traffic.
What you'll learn:
Filter out noisy processes (curl, wget)
Apply different capture levels by domain
Use different capture levels for different endpoints
Why Filtering Matters
In real environments, you'll see a LOT of traffic. Level 1 captures everything, which means:
Kubernetes health checks flood your logs
Package managers create noise
Your own debugging with curl shows up
Monitoring agents add clutter
Level 2 teaches you to be selective.
Step 1: Create a Filtered Config
Create qtap-level2.yaml
:
version: 2
services:
event_stores:
- type: stdout
object_stores:
- type: stdout
stacks:
# Detailed stack for important APIs
detailed_stack:
plugins:
- type: http_capture
config:
level: full # (none|summary|details|full) - Full capture
format: text # (json|text)
# Lightweight stack for everything else
lightweight_stack:
plugins:
- type: http_capture
config:
level: summary # (none|summary|details|full) - Just basic info
format: text # (json|text)
tap:
direction: egress # (egress|egress-external|egress-internal|ingress|all)
ignore_loopback: true # (true|false)
audit_include_dns: false # (true|false)
# Default: use lightweight stack
http:
stack: lightweight_stack
# Filter out noisy processes
filters:
groups:
- qpoint # Don't capture qtap's own traffic
custom:
- exe: /usr/bin/curl
strategy: exact # Filter out manual curl commands
# Apply specific stacks to specific domains
endpoints:
- domain: 'httpbin.org'
http:
stack: detailed_stack # Full capture for httpbin
- domain: 'api.github.com'
http:
stack: detailed_stack # Full capture for GitHub API
What's new here:
Two stacks: Detailed (full) vs lightweight (summary only)
Process filters: Ignore curl (filter out manual testing)
Domain-specific stacks: httpbin.org uses detailed_stack for full capture
Default lightweight: Everything else is summary only
Step 2: Start Level 2
docker run -d \
--name qtap-level2 \
--user 0:0 \
--privileged \
--cap-add CAP_BPF \
--cap-add CAP_SYS_ADMIN \
--pid=host \
--network=host \
-v /sys:/sys \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/qtap-level2.yaml:/app/config/qtap.yaml" \
-e TINI_SUBREAPER=1 \
--ulimit=memlock=-1 \
us-docker.pkg.dev/qpoint-edge/public/qtap:v0 \
--log-level=info \
--log-encoding=console \
--config="/app/config/qtap.yaml"
sleep 6
Step 3: Test the Filters
Test 1: Filtered traffic (should NOT appear)
docker run --rm curlimages/curl -s https://example.com/get
Check logs:
docker logs qtap-level2 2>&1 | grep "example.com"
Expected: Nothing! curl is filtered.
Test 2: Domain-specific capture (should appear with FULL details)
docker run --rm alpine sh -c "wget -O- https://httpbin.org/get 2>/dev/null"
Check logs:
docker logs qtap-level2 2>&1 | grep -A 20 "httpbin.org"
Expected: Full HTTP details including body because:
httpbin.org matches our endpoint rule
Endpoint rule applies
detailed_stack
for full capturewget (/bin/busybox) is NOT filtered, so traffic is captured
Understanding the Results
What you should see:
Curl traffic: Filtered out
# This returns nothing docker logs qtap-level2 | grep -c "curl" # Output: 0
httpbin.org via wget: CAPTURED with full details
Method, URL, headers, body all visible
Because it matches the endpoint rule
Other domains: Would show summary only (if we tested them)
Basic info: method, URL, status, duration
No headers, no bodies
Why This Matters
This configuration gives you:
Less noise: Filters out development tools
Selective detail: Full capture only where you need it
Better performance: Summary-only for bulk traffic
Cost savings: Less data stored/transmitted
Advanced: Using Prefix Filters
Want to filter an entire directory?
filters:
custom:
- exe: /usr/bin/
strategy: prefix # Filters ALL /usr/bin/* processes
This would block /usr/bin/curl
, /usr/bin/wget
, /usr/bin/python3
, etc.
Clean Up
docker rm -f qtap-level2
What's Next?
✅ Level 2 Complete! You now know:
How to filter noisy processes
How to apply different capture levels by domain
How to use multiple stacks for different traffic types
Next up: Level 3 - Conditional Capture with Rulekit
Use rulekit expressions for intelligent capture
Create reusable macros
Capture only errors and specific request types
Filter by container and pod labels
Level 3: Conditional Capture with Rulekit
Goal: Use rulekit expressions for intelligent, conditional traffic capture.
What you'll learn:
Capture only errors and POST requests
Use different capture levels based on conditions
Create reusable macros for complex logic
Filter by container and pod labels
Why Conditional Capture Matters
In high-traffic environments, capturing everything is expensive and noisy. You want:
Selective detail: Full capture only when needed
Error focus: Always capture failures
Method-based filtering: Capture mutating operations (POST, PUT, DELETE)
Smart sampling: Reduce volume without losing critical data
Step 1: Create a Rulekit-Based Config
Create qtap-level3.yaml
:
version: 2
# Define reusable macros
rulekit:
macros:
- name: is_error
expr: http.res.status >= 400 && http.res.status < 600
- name: is_post
expr: http.req.method == "POST"
- name: is_api_call
expr: http.req.path matches /^\/api\//
services:
event_stores:
- type: stdout
object_stores:
- type: stdout
stacks:
# Smart stack: Conditional capture levels
smart_stack:
plugins:
- type: http_capture
config:
level: summary # (none|summary|details|full) - Default: basic info only
format: text # (json|text)
rules:
# Full capture for errors
- name: "Capture all errors with full details"
expr: is_error()
level: full
# Full capture for POST requests
- name: "Capture POST requests"
expr: is_post()
level: full
# Details for API calls (headers, no body)
- name: "Capture API calls with headers"
expr: is_api_call()
level: details
# Error-only stack: Only capture failures
error_only_stack:
plugins:
- type: http_capture
config:
level: none # (none|summary|details|full) - Default: capture nothing
format: text # (json|text)
rules:
# Only capture 4xx and 5xx
- name: "Client errors"
expr: http.res.status >= 400 && http.res.status < 500
level: full
- name: "Server errors"
expr: http.res.status >= 500
level: full
tap:
direction: egress # (egress|egress-external|egress-internal|ingress|all)
ignore_loopback: true # (true|false)
audit_include_dns: false # (true|false)
http:
stack: smart_stack # Default: smart conditional capture
filters:
groups:
- qpoint
custom:
- exe: /usr/bin/curl
strategy: exact # (exact|prefix|regex)
# Apply different stacks to different domains
endpoints:
# GitHub API: error-only capture
- domain: 'api.github.com'
http:
stack: error_only_stack
# httpbin: smart capture with all rules
- domain: 'httpbin.org'
http:
stack: smart_stack
Step 2: Understanding Rulekit
Macros:
Reusable expressions defined once, used many times
Called like functions:
is_error()
,is_slow()
Make configs cleaner and easier to maintain
Capture Levels in Rules:
none
: Skip capture entirelysummary
: Basic info (method, URL, status, duration, process)details
: Include headers (no bodies)full
: Everything (headers + bodies)
Available Fields:
Request:
http.req.method
,http.req.path
,http.req.host
,http.req.url
Response:
http.res.status
Headers:
http.req.header.<name>
,http.res.header.<name>
Source:
src.container.name
,src.container.labels.<key>
,src.pod.name
,src.pod.labels.<key>
Step 3: Start Level 3
docker run -d \
--name qtap-level3 \
--user 0:0 \
--privileged \
--cap-add CAP_BPF \
--cap-add CAP_SYS_ADMIN \
--pid=host \
--network=host \
-v /sys:/sys \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/qtap-level3.yaml:/app/config/qtap.yaml" \
-e TINI_SUBREAPER=1 \
--ulimit=memlock=-1 \
us-docker.pkg.dev/qpoint-edge/public/qtap:v0 \
--log-level=info \
--log-encoding=console \
--config="/app/config/qtap.yaml"
sleep 6
Step 4: Test Conditional Capture
Test 1: Success (should show SUMMARY - basic info only):
docker run --rm alpine sh -c "wget -O- http://httpbin.org/get 2>/dev/null"
Check logs - should see basic metadata (method, URL, status) but no headers or body:
docker logs qtap-level3 2>&1 | grep -A 20 "HTTP Transaction"
Test 2: Error (should show FULL details):
docker run --rm alpine sh -c "wget -O- http://httpbin.org/status/404 2>/dev/null" || true
Check logs - should see full capture with all details:
docker logs qtap-level3 2>&1 | grep -A 20 "404"
Test 3: API path (should show DETAILS with headers):
docker run --rm alpine sh -c "wget -O- http://httpbin.org/api/test 2>/dev/null" || true
Understanding the Results
What you should see:
Success requests: Summary level (default)
Method, URL, status code
Duration, process info
No headers, no bodies
Error requests (404, 500): Full capture
Complete request and response headers
Full request/response bodies
Because they match
is_error()
macro
API paths: Details level
Headers included
No bodies (saves space)
Because path matches
/^\/api\//
Advanced Rulekit Patterns
Container-based filtering:
rules:
- name: "Debug specific container"
expr: src.container.name == "my-app"
level: full
Regex matching:
rules:
- name: "External APIs"
expr: http.req.host matches /\.(googleapis\.com|amazonaws\.com)$/
level: details
Complex conditions:
rules:
- name: "Production API errors"
expr: is_api_call() && is_error() && src.pod.labels.env == "production"
level: full
Clean Up
docker rm -f qtap-level3
Level 4: Production Storage with S3
Goal: Store captured traffic in S3 for long-term retention and compliance.
What you'll learn:
Configure S3-compatible object storage
Use MinIO for local testing
Combine stdout (debugging) with S3 (persistence)
Production storage best practices
Why S3 Storage Matters
The primary benefit of S3 storage is keeping sensitive data within your network boundary.
When you use S3-compatible storage (MinIO, AWS S3, GCS), captured HTTP traffic containing sensitive information (API keys, tokens, PII, etc.) never leaves your infrastructure. This is critical for:
Data sovereignty: Keep sensitive data in your own network/region
Security: Prevent data exfiltration - traffic never goes to external services
Compliance: Meet regulatory requirements (GDPR, HIPAA, SOC2)
Control: You own and control access to all captured data
Additionally, S3 provides:
Forensics: Investigate security incidents weeks/months later
Analytics: Analyze traffic patterns over time
Debugging: Review full request/response data when issues occur
Durability: Long-term retention with lifecycle policies
stdout is great for development, but production requires secure, persistent storage within your control.
Step 1: Set Up MinIO (Local S3)
For this guide, we'll use MinIO - an S3-compatible storage you can run locally:
# Start MinIO
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
quay.io/minio/minio server /data --console-address ":9001"
sleep 3
# Create bucket
docker run --rm \
--network=host \
--entrypoint /bin/sh \
quay.io/minio/minio -c "
mc alias set local http://localhost:9000 minioadmin minioadmin &&
mc mb local/qtap-captures &&
echo 'Bucket created successfully!'
"
Step 2: Create S3-Enabled Config
Create qtap-level4-s3.yaml
:
version: 2
# Reusable macros
rulekit:
macros:
- name: is_error
expr: http.res.status >= 400 && http.res.status < 600
services:
# Events: Still use stdout for real-time monitoring
event_stores:
- type: stdout
# Objects: Store in S3 for persistence
object_stores:
- type: s3
endpoint: localhost:9000
bucket: qtap-captures
region: us-east-1
insecure: true # Only for local MinIO testing
access_key:
type: text
value: minioadmin
secret_key:
type: text
value: minioadmin
stacks:
production_stack:
plugins:
- type: http_capture
config:
level: none # (none|summary|details|full) - Default: don't capture
format: json # (json|text)
rules:
# Only capture errors (saves S3 costs)
- name: "Capture errors"
expr: is_error()
level: full # (none|summary|details|full)
tap:
direction: egress # (egress|egress-external|egress-internal|ingress|all)
ignore_loopback: true # (true|false)
audit_include_dns: false # (true|false)
http:
stack: production_stack
filters:
groups:
- qpoint
custom:
- exe: /usr/bin/curl
strategy: exact # (exact|prefix|regex)
What's different:
S3 object_store: Captured HTTP transactions go to MinIO
stdout event_store: Connection metadata still goes to console
Error-only capture: Only store failures (reduces costs)
Step 3: Start Qtap with S3
docker run -d \
--name qtap-level4-s3 \
--user 0:0 \
--privileged \
--cap-add CAP_BPF \
--cap-add CAP_SYS_ADMIN \
--pid=host \
--network=host \
-v /sys:/sys \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/qtap-level4-s3.yaml:/app/config/qtap.yaml" \
-e TINI_SUBREAPER=1 \
--ulimit=memlock=-1 \
us-docker.pkg.dev/qpoint-edge/public/qtap:v0 \
--log-level=info \
--log-encoding=console \
--config="/app/config/qtap.yaml"
sleep 6
Step 4: Test S3 Storage
Generate an error (will be stored in S3):
docker run --rm alpine sh -c "wget -O- http://httpbin.org/status/500 2>/dev/null" || true
sleep 2
Check qtap logs for S3 upload confirmation:
docker logs qtap-level4-s3 2>&1 | grep -i "s3\|upload\|object"
List objects in MinIO:
docker run --rm \
--network=host \
--entrypoint /bin/sh \
quay.io/minio/minio -c "
mc alias set local http://localhost:9000 minioadmin minioadmin &&
mc ls local/qtap-captures/
"
You should see files with SHA digest names - these are your captured HTTP transactions!
Step 5: Retrieve a Captured Transaction
Get the digest from qtap logs:
DIGEST=$(docker logs qtap-level4-s3 2>&1 | grep '"digest":' | head -1 | grep -o '"digest":"[^"]*"' | cut -d'"' -f4)
echo "Digest: $DIGEST"
Download the object from MinIO:
docker run --rm \
--network=host \
--entrypoint /bin/sh \
quay.io/minio/minio -c "
mc alias set local http://localhost:9000 minioadmin minioadmin &&
mc cat local/qtap-captures/$DIGEST
"
You'll see the full HTTP transaction - request headers, response headers, and body!
Understanding S3 Configuration
S3 Parameters:
endpoint
S3 server address
s3.amazonaws.com
or localhost:9000
bucket
Where to store objects
qtap-captures
region
S3 region
us-east-1
insecure
Allow HTTP (MinIO only!)
true
for local, false
for production
access_key
S3 credentials
From env or text
secret_key
S3 credentials
From env or text
Production S3 Config (AWS):
object_stores:
- type: s3
endpoint: s3.amazonaws.com
bucket: my-company-qtap
region: us-west-2
insecure: false # Always false in production!
access_key:
type: env # Use environment variables
value: AWS_ACCESS_KEY_ID
secret_key:
type: env
value: AWS_SECRET_ACCESS_KEY
Then run qtap with:
docker run ... \
-e AWS_ACCESS_KEY_ID=your_key \
-e AWS_SECRET_ACCESS_KEY=your_secret \
...
Production Best Practices
Storage Strategy:
✅ Use S3 for object storage (HTTP transactions)
✅ Use stdout or logging service for events (metadata)
✅ Only capture what you need (errors, slow requests)
✅ Set S3 lifecycle rules to auto-delete old data
Security:
✅ Always use
insecure: false
in production✅ Use environment variables for credentials (never hardcode)
✅ Enable S3 bucket encryption
✅ Restrict S3 bucket access with IAM policies
Cost Optimization:
✅ Use
level: none
by default, capture via rules only✅ Capture errors at
full
level (need details to debug)✅ Capture success at
summary
ordetails
level✅ Use S3 lifecycle policies to transition to cheaper storage tiers
S3 Lifecycle Example
For AWS S3, set up lifecycle rules:
{
"Rules": [
{
"Id": "TransitionOldCaptures",
"Status": "Enabled",
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365
}
}
]
}
This:
Keeps data in standard storage for 30 days
Moves to infrequent access after 30 days (cheaper)
Archives to Glacier after 90 days (very cheap)
Deletes after 1 year (compliance)
Clean Up
# Stop qtap
docker rm -f qtap-level4-s3
# Stop MinIO (optional - keeps your test data)
docker rm -f minio
Congratulations! 🎉
You've completed the Ultimate Qtap Setup Guide. You now know how to:
Level 1 - Basics:
✅ Configure qtap to capture HTTP/HTTPS traffic
✅ Verify qtap can see inside TLS connections
✅ Understand qtap output format
Level 2 - Filtering:
✅ Filter processes to reduce noise
✅ Apply different capture levels by domain
✅ Use different capture levels for different endpoints
Level 3 - Conditional Capture:
✅ Use rulekit expressions for intelligent capture
✅ Create reusable macros
✅ Capture only errors and specific request types
✅ Filter by container and pod labels
Level 4 - Production Storage:
✅ Configure S3-compatible object storage
✅ Combine stdout and S3 for debugging + persistence
✅ Implement cost-effective storage strategies
✅ Follow security best practices
Next Steps
Explore More:
Storage Configuration - Full S3 reference
Traffic Processing with Plugins - Plugin details
Rulekit Documentation - Advanced expressions
Get Help:
Report issues on GitHub
All examples in this guide have been validated and tested. Every configuration is guaranteed to work.
Last updated