Przejdź do treści

Sovereign agent — architecture, trust model, security review checklist

Plan 118 engineering reference. Read alongside the operator runbook (docs/operations/sovereign-agent.md) and the customer guide (docs/customer/sovereign-agent-deployment.md).

Data flow

Customer environment                              Secruna control plane
─────────────────────────                         ────────────────────────
                                                  cp-api (FastAPI)
                                                   ├── /agent/{tenant_id}/poll
                                                   ├── /agent/{tenant_id}/verdicts
                                                   └── /agent/{tenant_id}/heartbeat
                                                  Postgres
                                                   ├── sovereign_agents (public keys)
                                                   └── audit_log
                                                  CDN
                                                   └── rule_book-{ver}.tar.gz + .sig

sovereign-agent container
 ├── main.py loop                                  outbound HTTPS only
 │   ├── poll  (every 30s)             ────────►   cp-api /agent/{tenant_id}/poll
 │   ├── post_verdict                  ────────►   cp-api /agent/{tenant_id}/verdicts
 │   └── heartbeat (every 60s)         ────────►   cp-api /agent/{tenant_id}/heartbeat
 ├── rule_book_fetcher (every 15 min)  ────────►   CDN GET rule_book.tar.gz + .sig
 │   └── verify_rule_book_artifact (local crypto)
 └── discovery + classify pipeline
     ├── connectors (AWS / GCP / Azure / GH / …)   stays in-customer
     ├── extractor (LLM call)                      LLM choice (Ollama / Azure OAI / …)
     └── classifier (rule book applied)            stays in-customer

Trust model

Inbound authentication (agent → cp-api)

  • Agent holds an RSA-2048 private key generated at provision time. Secruna never sees it post-provision.
  • On every cp-api call the agent mints a fresh JWT:
  • alg=RS256, typ=JWT, kid=sha256(public_pem)[:16].
  • Claims: sub (registered subject), aud=secruna-cp-api, tenant_id, iat, nbf=iat-30, exp=iat+300, jti (random UUID).
  • cp-api resolves the public key by sub claim against the sovereign_agents table, then verifies signature + claims (tenant_id must match path parameter, exp must be in the future).
  • Soft-revoke (revoked_at IS NOT NULL) immediately invalidates every future JWT for that subject.

Outbound (cp-api → CDN → agent)

  • Rule book artifacts are signed RSA-PSS-SHA256 by cp-api with the rule-book signing private key (operator-controlled, KV-stored).
  • Agent verifies signatures against the embedded public key. Verification failure → log + keep running on previous rule book; no fail-open.
  • The artifact carries MANIFEST.json listing per-file SHA-256; the agent re-hashes every member and rejects extras / missing / mismatched files.

What stays in the customer's network

  • Cloud account inventory (AWS Lambda IDs, ARNs, etc.) — only canonical IDs ever leave (e.g. the Lambda name); the function body / code never does.
  • LLM input + output — extractor invokes the customer's LLM endpoint, sees the full prompt + completion locally, then discards them.
  • Rule book content — version hash leaves (so cp-api can check it's a published version), the rules themselves never round-trip.

What leaves the customer's network

  • Verdict metadata: ai_system_id, framework_id, verdict_category, rule_version_hash, the metadata dict (source vendor + canonical id + discovery time).
  • Heartbeat: agent version, current rule book hash, queue depth.
  • Standard outbound HTTPS to the LLM provider if the customer picks anthropic or any cloud-hosted LLM. This caveat is loud in the customer guide.

Key crypto primitives

Surface Primitive Why
Agent → cp-api JWT RS256 / PKCS#1 v1.5 / SHA-256 Industry-standard, portable across Python / JS / Go for future agent ports.
Rule book artifact RSA-PSS / MGF1-SHA256 / salt=digest Modern preferred padding for standalone artifact signing in 2026.
Subject derivation secruna-agent-{tenant_id}-{base64url 9 bytes} Globally unique by 72-bit nonce. DB partial-unique on tenant_id WHERE revoked_at IS NULL enforces single-active-per-tenant.
Manifest digest SHA-256 per file, sorted before JSON serialization Deterministic byte representation enables reproducibility checks.

Failure modes

Failure Behaviour Audit row
Bad JWT signature 401 sovereign_agent.invalid_jwt reason=invalid_jwt
Unknown subject 401 sovereign_agent.invalid_jwt reason=unknown_subject
Revoked agent 401 sovereign_agent.invalid_jwt reason=revoked
tenant_id path ≠ JWT claim 401 sovereign_agent.invalid_jwt reason=tenant_mismatch
Verdict has unknown verdict_category 422 none (validation-only)
Verdict has unrecognised rule_version_hash 422 sovereign_agent.rule_book_outdated
Bad rule book signature Agent logs rule_book_fetcher.verification_failed, keeps running on previous rule book None on cp-api side
LLM endpoint down (customer side) Discovery task fails locally; last_queue_depth rises in heartbeat None on cp-api side

Security review checklist

Pre-shipping reviewers should confirm:

  • Private key never written to the audit log (search audit_log.context for the string "BEGIN PRIVATE" returns zero rows).
  • Private key never logged by structlog (grep -r "private_key_pem" apps/cp-api/src shows only places where it's returned in the HTTP response body, never logger.info(...)).
  • verify_agent_jwt requires aud=secruna-cp-api + tenant_id claim; tested in tests/unit/shared/test_agent_auth.py.
  • Rule book artifact verifier rejects: (a) bad signature, (b) wrong public key, © tampered tarball, (d) manifest with extra files, (e) manifest with missing files, (f) manifest with mismatched digests. All covered in tests/unit/agent/rule_book/test_verify_signed_artifact.py.
  • sovereign_agent admin endpoints are 403 for non-platform-admins (tests/integration/cp/sovereign_agent/test_sovereign_agent_endpoints.py::test_provision_agent_403_for_non_platform_admin).
  • Cross-tenant JWT replay (signed for tenant_a, presented to tenant_b's path) is rejected (test_jwt_for_other_tenant_is_rejected).
  • Revoked agent's JWT is rejected (test_verdict_rejected_after_revoke).
  • Stale rule_version_hash → 422 + audit (test_verdict_rejected_when_rule_version_hash_is_bad).

Known v1 limitations (Plan 118.5 follow-ups)

  1. No JWT replay nonce cache. A leaked JWT can be replayed until its exp. Mitigation: 5 min lifetime + per-request audit row. Plan 118.5 adds a Redis-backed nonce cache.
  2. Empty poll endpoint returns empty tasks: []. The actual work distributor (cp-api queuing discovery instructions to specific agents) is a separate plan.
  3. CDN upload is operator-side. cp-api builds + signs locally; ops uploads from dist/. Plan 118.5 wires direct blob upload.
  4. Rule book hash registry is shape-checked only. Any 16-64 char hex string passes; the actual cross-reference against published versions is deferred. Real registry lookup ships with Plan 118.5.
  5. Heartbeat history is lossy. Only the latest values are kept on sovereign_agents. Historical heartbeats live in audit log only if you stamp them yourself.