Security posture

Defense-in-depth, with the gaps stated out loud.

Cloudflare Access + bearer auth + 30-day token rotation + hash-chained audit + per-connector rate limiting. Every claim on this page links to a concrete audit artifact or commit. The certifications we do not yet hold are listed at the bottom; we would rather you find them on a webpage than in procurement.

Snapshot 2026-05-11 · sources: A:\GRIFF_AI\01_INTERNAL\_audit\ and the live griffai-memory + brain-runner repos.

1. Authentication — defense in depth

Every request to the production memory surface clears three gates before it reaches application logic: Cloudflare Access at the edge, the mTLS tunnel transport, and an in-process bearer middleware at the origin. A single compromised layer does not yield access.

AUTH-1

In place

Cloudflare Access (zero-trust app-level gate)

Production memory surface (memory.griff.run) sits behind Cloudflare Access. Every request is gated by Access policy before it reaches the origin. Access emits a signed CF-Access-Jwt-Assertion and a CF-Access-Authenticated-User-Email header that the origin can verify; the origin does not expose a public listener.

Source: A:\projects\github\griffin9899\griffai-memory\docs\threat-model.md (§3.1 Edge — Cloudflare Access)

AUTH-2

In place

Per-origin bearer token (defense-in-depth, in-process)

A shared-secret bearer (GRIFFAI_MEMORY_API_TOKEN) is validated on every non-/health request by an in-process middleware. Constant-time comparison via hmac.compare_digest so token-recovery via timing is not viable. This is the second gate behind Access — it catches a request even if an Access policy is misconfigured or a tunnel-routing bug exposes the origin.

Source: A:\projects\github\griffin9899\griffai-memory\src\memory_orchestrator\mcp_http_server.py (BearerAuthMiddleware) + commit 4a4955e (RV4 HIGH-1 constant-time fix)

AUTH-3

In place

30-day service-token rotation

All four connector service tokens were rotated on 2026-05-10 from a 1-year (8760h) lifetime to a 30-day (720h) lifetime, with fresh client_id + secret pairs minted. Old tokens DELETEd. Blast radius of a leaked token reduced from 365 days to 30 days. Session duration reduced 24h → 8h in the same lane.

Source: A:\GRIFF_AI\01_INTERNAL\_audit\2026-05-10-final-drain-N23-cf-hardening.md

AUTH-4

In place

mTLS-authenticated Cloudflare Tunnel (no public ingress)

cloudflared initiates an outbound, mTLS-authenticated connection to the Cloudflare edge. The origin host has no inbound public listener — there is no port-forward, no direct DNS entry pointing at it. An attacker who somehow forges traffic at Cloudflare's edge still has to clear Access policy and the origin bearer.

Source: A:\projects\github\griffin9899\griffai-memory\docs\threat-model.md (§3.2 Edge → Origin — Cloudflare Tunnel)

2. Encryption

TLS in transit is in place. Volume-level encryption at rest is NOT enabled on the operator workstation today — verified 2026-05-11 and stated here rather than fudged.

CRYPT-1

In place

TLS in transit (Cloudflare-terminated)

All public traffic to memory.griff.run terminates TLS at Cloudflare's edge. Edge → origin traffic rides the Cloudflare Tunnel's mTLS-authenticated connection. There is no plaintext public path to the origin.

Source: A:\projects\github\griffin9899\griffai-memory\docs\threat-model.md (§3.2)

CRYPT-2

Not yet

Volume-level encryption (BitLocker) at rest

BitLocker is NOT currently enabled on the operator workstation. Verified 2026-05-11: every volume (A:, B:, C:, D:, E:) returns Protection Off / 0% Encrypted / no key protectors. Secrets-at-rest today depend on NTFS ACLs plus the host filesystem being on non-removable internal storage. This is a known gap and we will not claim otherwise.

