ai-memory Security Overview
Threat model, trust boundaries, and hardening options for operators.
For responsible disclosure: security@alphaone.dev. Please encrypt
against the maintainer key listed in SECURITY.md.sig (if present)
or via the fingerprint on our releases page.
Threat model
ai-memory is designed to be safe under the following attacker capabilities:
- Local untrusted user on the same machine as the CLI or HTTP daemon. They should NOT be able to read memories outside their own database, alter governance state, or escalate to the daemon’s effective UID.
- Network attacker reaching the HTTP daemon. They should NOT be
able to bypass API-key / mTLS, inject memories with a forged
agent_id, or enumerate memories without authorization. - Compromised peer holding valid mTLS cert. Under the default
posture they can author only as themselves: the peer-attestation
layer re-stamps inbound rows with the authenticated peer identity, and
a
body.sender_agent_idclaim not on that peer’s operator-configured allowlist is refused withsender_agent_id_mismatch(seedocs/federation.mdLayer 3). v0.8.0 (#1464) extends this from the body sender to per-memory granularity: each synced row’s claimedmetadata.agent_idis checked against the peer’s authorship allowlist, and an unauthorized relayed claim is rewritten to the sender and stampedattest_level = "claimed"so a forged claim can never own the row or charge another agent’s quota (resolve_inbound_attribution). The legacy trust-the-body posture exists only behind the explicitAI_MEMORY_FED_TRUST_BODY_AGENT_ID=1escape hatch. Operators should still treat theagent_idon synced memories as a claimed (allowlist-authorized) identity, not a cryptographically attested one, and keep the mTLS peer allowlist tight. (Store-path agent attestation — #626 Layer-3 — upgrades directly authored CLI/MCP/HTTP writes toagent_attestedwhen a valid Ed25519 signature is presented; the store-path default will flip to require-attestation in v0.9 (#1751), preceded by a v0.8.0 deprecation WARN. The federation receive path remains claimed-by-default — per-write cryptographic attestation of synced memories needs a wire-protocol extension (sender signs each row, receiver verifies) and is tracked under Pillar-3 #1719; mTLS + the per-memory authorship allowlist stay the trust boundary for synced writes.) - Compromised LLM (Ollama returning malicious content). Autonomy
hooks never
execor write to disk outside the database. Worst case: bad tags, spurious contradiction flags. Reversible via the rollback log.
Out of scope (non-goals):
- Byzantine peer tolerance. Peers are assumed to be honest at the sync protocol level; mTLS on the peer allowlist is the trust boundary.
- Side-channel attacks (timing, cache, etc.) on the SQLCipher passphrase. We expose the passphrase only via a root-readable file.
- Denial of service at the database layer. SQLite uses a process-global mutex; malicious writers can queue. Rate-limit upstream of the daemon.
Trust boundaries
┌──────────────┐ no auth ┌──────────────┐
│ MCP client │───────────────────▶│ ai-memory │
│ (Claude Code)│ stdio JSON-RPC │ daemon / │
├──────────────┤ │ MCP server / │
│ HTTP client │────────────────────▶│ CLI │
│ (SDK, curl) │ API key + mTLS │ │
├──────────────┤ mTLS + sync └──────┬───────┘
│ peer daemon │◀────────────────────┐ │
└──────────────┘ │ │ SQLite mutex
│ ▼
┌──────┴──────┐
│ ai-memory.db│
│ (optionally │
│ SQLCipher) │
└─────────────┘
- MCP (stdio): trusts the parent process. Run as the same user as the MCP client. No authentication needed.
- HTTP daemon: trusts no one by default. API key + mTLS gate inbound.
- Peer sync: trusts peers on the mTLS allowlist.
- Governance: trusts registered agents for approvals. Adding an approver is the only way in.
Authentication
API key (HTTP)
Set at daemon startup:
ai-memory serve --api-key "$(pwgen -s 48 1)"
Or via config file:
api_key = "long-random-string"
Every HTTP endpoint except /api/v1/health enforces the key (when
the mTLS allowlist is enforced, the /api/v1/sync/* federation
endpoints additionally bypass the key check — they have already
cleared a stronger transport gate; see #702 and
docs/federation.md). Accepts either:
- Header:
X-API-Key: <key>— the supported channel. - Query parameter:
?api_key=<key>— DEPRECATED at v0.7.0 (#1574; URL-embedded credentials leak into access logs, Referer headers, and proxy logs — a once-per-process WARN is emitted on use). Slated for removal; migrate callers to the header.
Rotation: generate new key, update config, restart the daemon. Clients have a grace period determined by their connection lifetime — there’s no in-flight rotation today.
mTLS (Layer 1 + Layer 2)
Layer 1 enables HTTPS:
ai-memory serve \
--tls-cert /etc/ai-memory/cert.pem \
--tls-key /etc/ai-memory/key.pem
rustls under the hood, no OpenSSL dep. PKCS#8 and RSA keys both
supported. Certificate expiry is the operator’s responsibility; the
daemon does not notify on impending expiry.
Layer 2 adds a client-cert fingerprint allowlist:
ai-memory serve \
--tls-cert /etc/ai-memory/cert.pem \
--tls-key /etc/ai-memory/key.pem \
--mtls-allowlist /etc/ai-memory/peer-fingerprints.txt
Allowlist format: one SHA-256 hex fingerprint per line, optional
: separators, # comments. Any peer not on the allowlist cannot
complete the TLS handshake.
# peer-a.example.com
2F:79:84:AB:…:CD
# peer-b.example.com
7E:1B:FE:22:…:AA
Data at rest
SQLCipher encryption
Opt-in cargo feature. Replaces the bundled SQLite with SQLCipher (AES-256 page encryption).
cargo build --release --no-default-features --features sqlcipher
Supply the passphrase via a root-readable file:
echo -n 'strong-passphrase' > /etc/ai-memory/db.key
chmod 0400 /etc/ai-memory/db.key
ai-memory --db-passphrase-file /etc/ai-memory/db.key <cmd>
The CLI reads the file, exports AI_MEMORY_DB_PASSPHRASE for the
process lifetime, and clears it on exit. Passphrase never appears in
ps//proc/<pid>/environ.
Defaults (page size, cipher, KDF iterations) match SQLCipher 4.x. To
open the DB manually: sqlcipher ai-memory.db + PRAGMA key='…';.
File permissions
The daemon expects the DB file + WAL/SHM companions to be writable
only by the ai-memory user:
chown ai-memory:ai-memory /var/lib/ai-memory/*.db*
chmod 0600 /var/lib/ai-memory/*.db*
The bundled systemd unit enforces ReadWritePaths=/var/lib/ai-memory
and drops all capabilities.
Backups
ai-memory backup writes SQLCipher-encrypted snapshots too when the
daemon is built with --features sqlcipher. The sha256 manifest
commits to the ciphertext, not the plaintext — verification works
without the passphrase.
Input validation
Every write path validates:
agent_id— regex^[A-Za-z0-9_\-:@./]{1,128}$. Rejects shell metacharacters, whitespace, control chars.namespace— rejects..segments (path traversal), caps length.title/content— length caps, HTML-safe (not stripped).tags— each tag validated against the same regex as above.tier/scope— whitelisted values only.metadata— JSON object, size capped.
Body-size limit: 2 MiB per request
(DefaultBodyLimit::max(HTTP_BODY_LIMIT_BYTES),
HTTP_BODY_LIMIT_BYTES = 2 * MIB in src/lib.rs).
Network hardening
Bind address
ai-memory serve --host 127.0.0.1 # loopback-only (default)
ai-memory serve --host 0.0.0.0 # public (requires TLS + auth)
ai-memory serve --host 10.0.0.5 # specific interface
Never bind 0.0.0.0 without TLS + API key + mTLS.
Two related hardening knobs:
AI_MEMORY_REQUIRE_API_KEY=1(#1458) — hard-refuse daemon start without anapi_keyon ANY bind host, including loopback. Use it on reverse-proxy /--network=host/socatdeployments where the loopback host string does not reflect off-host reachability.AI_MEMORY_ADMIN_HEADER_TRUST(#1570) — default off: when admin ids are configured and the daemon has no request authentication (noapi_key), a bare self-assertedX-Agent-Idnaming an admin id is REFUSED admin-role resolution (boot emits a WARN naming the flag). Set truthy only on isolated / mTLS-fronted deployments to restore the legacy trust-the-header posture. The same gate covers BOTH therequire_adminendpoints AND the read handlers that OR an admin flag past the per-rowscope=privatevisibility filter (/contradictions,/kg/query,/links/{id},/archive,/pending,/taxonomy) via the authn-gatedis_admin_caller_trustedpredicate (#1582), so a self-asserted admin header on a keyless deployment cannot read other tenants’ private rows.
Webhooks (SSRF-hardened)
The webhook dispatch path validates URLs before POSTing:
https://required unless the host is a loopback address.- Private-range IPv4 (10/8, 172.16/12, 192.168/16), IPv6 unique-local, and link-local are rejected.
- DNS is resolved once per send; we do NOT follow redirects.
- HMAC-SHA256 signs every payload when a secret is supplied.
Subscribers can still receive malicious webhook-URL registrations — review subscription inserts through governance if that’s a concern.
Sync peer push
POST /api/v1/sync/push is gated by the mTLS fingerprint allowlist
at the transport layer, and at v0.7.0 by per-message Ed25519
signatures by default: AI_MEMORY_FED_REQUIRE_SIG=1 (#791 secure
default) rejects missing/invalid X-Memory-Sig headers with 401,
and AI_MEMORY_FED_REQUIRE_NONCE=1 (#922 secure default) refuses
byte-for-byte replays via per-peer nonce freshness
(X-Memory-Nonce; nonces persist across restarts in the
federation_nonce_cache table, schema v51 / #1255). Peer
attestation (AI_MEMORY_FED_PEER_ATTESTATION) further scopes what
an authenticated peer may claim and pull. Never run the sync
endpoint on a public network without mTLS.
Governance
Policies are set per-namespace via memory_namespace_set_standard
(MCP) or directly in SQL. The per-action levels
(crate::models::GovernanceLevel) are:
any— ungated (the allow-on-silence default forwrite/promote; seedocs/governance.md).registered— any registered agent.owner— only the row’s authoring agent (the default fordelete).approve— queue a pending action for the namespace’s configured approver (human,{"agent": "<id>"}, or{"consensus": N}—crate::models::ApproverType).
An action that hits an approve policy returns 202 Accepted with a
pending_id; approvers POST to /api/v1/pending/{id}/approve.
Critical: consensus: N requires pre-registered agents
(issue #216 / #234). Unregistered approvers cannot satisfy the
quorum. See ai-memory agents register.
Audit trail
Every memory carries metadata.agent_id (immutable once written).
Governance actions log decided_by. The curator’s rollback log
preserves every autonomous action as a reversible snapshot memory
in _curator/rollback/<ts>.
For compliance-grade audit, also:
- Enable daemon structured logs (
RUST_LOG=ai_memory=info) and ship to syslog. - Enable Prometheus
/metricsand scrape the full counter set. - Retain
archivememories (don’tarchive purge).
Responsible disclosure
If you find a security vulnerability, please:
- Do not open a public issue.
- Email security@alphaone.dev with details. Encrypt to the key listed on our releases page if the impact is severe.
- We aim to acknowledge within 72 hours and ship a fix within 14 days for critical issues. We’ll credit reporters who wish to be credited.
Our bug bounty program is documented at https://alphaonedev.github.io/ai-memory-mcp/security.