NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →

Security

This page is for operators securing a Dakera v0.12.3 deployment. It covers authentication and permissions, encryption at rest and key rotation, cluster and gRPC authentication, CORS, rate limits, input validation, audit logging, and a production checklist.

Every request passes through rate limiting, authentication and input validation before it reaches the engine. Memory content can be encrypted at rest with AES-256-GCM, and every request is written to an audit log.

Security layers

LayerProtectionConfiguration
Encryption at restAES-256-GCM, values bound to their record, rotating keyringDAKERA_ENCRYPTION_KEY
API authenticationFour key scopes, optional namespace restrictionDAKERA_ROOT_API_KEY
Input validationRefused patterns, field and size limitsBuilt in, always on
Security headersCORS, CSP, HSTS, X-Frame-Options and othersDAKERA_HSTS_*, DAKERA_CSP_*, DAKERA_FRAME_OPTIONS, DAKERA_CORS_ORIGINS
Rate limitingServer-wide token bucket, plus optional per-namespace limitsDAKERA_RATE_LIMIT_RPS, DAKERA_RATE_LIMIT_BURST
Cluster authenticationShared secret on node-to-node routes, HMAC-authenticated gossip; required on every cluster node (v0.12.1)DAKERA_CLUSTER_SECRET
Audit loggingOne entry per REST request and gRPC callDAKERA_AUDIT_*

Input validation

Validation is always on. A value that matches one of these patterns is refused with 400, and the message names the rule it broke:

PatternWhere it is refused
$where, $expr, $functionNames and text (MongoDB-style operators)
javascript:Names and text
<scriptNames and text
A NUL byteNames and text
../ or ..\ (path traversal)Names only (ids and metadata keys). Text such as metadata values, tags and filter values may contain a relative path

Filters accept only the documented operators. Outbound URLs that a caller can choose (such as extractor_override.base_url) are checked against private and reserved address ranges; see Limits and hardening.

Field limits

FieldLimit
Request bodyDAKERA_MAX_BODY_SIZE, default 10 MiB (413 above it)
Memory contentDAKERA_MAX_MEMORY_CONTENT_BYTES, default 100,000 UTF-8 bytes, on every memory write path (configurable from v0.12.2; updates are checked too)
Tags per memory50, each at most 256 characters
Agent ID241 bytes (v0.12.2; was 256), so _dakera_agent_{id} fits one path segment
Session ID256 characters
Namespace name128 characters: letters, digits, underscore and hyphen, starting with a letter or digit
Vector ID512 characters
Metadata100 fields per record; keys up to 256 characters; values up to 32,768. Memory metadata (v0.12.2): a JSON object or null, keys 1 to 256 bytes, at most the content limit serialized, and no _dakera_* key other than _dakera_content_date and _dakera_lang
Key grantsAt most 100 entries of at most 255 bytes (v0.12.2)
Filter nesting10 levels, 50 conditions
top_k1 to 10,000
Vectors per upsert10,000
API key name128 characters

Security headers

Dakera sets these response headers by default (enabled unless noted):

Header Default value Setting
Strict-Transport-Security max-age=31536000; includeSubDomains DAKERA_HSTS_ENABLED (default true), DAKERA_HSTS_MAX_AGE (seconds)
Content-Security-Policy default-src 'none'; frame-ancestors 'none' DAKERA_CSP_ENABLED (default true), DAKERA_CSP_DIRECTIVES
X-Frame-Options DENY DAKERA_FRAME_OPTIONS: DENY, SAMEORIGIN, or empty to disable. Any other value is an invalid value (a fresh install refuses to start)
X-Content-Type-Options nosniff always on
X-XSS-Protection 1; mode=block always on
Referrer-Policy strict-origin-when-cross-origin always on
Permissions-Policy geolocation=(), microphone=(), camera=() always on

Dakera does not terminate TLS; set HSTS at the reverse proxy as well if it terminates TLS for you. There are no preset names to select: the headers above are what the environment variables configure.

export DAKERA_HSTS_MAX_AGE=63072000
  export DAKERA_FRAME_OPTIONS=SAMEORIGIN

CORS and cross-origin protection

