Detonate manual
Detonate is a behavioral detonation platform for software supply chain security. It takes the artifacts you depend on — npm packages, Python libraries, Java JARs, binaries and Docker images — and runs them inside completely isolated micro-virtual machines to observe what they actually do.
Every file the artifact reads, every network connection it makes, every process it spawns is captured and classified. If it tries to steal your SSH keys, exfiltrate credentials, or phone home to a command-and-control server — you'll know before it ever touches production.
How does it work?
When you submit an artifact (e.g. npm install some-package), Detonate:
- Spins up a micro-virtual machine in ~125 milliseconds using Firecracker — the same technology AWS uses for Lambda and Fargate
- Plants honeypot files inside the VM — fake SSH keys, AWS credentials, API tokens, .env files. If the artifact touches any of them, that's an instant red flag
- Installs and runs your artifact inside the VM, just like it would run on a real server
- Monitors everything — file access, network connections, DNS queries, process execution, credential hunting
- Generates a verdict: benign, suspicious, or policy-violating — with a full event timeline explaining why
The artifact sees a real Linux machine with real files, real network, and realistic credentials to steal. It has no way to detect that it's being observed.
Why Firecracker?
Most sandboxing tools use Docker containers or ptrace-based tracing (like strace). Sophisticated malware can detect both — by checking /.dockerenv, inspecting /proc/1/cgroup, or measuring the timing overhead of syscall interception.
Detonate uses Firecracker microVMs instead. Firecracker is an open-source virtual machine monitor created by Amazon for running AWS Lambda. Each detonation runs inside a real virtual machine with its own Linux kernel, backed by hardware-level KVM isolation. The artifact sees:
- A real Linux kernel and real
/procfilesystem — not a container - No
/.dockerenv, no container cgroup markers - No strace overhead or
TracerPidin/proc/self/status - Realistic CPU count, memory, hostname, and user environment
- Convincing fake credentials that look like a real production server
Firecracker boots in ~125ms, uses minimal resources (~2MB memory overhead), and provides the same isolation guarantees as a full virtual machine. If malware can escape a Firecracker VM, that's a KVM kernel bug — not a misconfigured Docker socket.
Requirements
| Component | Requirement |
|---|---|
| OS | Linux (x86_64) with KVM support |
| CPU | Intel VT-x or AMD-V (check with egrep -c '(vmx|svm)' /proc/cpuinfo) |
| KVM | /dev/kvm must exist and be accessible |
| Docker | Docker 24+ (for containerized deployment) |
| RAM | 4GB minimum (each microVM uses 2GB by default) |
| Disk | ~2GB for Firecracker + kernel + rootfs images |
| Database | SQLite (default) or PostgreSQL 14+ |
Quick start
Everything is baked into the Docker image — Firecracker, kernel, rootfs images and the monitoring agent. One command, 20 seconds:
# That's it. Everything is included.
docker run -d \
--name detonate \
-p 3000:3000 \
--device /dev/kvm:/dev/kvm \
finsys/detonate:latest
Then open http://your-host:3000, create your admin account, and submit your first artifact.
--device /dev/kvm flag passes your CPU's hardware virtualization to the container. Your host must be a Linux machine (bare metal or VM with nested virtualization enabled) with an Intel VT-x or AMD-V capable CPU.
Custom Firecracker installation
Firecracker is a single static binary (~3MB) with zero dependencies. No package manager, no runtime, no configuration files — just one executable that talks directly to your CPU's virtualization hardware.
# Create directories
sudo mkdir -p /opt/detonate/{rootfs,data}
sudo chown -R $(whoami):$(whoami) /opt/detonate
# Download Firecracker v1.11.0 (latest stable)
ARCH=$(uname -m) # x86_64 or aarch64
VERSION="v1.11.0"
curl -fsSL "https://github.com/firecracker-microvm/firecracker/releases/download/${VERSION}/firecracker-${VERSION}-${ARCH}.tgz" \
-o /tmp/firecracker.tgz
# Extract and install
tar xzf /tmp/firecracker.tgz -C /tmp
cp /tmp/release-${VERSION}-${ARCH}/firecracker-${VERSION}-${ARCH} /opt/detonate/firecracker
chmod +x /opt/detonate/firecracker
# Verify
/opt/detonate/firecracker --version
# Clean up
rm -rf /tmp/firecracker.tgz /tmp/release-${VERSION}-${ARCH}
Kernel image
Every virtual machine needs a Linux kernel to boot. Detonate uses a minimal kernel image (~21MB) that's optimized for fast startup. This is not your host's kernel — it runs only inside the microVM.
# Download the recommended kernel
curl -fsSL "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/x86_64/kernels/vmlinux.bin" \
-o /opt/detonate/vmlinux
# Verify
ls -lh /opt/detonate/vmlinux # should be ~21MB
CONFIG_VIRTIO_* and CONFIG_VSOCK enabled for smaller images (~5MB). See the Firecracker docs.
Rootfs images
When Firecracker boots a microVM, it needs a filesystem to use as the VM's "hard drive". These are called rootfs images — each one is a self-contained Linux environment tailored to a specific artifact type.
Think of them like Docker images, but for virtual machines. Each rootfs contains:
- A complete Linux userspace with the right runtime (Node.js for npm, Python for PyPI, JRE for Maven, etc.)
- The Detonate monitoring agent — a small program that watches everything the artifact does
- A realistic-looking file system with user directories, config files, and planted honeypot credentials
| Image | Runtime | Size | Used for |
|---|---|---|---|
rootfs-npm.ext4 | Node.js 20 | ~500MB | npm packages |
rootfs-pypi.ext4 | Python 3.12 | ~400MB | Python packages (pip, wheels) |
rootfs-maven.ext4 | Java 21 (JRE) | ~400MB | Java JARs and Maven artifacts |
rootfs-binary.ext4 | Ubuntu 24.04 | ~200MB | Binary executables (ELF, scripts) |
Building rootfs images
Detonate ships with a build system that creates rootfs images from standard Docker base images. You need Go 1.21+, Docker, and e2fsprogs (for mkfs.ext4).
# Install build dependencies
sudo apt install -y golang-go e2fsprogs docker.io
# From the detonate repository
cd sandbox/
# Build everything at once
make build-all
# Or build individually
make build-agent # compile the monitoring agent (~5MB)
make build-rootfs-npm # Node.js environment (~500MB)
make build-rootfs-pypi # Python environment (~400MB)
make build-rootfs-maven # Java environment (~400MB)
make build-rootfs-binary # Ubuntu base (~200MB)
# Install to /opt/detonate/rootfs/
make install
Custom rootfs images
You can create rootfs images from any Docker image. This is useful if your artifacts need specific runtimes, libraries, or tools that aren't in the default images.
# Export any Docker image as a filesystem
docker create --name temp your-custom-image:latest
docker export temp | gzip > custom-rootfs.tar.gz
docker rm temp
# Create an ext4 filesystem image
dd if=/dev/zero of=rootfs-custom.ext4 bs=1M count=1024
mkfs.ext4 rootfs-custom.ext4
# Mount and populate
sudo mkdir -p /mnt/rootfs
sudo mount rootfs-custom.ext4 /mnt/rootfs
sudo tar xzf custom-rootfs.tar.gz -C /mnt/rootfs
# Add the Detonate monitoring agent
sudo cp detonate-agent /mnt/rootfs/usr/local/bin/
sudo mkdir -p /mnt/rootfs/etc/detonate
# Finalize
sudo umount /mnt/rootfs
mv rootfs-custom.ext4 /opt/detonate/rootfs/
Verify installation
Check that everything is in place:
ls -lh /opt/detonate/
# Expected:
# firecracker 2.6M (the binary)
# vmlinux 21M (kernel image)
# rootfs/ 1.5G (directory with ext4 images)
# rootfs-npm.ext4
# rootfs-pypi.ext4
# rootfs-maven.ext4
# rootfs-binary.ext4
# Quick smoke test — start and immediately stop a VM
/opt/detonate/firecracker --api-sock /tmp/fc-test.sock &
FC_PID=$!
sleep 1
kill $FC_PID
rm -f /tmp/fc-test.sock
echo "Firecracker works!"
Docker deployment
The recommended way to run Detonate. The Docker image includes everything — Firecracker, kernel, rootfs images, monitoring agent. Just add KVM.
# Minimal — uses SQLite, everything self-contained
docker run -d \
--name detonate \
--restart unless-stopped \
-p 3000:3000 \
--device /dev/kvm:/dev/kvm \
finsys/detonate:latest
# With PostgreSQL and persistent data
docker run -d \
--name detonate \
--restart unless-stopped \
-p 3000:3000 \
--device /dev/kvm:/dev/kvm \
-v detonate-data:/app/data \
-e DATABASE_URL=postgres://user:pass@db-host:5432/detonate \
finsys/detonate:latest
Environment variables
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | HTTP port |
DATABASE_URL | none (SQLite) | PostgreSQL connection string. Omit to use SQLite. |
DATA_DIR | /app/data | SQLite database and upload storage |
FIRECRACKER_BIN, FIRECRACKER_KERNEL, FIRECRACKER_ROOTFS_DIR) are pre-configured inside the image. Override them only if you want to use custom binaries or rootfs images mounted from the host.
Bare metal deployment
# Install bun
curl -fsSL https://bun.sh/install | bash
# Clone and build
git clone https://gitea.bor6.pl/jarek/detonate.git
cd detonate
bun install
bun run build
# Run
DATABASE_URL=postgres://user:pass@localhost:5432/detonate \
node build/index.js
Reverse proxy
Traefik (file provider)
http:
routers:
detonate:
rule: "Host(`detonate.example.com`)"
entryPoints:
- websecure
service: detonate
tls:
certResolver: letsencrypt
services:
detonate:
loadBalancer:
servers:
- url: "http://192.168.1.113:3000"
Caddy
detonate.example.com {
reverse_proxy localhost:3000
}
Nginx
server {
listen 443 ssl;
server_name detonate.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Database setup
Detonate supports both SQLite and PostgreSQL. Migrations run automatically on startup.
SQLite (default)
No configuration needed. The database is created at $DATA_DIR/detonate.db on first start.
PostgreSQL
# Create the database
psql -U postgres -c "CREATE DATABASE detonate;"
# Set the connection string
export DATABASE_URL=postgres://user:pass@host:5432/detonate
# Tables are created automatically on first start
First login
On first visit, Detonate enters setup mode. Create your admin account with a username and password (min 8 characters). After that, you'll be redirected to the dashboard.
Submitting artifacts
Go to Submit in the sidebar. Choose the artifact type, enter the source (package name, URL, or upload a file), select quarantine duration and policy profile, then hit Detonate in cage.
| Type | Source format | Example |
|---|---|---|
| npm | Package name with version | lodash@4.17.21 |
| PyPI | Package name | requests==2.31.0 |
| Maven | Group:artifact:version | org.apache.commons:commons-lang3:3.14.0 |
| Binary | File upload | Any ELF/executable |
| Docker | Image reference | nginx:latest |
Deception packs
Detonate plants fake credentials inside every cage. If the artifact reads any of these honeypot files, it's flagged as a deception hit with critical severity.
| Pack | What's planted |
|---|---|
| SSH keys | Fake id_rsa, id_ed25519, known_hosts, SSH config |
| AWS credentials | Fake ~/.aws/credentials and config |
| npm/yarn tokens | Fake .npmrc, .yarnrc.yml with auth tokens |
| PyPI tokens | Fake .pypirc with upload credentials |
| Git credentials | Fake .git-credentials, .gitconfig |
| Cloud metadata | Fake GCP service account, Azure profile |
| Docker config | Fake ~/.docker/config.json with registry auth |
| Kubernetes | Fake kubeconfig with cluster endpoints |
| Environment files | Fake .env with DB URLs, API keys, Stripe keys |
Policies and whitelists
Policies define what's allowed during detonation. Create whitelist rules for file paths, IP addresses, DNS domains and ports. Group rules into reusable policy profiles.
Rule types:
- file_path — exact path match
- file_glob — glob pattern (e.g.
/tmp/**) - ip_address — single IP
- ip_cidr — CIDR range (e.g.
10.0.0.0/8) - dns_domain — exact domain
- dns_wildcard — wildcard (e.g.
*.npmjs.org) - port — port number
Understanding verdicts
| Verdict | Meaning | Typical triggers |
|---|---|---|
| Benign | No suspicious behavior detected | Normal file access, expected network activity |
| Suspicious | Some anomalous behavior, needs review | Accessing sensitive paths, unexpected outbound connections, anti-evasion signals |
| Policy-violating | Clear malicious intent | Deception file access, credential exfiltration, C2 communication |
Reports and exports
From any cage detail page, click the Report dropdown to download:
- JSON — full structured report with all events
- CSV — flat event table for spreadsheets/SIEM import
- Text — human-readable summary report
API tokens
Go to API tokens in the sidebar. Create a token with the permissions you need (submit, read, admin). The token is shown once at creation — copy it immediately.
# Token format
dt_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789ab
Submitting via API
curl -X POST https://detonate.example.com/api/v1/submit \
-H "Authorization: Bearer dt_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"artifactType": "npm",
"source": "suspicious-package@1.0.0",
"quarantineDuration": 600
}'
# Response: { "id": 42, "status": "pending", "message": "..." }
Polling verdicts
# Check status
curl https://detonate.example.com/api/v1/status/42 \
-H "Authorization: Bearer dt_YOUR_TOKEN"
# Get verdict (for pipeline pass/fail)
curl https://detonate.example.com/api/v1/verdict/42 \
-H "Authorization: Bearer dt_YOUR_TOKEN"
# Response: { "id": 42, "verdict": "benign", "pass": true, "completed": true }
# Download full report
curl https://detonate.example.com/api/v1/report/42?format=json \
-H "Authorization: Bearer dt_YOUR_TOKEN"
GitHub Actions
- name: Detonate in Detonate
run: |
CAGE_ID=$(curl -s -X POST \
$DETONATE_URL/api/v1/submit \
-H "Authorization: Bearer $TC_TOKEN" \
-H "Content-Type: application/json" \
-d '{"artifactType":"npm","source":"$PACKAGE","quarantineDuration":600}' \
| jq -r '.id')
# Poll until complete
while true; do
RESULT=$(curl -s $DETONATE_URL/api/v1/verdict/$CAGE_ID \
-H "Authorization: Bearer $TC_TOKEN")
if [ "$(echo $RESULT | jq -r '.completed')" = "true" ]; then
if [ "$(echo $RESULT | jq -r '.pass')" != "true" ]; then
echo "BLOCKED: $(echo $RESULT | jq -r '.verdict')"
exit 1
fi
echo "PASSED: benign"
break
fi
sleep 30
done
env:
DETONATE_URL: ${{ secrets.DETONATE_URL }}
TC_TOKEN: ${{ secrets.TC_TOKEN }}
GitLab CI
detonate-scan:
stage: test
image: curlimages/curl:latest
script:
- CAGE_ID=$(curl -s -X POST
"$DETONATE_URL/api/v1/submit"
-H "Authorization: Bearer $TC_TOKEN"
-H "Content-Type: application/json"
-d "{\"artifactType\":\"npm\",\"source\":\"$PACKAGE\"}"
| jq -r '.id')
- |
while true; do
VERDICT=$(curl -s "$DETONATE_URL/api/v1/verdict/$CAGE_ID"
-H "Authorization: Bearer $TC_TOKEN")
COMPLETED=$(echo "$VERDICT" | jq -r '.completed')
[ "$COMPLETED" = "true" ] && break
sleep 30
done
- PASS=$(echo "$VERDICT" | jq -r '.pass')
- '[ "$PASS" = "true" ] || exit 1'
How it works
Detonate controller (SvelteKit + Node.js)
│
├── Firecracker API (REST on Unix socket)
│ → microVM boots in ~125ms
│ ├── Custom init agent (Go binary)
│ ├── fanotify → file access monitoring
│ ├── iptables LOG → network monitoring
│ ├── /proc polling → process monitoring
│ ├── Deception honeypots planted
│ └── Artifact executed per type
│
├── virtio-vsock ← event stream (JSONL)
│ → Collector parses, classifies, batches to DB
│
├── Timeout → kill VM → generate verdict
│
└── SSE → real-time events to UI
Why Firecracker
We chose Firecracker microVMs over Docker containers or gVisor for one reason: stealth.
- The artifact sees a real Linux kernel, real
/proc, real hardware — not a container - No
/.dockerenv, no container cgroup markers, noTracerPidfrom strace - No measurable timing overhead (unlike ptrace/strace)
- Boot time ~125ms, minimal resource overhead
- Hardware-level KVM isolation — VM escape is a KVM bug, not a container misconfiguration
In-VM agent
A small Go binary (~5MB) runs as PID 1 inside each microVM. It:
- Mounts
/proc,/sys,/dev,/tmp - Disguises its own process name (see below)
- Creates a realistic environment (hostname, users, /etc/hosts)
- Plants deception files from the cage config
- Starts file monitoring via
fanotify - Sets up network logging via
iptables -j LOG - Connects to the host via virtio-vsock
- Executes the artifact (
npm install,pip install,java -jar, etc.) - Streams all events as JSONL to the host until killed
Agent process disguise
Sophisticated malware may inspect /proc/1/comm, /proc/1/cmdline, or run ps to identify monitoring agents. To counter this, Detonate disguises the agent on every detonation:
- The agent binary is installed in the rootfs under
/usr/lib/systemd/systemd-journald— a path that exists on every systemd-based Linux system - At boot, the host picks a random process name from a pool of common system processes (
bash,sshd,cron,rsyslogd,dbus-daemon,agetty,polkitd, etc.) and passes it to the agent via config - The agent applies the disguise using
prctl(PR_SET_NAME), changing what appears in/proc/self/commandpsoutput - The chosen disguise name is logged on the host for each cage:
[sandbox] cage 5 agent disguise: rsyslogd
This means every detonation has a different-looking PID 1, and there are no files named "detonate" visible anywhere inside the VM.
Anti-evasion detection
Detonate detects artifacts that try to fingerprint the sandbox:
| Technique | What we detect |
|---|---|
| Long sleep | sleep/nanosleep calls > 60 seconds (delayed payload) |
| Environment fingerprinting | Reading /proc/cpuinfo, /sys/class/dmi, VM indicators |
| Sandbox detection | Checking for /.dockerenv, container cgroup markers |
| Delayed callbacks | Outbound connections > 5 minutes after start |
| Timing attacks | Rapid repeated timing-sensitive syscalls |
Since we use Firecracker (not Docker), most sandbox detection techniques fail — the artifact sees a real Linux machine.