Security Posture
This document describes what data flows where, what executes in customer infrastructure vs BlackLake cloud, how governance decisions are signed and verified, and what the honest limits of BlackLake's protection model are.
Capture paths and data boundaries#
For each capture path, "what BlackLake sees" means: what data the BlackLake API receives and persists.
MCP gateway#
What flows where: An MCP-compatible client (Cursor, Claude Code, Claude Desktop,
etc.) routes tool calls through the BlackLake MCP proxy. The proxy receives the
tool name and arguments, calls govern(), and — if allowed — forwards the
call to the registered upstream MCP server.
Customer infrastructure vs BlackLake cloud:
- In cloud MCP mode: the client connects to
api.blacklake.systems/mcp. Tool call arguments pass through BlackLake's cloud infrastructure. BlackLake sees the tool name and the full argument payload for every governed call. - In local MCP mode (
blacklake serve): the client connects tolocalhost:3100/mcp. BlackLake's local process sees the tool call arguments, but they never leave the developer's machine.
What BlackLake stores: Tool name, agent identifier, action payload
(from govern() call), decision, policy snapshot, cost summary, receipt.
Action payloads are stored in policy_evaluations.action_payload (JSONB).
Signing: Every decision produces a signed token. See Signing section below.
SDK (govern() direct calls)#
What flows where: Application code calls govern() from
packages/sdk/src/index.ts. The action parameter — a Record<string, unknown> —
is sent to POST /v1/govern.
Customer infrastructure vs BlackLake cloud: The application code executes in
the customer's environment. BlackLake receives the action payload the customer
chooses to send. There is no SDK-side interception of LLM responses or tool
outputs; the customer controls what goes in action.
What BlackLake stores: Same as MCP: agent, tool, action payload, decision, policy snapshot, cost summary.
CI (GitHub Actions, generic CI)#
What flows where: packages/govern-action/ calls govern() before a deploy
step and posts an action_result after. The action payload contains metadata
the customer provides (commit SHA, environment, etc.).
Customer infrastructure vs BlackLake cloud: CI pipeline runs on the customer's CI infrastructure (GitHub-hosted runners, self-hosted runners). The govern call is an outbound HTTPS request to BlackLake's API. BlackLake does not execute CI commands.
What BlackLake stores: Agent, tool, action payload, decision, action result (status, exit code, duration, optional output digest).
Shell (blx)#
What flows where: blx <command> classifies the command
(packages/cli/src/blx.ts), calls govern(), waits for approval if needed,
then executes the underlying child process. The child process output is not sent
to BlackLake by default.
Customer infrastructure vs BlackLake cloud: blx runs on the developer's
or operator's machine. The underlying command (git push, terraform apply,
etc.) executes locally. BlackLake receives only the govern request and, after
execution, the action result metadata.
What BlackLake stores: Classified command (e.g., shell.deploy), action
payload (command arguments the customer includes), decision, action result.
Cloud audit ingest#
What flows where: External systems (GCP Cloud Audit Logs, AWS CloudTrail,
GitHub webhooks) push events to POST /v1/audit/ingest. BlackLake stores these
events in the external_events table and matches them against existing receipts
for reconciliation.
Customer infrastructure vs BlackLake cloud: The forwarder (Pub/Sub → Cloud Run function, EventBridge → Lambda) runs on the customer's cloud. BlackLake receives the event payloads the customer's forwarder sends.
What BlackLake stores: Source, event type, resource identifier, principal, payload (as JSONB), received timestamp.
Honest caveat: BlackLake does not intercept cloud provider API calls at the network level. "Cloud audit" coverage depends on the customer setting up a forwarder that sends relevant events. Unforwarded activity is not visible to BlackLake.
Existing workflow engine (Temporal, Inngest, etc.)#
What flows where: The customer's workflow code (running on their
infrastructure) calls withGovernance() or govern() at consequential steps.
Same data boundary as SDK mode.
Customer infrastructure vs BlackLake cloud: Workflow execution stays on customer infrastructure. BlackLake receives govern requests and action results.
Depth (local execution)#
What flows where: Depth runs entirely on the customer's infrastructure.
ctx.llm() calls go directly to the LLM provider using the customer's API keys.
ctx.tool() calls go through the customer's local MCP upstreams (or registered
local executors). If BLACKLAKE_SURFACE_URL is set to a cloud Surface instance,
governance decisions flow through BlackLake cloud; otherwise nothing leaves the
customer's machine.
Customer infrastructure vs BlackLake cloud (cloud Surface mode):
- Govern requests (agent, tool, action arguments): sent to cloud Surface.
- LLM responses and step outputs: stay in local SQLite (
depth.db). Not sent to BlackLake. - BYO LLM API keys: used directly from the customer's environment variables. BlackLake never receives them.
Plain-MCP gateway mode (cloud Surface, MCP capture):
If a Depth workflow calls ctx.tool() and that tool routes through the cloud
Surface MCP proxy, the tool call arguments pass through BlackLake cloud. This
is the same boundary as the MCP gateway section above.
Signing: decision tokens#
Every govern() call produces a signed token on the evaluation record.
Implementation: apps/api/src/lib/decision-token.ts.
v1 token (bldt_v1:<base64url-hmac>): HMAC-SHA256 binding
evaluation_id | decision. Derived from BLACKLAKE_WEBHOOK_KEK via HKDF-style
key derivation (createHmac('sha256', kek).update('blacklake-decision-token-v1').digest()).
The decision token is returned in the govern() response and can be verified via
POST /v1/decisions/verify.
v2 token (bldt_v2:<base64url-hmac>): HMAC-SHA256 binding
evaluation_id | decision | cost_summary. The cost summary is canonicalised
to 8 decimal places before signing so field additions do not break old verifiers.
v2 receipts include both v1 and v2 tokens for backward compatibility.
What signing proves: That the (evaluation_id, decision) pair was produced
by the BlackLake API that holds the signing key, and has not been tampered with
since. An LLM agent cannot fabricate a valid token.
What signing does not prove: That the governed action was the only action
taken. An agent can call a tool without going through govern(). BlackLake
governs routed paths; it does not magically intercept all tool calls from all
AI agents. Cloud audit reconciliation catches ungoverned production mutations
after the fact, but that relies on the customer forwarding relevant audit events.
Key rotation: BLACKLAKE_WEBHOOK_KEK rotation invalidates existing tokens.
Old evaluations retain their tokens; re-verification after a rotation will fail
for tokens created with the old key. Customers who need long-lived verifiable
receipts should export receipts before rotating keys (BL-DIF-5 offline
verification is deferred).
API keys and authentication#
- API keys are stored as bcrypt hashes (
apps/api/src/lib/password.ts-style hashing, seeapps/api/src/db/schema.pg.tsapi_keys.key_hash). The raw key is returned once at creation and never stored. - User-scoped keys (
api_keys.user_idnon-null) attribute actions to a specific user. Org-scoped service keys (user_idnull) attribute to the workspace. - Local mode auto-authenticates when no
x-api-keyheader is present and the request comes from localhost. Seeapps/api/src/lib/mode.ts. - Magic-link tokens for approval emails are HMAC-signed with their own derived
key (
apps/api/src/lib/approval-tokens.ts), time-limited (15 minutes), and single-use.
Audit retention#
Default retention window: 90 days (configurable via AUDIT_RETENTION_DAYS
env var). After 90 days, policy_evaluations, cost_records, and
external_events rows are streamed out of Postgres to NDJSON+gzip files in
GCS (gs://blacklake-audit-archive/<org_id>/<run_timestamp>/<table>.ndjson.gz)
and then deleted from Postgres.
Implementation: apps/api/src/lib/audit-archive.ts (BL-OPS-4).
The archive job runs nightly. Rows are only deleted after GCS upload confirms 200 OK, so a failed upload leaves rows in Postgres for the next nightly retry.
Read path for archived data: Not yet implemented (BL-OPS-4b deferred).
bl.audit.export({ includeArchived: true }) does not yet fall through to GCS.
Customers who need to query data older than the retention window must download
the NDJSON.gz objects from GCS directly.
Enterprise extended retention: Not currently offered as a product
configuration. Custom AUDIT_RETENTION_DAYS is the only control.
Provider credential handling#
| Deployment mode | Who holds LLM API keys | Does BlackLake see the key? |
|---|---|---|
| Depth-only (local) | Customer's environment | No |
| Depth + cloud Surface | Customer's environment | No |
| MCP proxy (cloud Surface) | Customer's MCP upstream credentials | Only upstream auth headers, not provider model keys |
| SDK/govern() direct | Customer's backend | No — govern() takes action metadata, not API keys |
| Hosted Depth (future) | Customer secrets stored encrypted in BlackLake | Yes, at injection time — see BL-SD-8 and BL-SD-21 |
For all current deployments: BlackLake does not see model API keys for SDK, Depth, or shell capture paths. The MCP proxy sees upstream auth credentials (e.g., a Linear API key) to the extent the customer configures them in the upstream registration, but does not proxy model-provider requests.
Honest limits#
- BlackLake governs the paths you route through it. It does not intercept
tool calls that bypass
govern(). - Cloud audit reconciliation only detects ungoverned actions for events the
customer forwards to
POST /v1/audit/ingest. It is not a passive network monitor. - HMAC signing is symmetric — receipt verification requires trusting that the verifying party uses the same KEK. Offline/public-key verification (BL-DIF-5) is deferred.
- Local mode has no authentication by design. Do not expose
localhost:3100to untrusted networks.
Security contact#
Vulnerability disclosures: security@blacklake.systems.
SOC2 Type II audit: not yet initiated. In scope for the enterprise roadmap (BL-ENT-5).