Secrets Management
DjinnBot includes a built-in secrets management system for storing sensitive values — API keys, tokens, SSH keys, and other credentials — that agents need during execution.
How It Works
Secrets are stored in PostgreSQL, encrypted at rest with AES-256-GCM. The encryption scheme uses:
- 256-bit key derived from the
SECRET_ENCRYPTION_KEYenvironment variable - Random 96-bit nonce per encryption operation
- 128-bit authentication tag (GCM default)
- Storage format:
base64(nonce[12] + tag[16] + ciphertext)
Most API endpoints return only masked previews (e.g., ghp_...abc1). However, the engine injection endpoint (/v1/secrets/agents/{agent_id}/env) returns plaintext values — see the security note below.
Secret Types
Secrets support different types to help organize credentials:
- API keys — third-party service credentials
- Tokens — GitHub PATs, OAuth tokens, etc.
- SSH keys — for git operations or server access
- Custom — any sensitive value
Agent Access Control
Secrets are not globally available to all agents. You explicitly grant each secret to specific agents:
- Create a secret in the dashboard or via API
- Grant the secret to one or more agents
- Only granted agents receive the secret in their container environment
This prevents agents from accessing credentials they don’t need.
How Secrets Reach Agents
When the engine spawns an agent container:
- It calls
GET /v1/secrets/agents/{agent_id}/envon the API server - The API decrypts the agent’s granted secrets and returns them as a plaintext environment variable map
- The engine injects these as environment variables into the container
This endpoint is protected by the ENGINE_INTERNAL_TOKEN shared secret. The engine and agent containers send the token in the Authorization: Bearer <token> header. Without a valid token, the endpoint returns 403 Forbidden.
If ENGINE_INTERNAL_TOKEN is not set, the /env endpoint is unprotected for backward compatibility and local development. Always set this variable in production. Generate one with:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"The token flows as follows:
- You set
ENGINE_INTERNAL_TOKENin.env - Docker Compose passes it to the API server and engine containers
- The engine injects it into agent containers as an environment variable
- All three (engine, agent container, API) share the same secret
- The API validates the token on the
/envendpoint only — all other endpoints remain open
Managing Secrets
Via Dashboard
- Go to Settings in the dashboard
- Navigate to the Secrets section
- Create, update, or delete secrets
- Grant or revoke agent access
Via API
# List secrets (masked, no plaintext)
GET /v1/secrets/
# Create a secret
POST /v1/secrets/
{
"name": "GITHUB_TOKEN",
"description": "GitHub PAT for repo access",
"secret_type": "token",
"value": "ghp_actual_secret_value"
}
# Grant to an agent
POST /v1/secrets/{secret_id}/grant/{agent_id}
# Revoke from an agent
DELETE /v1/secrets/{secret_id}/grant/{agent_id}
# List secrets granted to an agent (masked)
GET /v1/secrets/agents/{agent_id}
# Delete a secret (and all its grants)
DELETE /v1/secrets/{secret_id}Encryption Key
The encryption key is configured via the SECRET_ENCRYPTION_KEY environment variable:
# Generate a strong key
python3 -c "import secrets; print(secrets.token_hex(32))"SECRET_ENCRYPTION_KEY set, the system auto-generates an ephemeral key on first use. This key is lost if the database is reset or the service restarts without persisting it, making all stored secrets permanently unrecoverable. Always set this variable in production.Security Properties
- Secrets are encrypted before touching the database — PostgreSQL never sees plaintext
- Each encryption uses a unique random nonce — identical secrets produce different ciphertexts
- GCM authentication tags prevent tampering — any modification is detected on decryption
- Most API endpoints return only masked previews — you can verify a secret exists without seeing its value
- The plaintext injection endpoint (
/env) is protected byENGINE_INTERNAL_TOKEN— without it, callers get403 Forbidden - Agent containers receive secrets only at spawn time via environment variables — they’re not written to disk inside the container