Table of Contents
- REST API Reference
- Roles
- Endpoint summary
- Authentication
- Logs
- Bounty
- Observability
- Fleet
- GET /deckies
- POST /deckies/deploy
- POST /deckies/{decky_name}/mutate
- PUT /deckies/{decky_name}/mutate-interval
- Attackers
- Configuration
- GET /config
- PUT /config/deployment-limit
- PUT /config/global-mutation-interval
- POST /config/users
- DELETE /config/users/{user_uuid}
- PUT /config/users/{user_uuid}/role
- PUT /config/users/{user_uuid}/reset-password
- DELETE /config/reinit
- Artifacts
REST API Reference
The DECNET web API is a FastAPI application mounted under the /api/v1 prefix.
All JSON endpoints return application/json and use ORJSONResponse internally.
Authentication is JWT bearer (Authorization: Bearer <token>) except where noted.
Related pages: Web dashboard, Environment variables.
Roles
viewer— read-only access to logs, attackers, stats, stream, health, bounty, deckies, config (non-admin fields).admin— everythingviewercan do, plus mutating endpoints (deploy, mutate, user CRUD, config writes, artifact download).
require_viewer admits both roles. require_admin is admin-only. require_stream_viewer
is the SSE variant of viewer (accepts the token via query string or header).
Endpoint summary
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /auth/login |
public | Obtain a JWT access token |
| POST | /auth/change-password |
JWT | Change current user's password |
| GET | /logs |
viewer | Paginated log feed with filters |
| GET | /logs/histogram |
viewer | Time-bucketed log counts |
| GET | /bounty |
viewer | Paginated bounty vault |
| GET | /stats |
viewer | Aggregate telemetry |
| GET | /stream |
viewer | SSE live event stream |
| GET | /deckies |
viewer | Full fleet inventory |
| POST | /deckies/deploy |
admin | Deploy fleet from INI payload |
| POST | /deckies/{decky_name}/mutate |
admin | Force immediate mutation |
| PUT | /deckies/{decky_name}/mutate-interval |
admin | Set per-decky mutation interval |
| GET | /attackers |
viewer | Paginated attacker profiles |
| GET | /attackers/{uuid} |
viewer | Single attacker with behavior block |
| GET | /attackers/{uuid}/commands |
viewer | Paginated per-attacker commands |
| GET | /attackers/{uuid}/artifacts |
viewer | Captured file-drop log rows |
| GET | /config |
viewer | Config (users list admin-only) |
| PUT | /config/deployment-limit |
admin | Cap concurrent deckies |
| PUT | /config/global-mutation-interval |
admin | Fleet-wide mutation cadence |
| POST | /config/users |
admin | Create user |
| DELETE | /config/users/{user_uuid} |
admin | Delete user |
| PUT | /config/users/{user_uuid}/role |
admin | Change user role |
| PUT | /config/users/{user_uuid}/reset-password |
admin | Force password reset |
| DELETE | /config/reinit |
admin+dev | Purge logs and bounties (developer mode only) |
| GET | /artifacts/{decky}/{stored_as} |
admin | Download captured SSH artifact bytes |
| GET | /health |
viewer | Component health; returns 503 when critical fails |
Authentication
POST /auth/login
Exchange username/password for a JWT.
Request body (LoginRequest):
{ "username": "admin", "password": "hunter2" }
Response (Token):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"must_change_password": false
}
Errors: 401 bad credentials, 422 validation.
curl -s -X POST http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"hunter2"}'
POST /auth/change-password
Rotate the calling user's own password. Clears must_change_password.
Request body (ChangePasswordRequest):
{ "old_password": "hunter2", "new_password": "newpass123" }
Response: {"message": "Password updated successfully"}
curl -s -X POST http://localhost:8000/api/v1/auth/change-password \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"old_password":"hunter2","new_password":"newpass123"}'
Logs
GET /logs
Paginated log feed. Unfiltered total count is cached for 2 s.
Query params:
limit(int, 1..1000, default 50)offset(int, 0.., default 0)search(str, optional)start_time,end_time(ISO-8601, optional)
Response (LogsResponse):
{
"total": 1423,
"limit": 50,
"offset": 0,
"data": [
{
"id": 1423,
"timestamp": "2026-04-18T02:22:56Z",
"decky": "decky-03",
"service": "ssh",
"event_type": "auth_fail",
"attacker_ip": "203.0.113.9",
"msg": "Failed password for root",
"fields": "{\"user\":\"root\"}"
}
]
}
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/api/v1/logs?limit=50&search=ssh'
GET /logs/histogram
Time-bucketed event counts. Default unfiltered response is cached for 5 s.
Query params: search, start_time, end_time, interval_minutes (int ge 1, default 15).
Response: list[{bucket, count}].
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/api/v1/logs/histogram?interval_minutes=5'
Bounty
GET /bounty
Paginated bounty vault (harvested credentials, payloads, etc.). Default unfiltered page is cached for 5 s.
Query params: limit (1..1000), offset, bounty_type, search.
Response (BountyResponse): same shape as /logs.
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/api/v1/bounty?bounty_type=credential'
Observability
GET /stats
Aggregate telemetry. Cached for 5 s.
Response (StatsResponse):
{ "total_logs": 14231, "unique_attackers": 47, "active_deckies": 5, "deployed_deckies": 5 }
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/stats
GET /stream
Server-Sent Events stream. Emits stats, histogram, and logs messages.
Query params: lastEventId (int), search, start_time, end_time,
maxOutput (developer mode only — terminates after N chunks).
Each event is a JSON payload inside an SSE data: line, e.g.:
event: message
data: {"type":"logs","data":[{...}]}
curl -N -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/api/v1/stream?lastEventId=0'
GET /health
Component health report. Returns HTTP 503 when a critical component
(database, docker, ingestion_worker) is failing.
Response (HealthResponse):
{
"status": "healthy",
"components": {
"database": {"status": "ok"},
"docker": {"status": "ok"},
"ingestion_worker": {"status": "ok"},
"collector_worker": {"status": "ok"},
"attacker_worker": {"status": "ok"},
"sniffer_worker": {"status": "ok"}
}
}
Overall tiers: healthy | degraded (non-critical failure) | unhealthy.
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/health
Fleet
GET /deckies
Full fleet inventory (cached 5 s).
Response: list[{name, ip, mac, services, status, mutate_interval, ...}].
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/deckies
POST /deckies/deploy
Deploy or extend the fleet from an INI payload. See INI config format.
Request body (DeployIniRequest):
{ "ini_content": "[general]\ninterface=eth0\n\n[decky-01]\nservices=ssh,smb\n" }
Response: {"message": "..."}
Errors: 409 network/INI conflict or deployment-limit exceeded, 422
schema error, 500 deploy failure.
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d @deploy.json http://localhost:8000/api/v1/deckies/deploy
POST /deckies/{decky_name}/mutate
Force an immediate mutation of a single decky. Name must match ^[a-z0-9\-]{1,64}$.
Response: {"message": "Successfully mutated decky-03"} or 404.
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/deckies/decky-03/mutate
PUT /deckies/{decky_name}/mutate-interval
Set per-decky mutation cadence. mutate_interval is <number><unit>
where unit is m (minutes), d (days), M (months), y/Y (years).
Pass null to clear.
Request body (MutateIntervalRequest):
{ "mutate_interval": "5d" }
Errors: 404 no active deployment, 422 invalid duration.
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"mutate_interval":"5d"}' \
http://localhost:8000/api/v1/deckies/decky-03/mutate-interval
Attackers
GET /attackers
Paginated attacker profiles, bulk-joined to their behavior block.
Query params: limit (1..1000), offset, search, service,
sort_by (recent | active | traversals, default recent).
Response (AttackersResponse): paginated envelope with
data[i].behavior populated for each IP.
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:8000/api/v1/attackers?sort_by=active'
GET /attackers/{uuid}
Single attacker with behavior block attached. 404 when not found.
curl -s -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/attackers/$UUID
GET /attackers/{uuid}/commands
Paginated commands issued by an attacker.
Query params: limit (1..200), offset, service.
Response: {total, limit, offset, data}.
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/api/v1/attackers/$UUID/commands?service=ssh"
GET /attackers/{uuid}/artifacts
List captured file_captured log rows for an attacker (newest first).
Response: {total, data: [log_row, ...]}.
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/api/v1/attackers/$UUID/artifacts"
Configuration
GET /config
Get current configuration. Admins additionally receive the full users list
and (in developer mode) a developer_mode: true flag.
Response (viewer):
{ "role": "viewer", "deployment_limit": 10, "global_mutation_interval": "30m" }
Response (admin adds):
{ "users": [ { "uuid": "...", "username": "admin", "role": "admin", "must_change_password": false } ] }
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/config
PUT /config/deployment-limit
Body (DeploymentLimitRequest): {"deployment_limit": 25} (int, 1..500).
PUT /config/global-mutation-interval
Body (GlobalMutationIntervalRequest): {"global_mutation_interval": "1d"}
(pattern ^[1-9]\d*[mdMyY]$).
POST /config/users
Create a user. New users always land with must_change_password=true.
Body (CreateUserRequest):
{ "username": "alice", "password": "initialpass1", "role": "viewer" }
Response (UserResponse): {uuid, username, role, must_change_password}.
Errors: 409 duplicate username.
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"initialpass1","role":"viewer"}' \
http://localhost:8000/api/v1/config/users
DELETE /config/users/{user_uuid}
Delete a user. You cannot delete yourself (403).
PUT /config/users/{user_uuid}/role
Body (UpdateUserRoleRequest): {"role": "admin"}. You cannot change
your own role (403).
PUT /config/users/{user_uuid}/reset-password
Body (ResetUserPasswordRequest): {"new_password": "newpass1234"}.
Target user is forced into must_change_password=true.
DELETE /config/reinit
Purge all logs and bounties. Requires admin and DECNET_DEVELOPER=true;
otherwise 403.
Response: {"message": "Data purged", "deleted": {"logs": N, "bounties": M}}.
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/config/reinit
Artifacts
GET /artifacts/{decky}/{stored_as}
Download the raw bytes of a captured SSH drop. Admin-only because the payloads are attacker-controlled content.
Path parameters:
decky—^[a-z0-9][a-z0-9-]{0,62}$stored_as—^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z_[a-f0-9]{12}_[A-Za-z0-9._-]{1,255}$
The path is resolved under DECNET_ARTIFACTS_ROOT
(default /var/lib/decnet/artifacts) and confined via resolve()
to prevent traversal.
Returns a FileResponse with application/octet-stream.
Errors: 400 invalid name, 404 missing file.
curl -s -OJ -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/api/v1/artifacts/decky-03/2026-04-18T02:22:56Z_abcdef012345_payload.sh"
DECNET
User docs
- Quick-Start
- Installation
- Requirements-and-Python-Versions
- CLI-Reference
- INI-Config-Format
- Custom-Services
- Services-Catalog
- Service-Personas
- Archetypes
- Distro-Profiles
- OS-Fingerprint-Spoofing
- Networking-MACVLAN-IPVLAN
- Deployment-Modes
- SWARM-Mode
- MazeNET
- Remote-Updates
- Environment-Variables
- Teardown-and-State
- Database-Drivers
- Systemd-Setup
- Logging-and-Syslog
- Service-Bus
- Web-Dashboard
- REST-API-Reference
- Mutation-and-Randomization
- Troubleshooting
Developer docs
DECNET — honeypot deception-network framework. Pre-1.0, active development — use with caution. See Sponsors to support the project. Contact: samuel@securejump.cl