Situation Behavior
Authentication on, DAKERA_CORS_ORIGINS unset Any origin may call the API from a browser; credentials are never allowed in this case
DAKERA_CORS_ORIGINS=https://app.example.com,https://admin.example.com Only the listed origins
Authentication off (DAKERA_AUTH_ENABLED=false), DAKERA_CORS_ORIGINS unset Loopback origins only (localhost, 127.0.0.1, [::1]). A POST, PUT, PATCH or DELETE from a web page of another origin (an Origin that is neither loopback nor listed, or Sec-Fetch-Site: cross-site) is refused with 403 CROSS_ORIGIN_REQUEST_REFUSED
DAKERA_CORS_CREDENTIALS=true with no origins or * Ignored with a startup warning: credentials are only sent when every allowed origin is named

Clients that send no Origin header (SDKs, curl, servers) and GET, HEAD and OPTIONS requests are never refused by the cross-origin check.

TLS and HTTPS

Dakera does not terminate TLS. Terminate it at a reverse proxy or load balancer (Nginx, Caddy, Traefik or a cloud load balancer) in front of the REST and gRPC ports.

See Deployment → Reverse proxy & TLS termination for complete Nginx and Caddy configurations. For gRPC, configure mTLS at the proxy level when client certificate authentication is required.

Encryption at rest

With DAKERA_ENCRYPTION_KEY set, memory content, text documents (_text) and the persisted full-text (BM25) indexes are sealed with AES-256-GCM before they reach storage. The write-ahead log, snapshots, RocksDB files, the disk cache, Redis, S3 objects, backups and cluster replication therefore only carry ciphertext. Nothing else needs configuring and nothing changes for clients: the memory API returns plaintext.

Setting Value
DAKERA_ENCRYPTION_KEY 64 hex characters (a raw 256-bit key, openssl rand -hex 32) or a passphrase of at least 8 characters (derived with PBKDF2-HMAC-SHA256). The same value on every node of a cluster.
Sealed form $enc$v2$<key_id>$... (AES-256-GCM). The key id is 16 hex characters derived one way from the key.
Binding Each value is authenticated together with its namespace, record id and field, so a sealed value copied into another record, namespace or field does not decrypt there.
Older values $enc$v1$ values written by v0.11 keep decrypting and are re-sealed to $enc$v2$ by a background pass at startup (values_migrated_v1 in the status).

Keyring

The key in DAKERA_ENCRYPTION_KEY is the first data key. Every rotation adds a data key to the keyring, which is stored in the reserved namespace _dakera_keyring, wrapped under a master key derived from DAKERA_ENCRYPTION_KEY (never in the clear), and replicated to every node. Old keys are kept, so values sealed under them keep opening while they are re-sealed and backups taken before a rotation still restore. A key that nothing references any more is marked retired but is still kept for old backups, WAL segments and snapshots. Key material never appears in responses, errors, logs or debug output; only key ids do.

Rotating keys

POST /admin/encryption/rotate-key (global admin scope; /v1/admin/encryption/rotate-key works too).

