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
subclaim against thesovereign_agentstable, then verifies signature + claims (tenant_idmust match path parameter,expmust 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.jsonlisting 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, themetadatadict (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
anthropicor 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.contextfor the string"BEGIN PRIVATE"returns zero rows). - Private key never logged by structlog (
grep -r "private_key_pem" apps/cp-api/srcshows only places where it's returned in the HTTP response body, neverlogger.info(...)). -
verify_agent_jwtrequiresaud=secruna-cp-api+tenant_idclaim; tested intests/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)¶
- 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. - Empty poll endpoint returns empty
tasks: []. The actual work distributor (cp-api queuing discovery instructions to specific agents) is a separate plan. - CDN upload is operator-side. cp-api builds + signs locally; ops uploads from
dist/. Plan 118.5 wires direct blob upload. - 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.
- 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.