# 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:

1. **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.
2. **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.
3. **Compromised peer** holding valid mTLS cert. Under the v0.7.0
   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_id` claim
   not on that peer's operator-configured allowlist is refused with
   `sender_agent_id_mismatch` (see
   [`docs/federation.md`](federation.md) Layer 3). The legacy
   trust-the-body posture exists only behind the explicit
   `AI_MEMORY_FED_TRUST_BODY_AGENT_ID=1` escape hatch. Operators
   should still treat the `agent_id` on synced memories as a claimed
   identity, not an attested one, and keep the mTLS peer allowlist
   tight. (Store-path agent attestation —
   #626 Layer-3 — upgrades *directly authored* CLI/MCP/HTTP writes to
   `agent_attested` when a valid Ed25519 signature is presented, but the
   federation **receive** path here remains claimed-by-default; mTLS +
   the peer allowlist stay the trust boundary for synced writes.)
4. **Compromised LLM** (Ollama returning malicious content). Autonomy
   hooks never `exec` or 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:

```bash
ai-memory serve --api-key "$(pwgen -s 48 1)"
```

Or via config file:

```toml
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`](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:

```bash
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:

```bash
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).

```bash
cargo build --release --no-default-features --features sqlcipher
```

Supply the passphrase via a root-readable file:

```bash
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:

```bash
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

```bash
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 an `api_key` on ANY bind host, including loopback. Use it
  on reverse-proxy / `--network=host` / `socat` deployments 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 (no `api_key`), a bare self-asserted `X-Agent-Id`
  naming 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 the `require_admin` endpoints AND the read handlers
  that OR an admin flag past the per-row `scope=private` visibility
  filter (`/contradictions`, `/kg/query`, `/links/{id}`, `/archive`,
  `/pending`, `/taxonomy`) via the authn-gated `is_admin_caller_trusted`
  predicate (#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 for `write` /
  `promote`; see [`docs/governance.md`](governance.md)).
- `registered` — any registered agent.
- `owner` — only the row's authoring agent (the default for
  `delete`).
- `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 `/metrics` and scrape the full counter set.
- Retain `archive` memories (don't `archive purge`).

## Responsible disclosure

If you find a security vulnerability, please:

1. **Do not** open a public issue.
2. Email **security@alphaone.dev** with details. Encrypt to the key
   listed on our releases page if the impact is severe.
3. 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>.