Source: Live manage-bde -status verification 2026-05-11. Cross-reference: V-8 NF-4 — A:\ drive type unverified, secrets on potentially-removable drive. See /trust page.

CRYPT-3

In place

Secrets isolation (no token persistence in audit log)

The per-request audit emitter records CF-Access-Client-Id (a non-secret identifier) only. The paired CF-Access-Client-Secret and the origin bearer token are never written to logs. Enforced by a negative test that posts the secret header sentinel and asserts it does not appear in the log file.

Source: A:\GRIFF_AI\01_INTERNAL\_audit\2026-05-10-final-drain-IMPL7-cf-rate-limit.md (§2 acceptance: No PII)

3. Audit chain — tamper-evident, fail-closed

The audit log is the receipt for everything else on this page. It is single-writer, hash-chained, and as of 2026-05-11 it fails closed on tail corruption rather than silently restarting from genesis. There is no “all good” render path that survives a tampered chain.

AUDIT-1

In place

Hash-chained event log (audit-chain.jsonl)

Every brain-runner event appends one JSONL row to A:\GRIFF_AI\00_MASTER_CONTROL\logs\audit-chain.jsonl. Each row carries seq, prev_hash, and entry_hash (SHA-256 of the canonical encoding including prev_hash). Tampering with any row breaks the chain on the next verify. A 1000-record concurrent integrity test (10 threads × 100 emits) is in the regression suite.

Source: A:\projects\github\griffin9899\brain-runner\src\brain_runner\audit_emitter.py + tests/test_audit_emitter.py::test_chain_integrity_concurrent

AUDIT-2

In place

Fail-closed chain reader (AR-001 critical fix)

On 2026-05-11, the Gemini AR-001 review surfaced a CRITICAL: if the chain's tail line was malformed JSON, the emitter silently returned None and the next emit would chain from "GENESIS", splitting the chain into two unverifiable halves while /proof and /honest-status kept rendering green. Patch: _read_tail_record now raises AuditChainCorruptError; emit() catches it, writes a structured record to <chain>.failures.jsonl, and re-raises. The audit chain now fails closed — a tamper attempt is observable in band.

Source: A:\GRIFF_AI\01_INTERNAL\_audit\2026-05-11-next2-ar001-audit-chain-fix.md

AUDIT-3

In place

Per-request HTTP audit log (CF-Access-Client-Id attribution)

The recall HTTP surface emits one JSONL audit line per request to A:\Logs\griffai-memory\audit-YYYY-MM-DD.jsonl. Shape: {ts, endpoint, cf_client_id, auth_status, status_code, latency_ms}. Daily rotation at UTC midnight. Audit middleware is outer to the rate limiter so 429s are still recorded with auth_status="rate_limited". Closes V-8 must-fix-now #2 (per-connector audit attribution).

Source: A:\GRIFF_AI\01_INTERNAL\_audit\2026-05-10-final-drain-IMPL7-cf-rate-limit.md + src/memory_orchestrator/audit_log.py

Sample audit-chain row (illustrative)

{
  "seq": 5142,
  "ts": "2026-05-11T14:22:01.482Z",
  "agent": "brain-runner",
  "task_type": "brain.run.called",
  "summary": "extract_triples on 14-token utterance",
  "prev_hash": "8a4f7d2c91b6e3a5fd4c7e8b1a2d9f3e6c5b4a7d8e9f0a1b2c3d4e5f6a7b8c9d",
  "entry_hash": "b91c6e8d3f5a2c7b4e9d8a1f0c6b5d4a7e2f3c8b9a1d0e6f5c4b3a2d8e7f1c9b"
}

Real rows live at A:\GRIFF_AI\00_MASTER_CONTROL\logs\audit-chain.jsonl. Verification path: recompute SHA-256(canonical(row including prev_hash)) and compare to entry_hash; mismatch on any row breaks the chain forward of that point.

4. Rate limiting — per-connector, dual-window