Field Meaning
namespace Rotate only this namespace: the new key seals it (its memories' content, text documents and BM25 index) and nothing else. Omitted: the global default is rotated and every namespace, including those rotated on their own, moves to the new key. The namespace must exist and hold data a key can seal.
new_key Optional passphrase (at least 8 characters) or 64 hex characters the new key is derived from. Omitted: the server generates a random 256-bit key. A shorter passphrase is a 400 INVALID_REQUEST.
wait_secs How long to wait for the re-seal before answering. Default 10, at most 300, 0 answers at once. The re-seal continues in the background either way.
# One namespace; every other namespace keeps its key
  curl -X POST localhost:3000/admin/encryption/rotate-key \
    -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
    -d '{"namespace": "_dakera_agent_alice"}'
  
  # Everything
  curl -X POST localhost:3000/admin/encryption/rotate-key \
    -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" -d '{}'

The response reports key_id, previous_key_id, scope (global or namespace), namespace, reseal (completed, running, or partial: work the background pass retries, such as records written meanwhile or a namespace that could not be read), rotated (values re-sealed by the time of the answer), skipped, namespaces, fulltext_indices_rotated, failed_namespaces, action_required and status_url. Reads keep working throughout. DAKERA_ENCRYPTION_KEY itself does not change after a rotation, and every node re-seals its own copy of the data after receiving the new key through replication.

Status, re-seal and master revocation

Route Scope What it does
GET /admin/encryption/status global admin Key ids, which key seals which namespace (namespace_keys), retirement state, this node's re-seal pass with live counters (reseal), every node's last complete pass (nodes). Never key material. enabled: false when encryption is off.
POST /admin/encryption/reseal global admin Runs a re-seal pass now. Body {"namespace"?, "wait_secs"?}, default wait_secs 0. 200 {"pass": ...} when the pass finished within the wait, 202 {"reseal": "running", "status_url": ...} otherwise, 400 when encryption is not configured. A pass also runs on its own after every rotation, keyring change and restore.
POST /admin/encryption/keys/{key_id}/revoke-master global admin Revokes the master of a retired key that was rotated in from a passphrase, so that passphrase no longer opens the keyring. The data key is kept, so backups taken under it still restore. 409 while the key still seals data or is not retired.

Metrics: dakera_encryption_values_resealed_total{kind}, dakera_encryption_values_unreadable, dakera_encryption_reseal_running, dakera_encryption_reseal_namespaces_remaining, dakera_encryption_deferred_writes_total and dakera_encryption_keys{state}. A shipped alert rule fires on unreadable encrypted values (see Operations).

Changing DAKERA_ENCRYPTION_KEY itself

  1. Rotate with new_key set to the new value. The keyring is then also wrapped under it.
  2. Set DAKERA_ENCRYPTION_KEY to the new value on every node and restart them.
  3. Once the key that came from the old passphrase is retired, optionally revoke its master with revoke-master.

Setting a new value without step 1 leaves the keyring, and everything sealed under its keys, unreadable. If encryption at rest is off, a global rotation with new_key turns it on for the running process and the response carries action_required: set DAKERA_ENCRYPTION_KEY to that value before the next restart.

What still holds old ciphertext after a rotation

Until rewritten or expired: WAL segments and snapshots written before the re-seal, RocksDB SST files until compaction, S3 object versions when bucket versioning is on, the disk and Redis caches until the entry is replaced, and backups. All of it stays decryptable with the keyring and none of it is plaintext.

Encrypted backups

POST /admin/backups accepts "encrypt": true (every file of the backup sealed with AES-256-GCM under a key derived from the operator key) and "compression": "zstd"; compression is applied first, then encryption, and both are recorded in the manifest. encrypt without DAKERA_ENCRYPTION_KEY, and "compression": "lz4", are refused with 400. Backups also carry _text sealed (v0.11 backups held it in plaintext). Details: Operations.

Going back to v0.11

dakera downgrade opens every value with the persisted keyring (generated keys, passphrase keys and namespace keys included) and re-seals memory content as $enc$v1$ under DAKERA_ENCRYPTION_KEY, with _text and the BM25 indexes in plaintext, which is what v0.11.108 reads. The keyring is kept, so backups taken under v0.12 still restore with v0.12. Values it cannot open (a key this DAKERA_ENCRYPTION_KEY cannot unwrap) are counted in values_unreadable and make the command exit 1. See Rolling back to v0.11.

Verify

curl -s localhost:3000/admin/encryption/status -H "Authorization: Bearer $ADMIN_KEY" | jq '{enabled, default_key_id, reseal}'
  curl -s localhost:3000/metrics | grep dakera_encryption_values_unreadable   # expect 0

API key authentication

Send the key as Authorization: Bearer <key> or X-API-Key: <key>. DAKERA_ROOT_API_KEY bootstraps a super_admin key; with authentication on (the default) and no key configured, the server exits at startup.

Scope hierarchy

ScopePermissionsUse case
readQuery vectors, recall memories, get namespace infoRead-only clients
writeUpsert/delete vectors, store/forget memories (includes read)Applications writing data
adminNamespace management, policies, /admin/* and /ops/* (includes write)Administrative tools
super_adminKey management (/admin/keys), backup download, upload and restore (includes admin)Operators only
Permissions in v0.12 — A key pinned to namespaces used to pass every check that asked for a scope only. Now it gets 403 on node-wide routes: /admin/*, /ops/*, /v1/analytics/*, /v1/audit*, /v1/kpis and /v1/events/stream need the scope with no namespace restriction. Backup download, upload and restore need global super_admin (a backup bundle carries every API key record). A namespace-pinned admin key may still delete, optimize, warm and rebuild its own namespaces. GET /v1/export and POST /v1/import need read / write on the agent's namespace, and POST /v1/namespaces/{ns}/keys with extra_namespaces only grants namespaces the calling key can reach. POST /ops/shutdown now really shuts the server down and needs a global admin key. Check namespace-pinned keys and backup tooling before upgrading — see Upgrading to v0.12.

Key grants (v0.12.2)

A key's namespaces is its grant list: null or ["*"] reach every namespace, [] reaches none, and each entry is an exact namespace name or a prefix pattern p* with one trailing star. Patterns are strict: team-* reaches team-a but not team or team-, and a pattern never reaches a server-internal namespace. ["_dakera_agent_mlx-*"] lets a developer key create and use its own agents (POST /v1/agents).

API key lifecycle

The complete workflow from bootstrap to rotation:

# 1. Bootstrap root key (set at server start)
export DAKERA_ROOT_API_KEY=$(openssl rand -hex 32)

# 2. Create a scoped application key
curl -X POST http://localhost:3000/admin/keys \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"query-service","scope":"read","expires_in_days":90}'
# {"key_id":"dk_key_1a2b3c4d","key":"dk_...","name":"query-service","scope":"read","namespaces":null,
#  "created_at":1790000000,"expires_at":1797776000,"warning":"..."}   (the key is shown only once)

# 3. Restrict key to a namespace (multi-tenant isolation)
curl -X POST http://localhost:3000/admin/keys \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"tenant-a","scope":"write","namespaces":["tenant-a"],"expires_in_days":90}'

# 4. List all keys
curl http://localhost:3000/admin/keys \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY"

# 5. Monitor key usage (by key_id)
curl http://localhost:3000/admin/keys/dk_key_1a2b3c4d/usage \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY"

# 6. Rotate a key, keeping the old one valid for an hour while clients move (v0.12.2)
curl -X POST http://localhost:3000/admin/keys/dk_key_1a2b3c4d/rotate \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"grace_secs":3600}'
# {"new_key":"dk_...","key_id":"dk_key_...","old_key_id":"dk_key_1a2b3c4d","old_key_expires_at":1791395803,...}

# 7. Or revoke a compromised key at once
curl -X DELETE http://localhost:3000/admin/keys/dk_key_1a2b3c4d \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY"
Recommended: use the root key only to create scoped application keys, and keep it in a secret store, never in version control. Set expires_in_days on application keys and rotate them on a schedule.

Namespace isolation

Namespaces are the isolation boundary in multi-tenant deployments. A key restricted to namespace tenant-a can read and write only that namespace, and namespace listings show it only the namespaces it can reach.

Isolation propertyGuarantee
Data isolationMemories, vectors, entities, and KG nodes are namespace-scoped
Key isolationNamespace-scoped keys cannot access other namespaces
Index isolationEach namespace has independent HNSW/BM25 indexes
Policy isolationMemory policies (TTL, decay, deduplication, rate limits) are set per namespace
# Create an isolated namespace for a tenant
curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"tenant-a","dimension":1024}'

# Create a key scoped to that namespace
curl -X POST http://localhost:3000/admin/keys \
  -H "Authorization: Bearer $DAKERA_ROOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"tenant-a-app","scope":"write","namespaces":["tenant-a"]}'

Permissions

Keys have one of four scopes (read, write, admin, super_admin, each including the one before) and may be restricted to namespaces. In v0.12.0 a key restricted to namespaces no longer passes checks that only asked for a scope. With authentication off every request is super_admin and none of this applies.

Route Needs
POST /admin/backups/restore, POST /admin/backups/upload, GET /admin/backups/{id}/download super_admin with no namespace restriction (a bundle carries every API key record)
Every other /admin/* and /v1/admin/* route (backups create, list, get, delete, schedule; restore status; encryption; cluster) admin with no namespace restriction. Exceptions a namespace-pinned admin key may still call for its own namespaces: DELETE /admin/namespaces/{ns}, POST /admin/namespaces/{ns}/optimize, POST /admin/cache/warm, POST /admin/indexes/rebuild
/admin/keys/* super_admin with no namespace restriction
/ops/diagnostics, /ops/jobs, /ops/jobs/{id}, /ops/compact, /ops/events, /v1/ops/metrics, /debug/config admin with no namespace restriction (/v1/ops/stats stays read and shows a pinned key its own namespaces)
POST /ops/shutdown super_admin with no namespace restriction (v0.12.3; a shutdown stops the node for every tenant)
/v1/analytics/*, /v1/audit, /v1/audit/stream, /v1/audit/export, /v1/kpis, /v1/events/stream, POST /v1/knowledge/network/cross-agent admin with no namespace restriction
GET /v1/export, POST /v1/import read (export) / write (import) on the agent's namespace _dakera_agent_{agent_id}
GET /v1/agents/{agent_id}/sessions access to the agent's namespace as well
Sessions: POST /v1/sessions/start, /end, /touch, GET /v1/sessions, GET /v1/sessions/{id} (v0.12.2) the scope on the session's agent namespace only (no _dakera_sessions grant). end and touch need Write scope, checked before the session is looked up
POST /v1/agents (v0.12.2) write on _dakera_agent_{agent_id}, so a pattern key works within its prefix
PATCH /v1/namespaces/{ns}/keys/{key_id} (v0.12.2) admin on {ns}; the new grants must be contained in the caller's
POST /v1/namespaces, PUT /v1/namespaces/{ns}, GET /v1/namespaces/{ns}/events the scope on that namespace
POST /v1/namespaces/{ns}/keys with extra_namespaces only namespaces the calling key can reach

Check namespace-pinned keys and backup tooling before upgrading (Upgrading to v0.12).


gRPC

With authentication on (the default), every RPC except Health needs an API key in the call metadata (x-api-key: <key> or authorization: Bearer <key>), authorized per scope and namespace like its REST twin (UNAUTHENTICATED, PERMISSION_DENIED). Calls draw on the server-wide rate limit (RESOURCE_EXHAUSTED), are bounded by DAKERA_REQUEST_TIMEOUT and, for Query, query_timeout_ms (DEADLINE_EXCEEDED), fail UNAVAILABLE while maintenance mode rejects requests, and are audited.


Cluster authentication

Cluster mode requires DAKERA_CLUSTER_SECRET (at least 16 characters, identical on every node) and a stable DAKERA_CLUSTER_NODE_ID. Generate the secret with openssl rand -hex 32 and store it like any other credential. It authenticates; it does not encrypt — keep node-to-node traffic on a private network or behind TLS.


Limits and hardening

Area v0.12.0 behavior
Sealed values from clients A vector write whose _text or content metadata starts with $enc$v1$ or $enc$v2$ is 400 (gRPC INVALID_ARGUMENT). Memory content that merely starts with those tags is stored as written. Memory content is opened only under the namespace it was read from
Importing a v0.11 export An encrypted v0.11 export carries $enc$v1$ content; import it with a key of global admin or super_admin scope. A $enc$v2$ value is refused from anyone
Raw vector APIs query, batch query, export, records and gRPC Query return a memory's content in its at-rest form (z64: or $enc$v2$); read memories through the memory API for plaintext
Knowledge graph Edges are keyed by namespace, so one agent cannot overwrite or delete another agent's edges. POST /v1/memories/{id}/links is 404 unless both ids are live memories of the agent. Path searches stop at 10 hops or 4096 memories (400)
Sessions POST /v1/sessions/start with another agent's session id is 409; GET /v1/agents/{id}/sessions needs access to the agent
Inputs Imported content is limited to 100,000 bytes per item; an uploaded backup bundle inflates to at most 16 times DAKERA_MAX_BODY_SIZE; extractor responses are capped at 4 MiB; namespace event streams are capped at 128 per key; non-finite vectors are refused
SSRF A caller-chosen extractor_override.base_url is not fetched through the system proxy. The ranges 198.18.0.0/15, the documentation networks and 64:ff9b:1::/48 are refused destinations
Secrets API keys passed as ?api_key= are redacted in the audit log; the HF token and the peer secret do not follow a cross-host redirect; the S3 secret is not logged; /health shows no filesystem paths or credential URLs in degraded reasons
Reserved namespaces Server-internal namespaces (the keyring _dakera_keyring, API keys, namespace configs, quotas, full-text snapshots, sessions, replication bookkeeping, tiered-storage bookkeeping and the store metadata that holds the embedding-model record) are not client-writable: a plain vector write into them is refused. Client namespace names may not begin with system_, internal_, admin_ or _ (other than _dakera_... names)
Request body size DAKERA_MAX_BODY_SIZE (default 10 MiB); an over-size body is 413 PAYLOAD_TOO_LARGE
Rate limit DAKERA_RATE_LIMIT_RPS (default 100) and DAKERA_RATE_LIMIT_BURST (default 50) per node; over the limit is 429 RATE_LIMIT_EXCEEDED with Retry-After and X-RateLimit-* headers
Namespace quotas Enforced on every write path: a write over a hard quota is 413 QUOTA_EXCEEDED
Connections (v0.12.3) A client has DAKERA_HTTP_HEADER_READ_TIMEOUT_SECS (default 30 s) to send a request head; an idle keep-alive or HTTP/2 connection is closed after the same time; at most DAKERA_MAX_CONNECTIONS (default 10000) connections are served at once; the graceful shutdown drains for at most DAKERA_SHUTDOWN_DRAIN_SECS (default 8 s)
DNS rebinding, authentication off (v0.12.3) A request whose Host is a dotted name not listed in DAKERA_ALLOWED_HOSTS is refused with 421 HOST_NOT_ALLOWED. localhost, IP addresses, single-label names, private names such as *.internal and *.local, and /health* are always served. No effect with authentication on
Admin request bodies (v0.12.3) POST /admin/keys, POST /v1/namespaces/{ns}/keys, POST /admin/backups and POST /admin/encryption/rotate-key answer 422 to a field they do not know, so a misspelled field can no longer weaken a key or a backup. Data-plane requests stay lenient and report ignored fields as warnings
Document import (v0.12.3) PDF and DOCX files are parsed in a sandboxed child process with an empty environment and time, memory, CPU and file limits; a malformed file or an archive bomb fails its import (422, 504 EXTRACTION_TIMEOUT) and never the server

Telemetry

Telemetry is on by default. It sends lifecycle events and a 6-hourly heartbeat, including the machine's hostname; PostHog also records the connecting IP address and derives a coarse location (GeoIP). It never sends memory content, queries, ids, keys or paths. Turn it off with DAKERA_TELEMETRY=0 or DO_NOT_TRACK=1. Full list: Telemetry.

Rate limiting

Dakera enforces two kinds of limit. The server-wide limit is a token bucket per node; with DAKERA_REDIS_URL set, the nodes share one counter in Redis (falling back to the per-node bucket if Redis does not answer). Per-namespace limits are optional and per minute: set rate_limit_enabled, rate_limit_stores_per_minute and rate_limit_recalls_per_minute in the namespace's memory policy (PUT /v1/namespaces/{ns}/memory_policy).

# Global rate limit
DAKERA_RATE_LIMIT_RPS=100       # requests per second (default; RATE_LIMIT_RPS still works, deprecated)
DAKERA_RATE_LIMIT_BURST=50      # burst capacity

gRPC calls take a token from the same limit.

When rate limited, clients receive a 429 Too Many Requests response with Retry-After header indicating when to retry.

# Example rate-limited response (also sends X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{"error":"Rate limit exceeded: 100 requests per second for this server. Resets at Unix time 1790000000.","code":"RATE_LIMIT_EXCEEDED","status":429,"details":"remaining: 0"}

Audit logging

Dakera writes one audit entry per request to the log (target audit), at a level that follows the status code. The entry records the key id, never the key.

Configuration

Variable Default Effect
DAKERA_AUDIT_ENABLED true Turns the audit log on or off
DAKERA_AUDIT_FORMAT json json or text (key=value lines). Anything else is an invalid value
DAKERA_AUDIT_REQUEST_BODY false Record request bodies
DAKERA_AUDIT_RESPONSE_BODY false Record response bodies
DAKERA_AUDIT_MAX_BODY_SIZE 4096 Bytes of a body kept; longer bodies are cut
DAKERA_AUDIT_EXCLUDE_PATHS /health,/health/ready,/health/live,/metrics Comma-separated paths not logged (setting it replaces the default list). OPTIONS requests are never logged

Body capture runs after authentication, so a rejected request is never read. Bodies are capped, secrets are redacted, and the bodies of key-management and cluster routes (/admin/keys, /admin/cluster, /v1/namespaces/{ns}/keys, and routes that carry keys) are always recorded as [REDACTED]. Secret query parameters such as ?api_key= are logged as [REDACTED]. Enabling capture grows the audit log.

Audit entry

{
    "request_id": "7f3c9a1e-00000042",
    "timestamp": "2026-10-01T10:30:45.123Z",
    "method": "POST",
    "path": "/v1/memory/store",
    "client_ip": "10.0.1.50",
    "user_agent": "curl/8.5.0",
    "api_key_id": "dk_key_1a2b3c4d",
    "status_code": 200,
    "duration_ms": 45,
    "request_size": 412,
    "response_size": 96
  }

request_id is the client's X-Request-ID when it sends one. Optional fields (query, client_ip, user_agent, api_key_id, request_size, response_size, request_body, response_body, error, context) appear only when they have a value. gRPC calls are audited as well.

Security monitoring

Monitor audit logs for:

Container security

PracticeImplementation
Non-root userThe official image runs as the dakera user, UID 1000
Read-only root filesystemA default start writes nothing outside its volumes: /data (the data root) and /app/models (the cache for models the image does not ship)
Minimal baseFrom v0.12.3 a distroless image with no curl and no package manager; the health check is dakera healthcheck, built into the server binary. The GPU image (CUDA 13) runs as the same non-root user
Signed imagesFrom v0.12.3 release images are signed keylessly with Sigstore cosign and carry SLSA provenance and an SPDX SBOM (see below)
Kubernetes security contextSet runAsNonRoot: true, readOnlyRootFilesystem: true and drop all capabilities
# Kubernetes security context
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]

Verifying release images (v0.12.3)

Every release tag (:<version>, :cpu, :latest, :gpu, :cuda) is signed by the release workflow, and the signature is recorded in the public Rekor transparency log. Verify an image with cosign v3 or later before you run it (cosign 2.x does not find these signatures), and read its provenance and SBOM:

cosign verify ghcr.io/dakera-ai/dakera:0.12.3 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/(?i:dakera-ai)/dakera/\.github/workflows/release\.yml@refs/tags/v'

docker buildx imagetools inspect ghcr.io/dakera-ai/dakera:0.12.3 --format '{{ json .Provenance }}'
docker buildx imagetools inspect ghcr.io/dakera-ai/dakera:0.12.3 --format '{{ json .SBOM }}'

Images published before v0.12.3 are unsigned.

Network security

PortServiceExposure
3000REST APIThrough a reverse proxy only; never expose it directly to the internet
50051gRPC APIInternal services only, or through a gRPC-aware proxy that terminates TLS
7946Gossip (SWIM)UDP, between cluster nodes only. Replication and elections use the REST port (/internal/*), so allow it between nodes too
# UFW firewall rules for a Dakera node
sudo ufw allow from any to any port 443 proto tcp       # HTTPS via reverse proxy
sudo ufw allow from 10.0.0.0/8 to any port 3000         # REST API from internal network
sudo ufw allow from 10.0.0.0/8 to any port 50051        # gRPC from internal network
sudo ufw allow from 10.0.0.0/8 to any port 7946 proto udp # Gossip between cluster nodes
sudo ufw deny 3000                                       # Block external REST access
sudo ufw deny 50051                                      # Block external gRPC access
sudo ufw deny 7946                                       # Block external gossip access

Production security checklist

ItemPriority
Root API key with 256-bit+ entropyRequired
Scoped application keys (least privilege)Required
TLS on all external endpoints (reverse proxy)Required
Server behind reverse proxy (never expose port 3000 publicly)Required
Firewall: only expose 443 externallyRequired
Encryption at rest (DAKERA_ENCRYPTION_KEY)Recommended
CORS restricted to application domainsRecommended
Rate limiting tuned for workloadRecommended
Audit logging enabledRecommended
Application keys with an expiry and a rotation scheduleRecommended
API keys on every gRPC client (v0.12)Required
DAKERA_CLUSTER_SECRET (at least 16 characters, identical) and a stable DAKERA_CLUSTER_NODE_ID on every node of a cluster (v0.12; a node without the secret does not start from v0.12.1)Required
Namespace-pinned keys and backup tooling checked against the v0.12 permission tableRequired when upgrading
Encryption key rotation schedule and GET /admin/encryption/status checkRecommended
Shipped Prometheus alert rules loaded (Operations)Recommended
Telemetry opt-out decided (DAKERA_TELEMETRY=0 / DO_NOT_TRACK=1)Review
Namespace-scoped keys for multi-tenant deploymentsRecommended
Container runs as non-root with read-only filesystemRecommended

Related: Deployment guide (reverse proxy, TLS, firewall setup) · Architecture (trust boundaries, storage layers) · Self-hosted deployment walkthrough

Included in the self-hosted server

Encryption at rest, namespace-scoped API keys, audit logging and Prometheus metrics are part of the free self-hosted binary.

Deploy for Free → Join Cloud Waitlist
Stay sharp on agent memory
Benchmark results, SDK releases, and production patterns. Under 500 words per issue.
✓ You're in. First issue lands soon — watch for Dakera in your inbox.