A leaked service token is a real threat class. Rate limiting keyed on CF-Access-Client-Id (not IP) caps bulk-extraction damage to one connector and gives the audit log a per-tenant abuse signal.

RL-1

In place

60 req/min + 600 req/hour per CF-Access-Client-Id

slowapi-based limiter, keyed on CF-Access-Client-Id (NOT remote IP — Cloudflare's edge is the IP every CF tenant would share). Dual budget catches both burst and slow-drip abuse (59 req/min sustained for 10 minutes = 590 reqs that fit under any 60/min cap; the 600/hour cap stops it). Returns 429 with a standard Retry-After header. Per-key budgets override-able via GRIFFAI_MEMORY_RATE_LIMIT_MIN / _HOUR. Closes V-2 Grok HIGH-1 (DLP-failure exposure on a leaked token).

Source: A:\GRIFF_AI\01_INTERNAL\_audit\2026-05-10-final-drain-IMPL7-cf-rate-limit.md + tests/test_rate_limit.py (5 tests)

RL-2

In place

Independent buckets per connector

Test test_rate_limit_keyed_per_cf_client_id asserts tenant-A exhausting its 60/min bucket does NOT throttle tenant-B's next request — buckets are independent on the CF-Access-Client-Id key. A noisy or compromised connector cannot DoS another connector at the origin.

Source: A:\projects\github\griffin9899\griffai-memory\tests\test_rate_limit.py

5. What we don’t claim

We are an early-stage, single-operator product. The following are explicitly NOT in place today. Listing them here is the cheapest way to keep a sales call honest:

  • · No SOC 2 Type I or Type II report.
  • · No ISO 27001 certification.
  • · No formal third-party penetration test.
  • · No FedRAMP authorization (Low, Moderate, or High).
  • · No BitLocker / volume-level encryption at rest on the operator workstation today (verified 2026-05-11).
  • · No continuous monitoring / SIEM / Cloudflare Logpush at the current tier.
  • · No 24/7 on-call rotation. Best-effort triage by a single operator.
  • · No DPA / DPIA / GDPR Article 28 paperwork stack.
  • · No dual control on production access. Single operator with operational discipline; not a control separation.
  • · No published external pentest report. The eight reviews in /trust are LLM-driven audits, not a substitute.

The roadmap to those certifications is procurement-conditional. When a qualified pilot or federal engagement closes, we will fund the controls, run the audits, and publish the reports here.

6. Vulnerability disclosure

If you believe you have found a security issue in any GRIFF AI surface, email security@griff.run. Include a description, reproduction steps, the affected URL or artifact, and any timeline constraints you need us to respect.

What we promise: we will acknowledge receipt, work the issue in good faith, and credit you in any public remediation note unless you ask us not to. We do not currently run a paid bug bounty.

What we will not do: pursue legal action against good-faith research that respects the boundaries below.

  • Do not access, modify, or exfiltrate data that is not yours.
  • Do not run automated scanners that would consume production capacity meaningfully (the rate limit in section 4 will catch you anyway).
  • Do not publicly disclose until we have had a reasonable chance to remediate — 90 days is the baseline; we will discuss shorter windows for already-public issues.

Honest SLA: this is a single-operator product. We do not yet publish a triage-time SLA. Expect an acknowledgement within a few business days for the foreseeable future. If that is not acceptable for your use case, route the issue through the contact form and flag it as security.

7. Owner-gated production controls

NDA-gated evidence

Production identity, messaging, billing, media custody, and partner integration evidence are documented under NDA via the private data room. Qualified security and procurement reviewers can request access through /contact.

Why public pages stop here

Public pages intentionally avoid publishing live secret material, provider topology details, or unredacted readiness logs. The audit bundles cited above contain the evidence; the public surface contains the receipts pointing at it.

For the full external review history (eight vendors, every finding, every remediation status), see /trust. For procurement-grade NIST control mapping and Section 889 attestation, see /federal.