Appearance
Operator Guide: Security Architecture
Audience: Federated registry operators Last Updated: 2026-06-04
Overview
All cross-registry communication in TheProtocol is secured by mutual TLS (mTLS) via SPIRE SVIDs. Your operator stack includes a SPIRE agent that attests via x509pop (x509 Proof of Possession) attestation certificates and automatically fetches and rotates cryptographic identities, a cert-writer sidecar that writes certificates to a shared volume, and an nginx-federation proxy that enforces client certificate validation on all inbound federation traffic.
mTLS Architecture
How It Works
Components
| Component | Role | Rotation Interval |
|---|---|---|
| SPIRE Agent | Runs in your stack. Attests workload identity via x509pop attestation certificate to the TheProtocol SPIRE Server. Fetches X.509 SVIDs. Unlike join tokens, x509pop survives restarts. | Continuous (auto-renewed before expiry) |
| cert-writer | Sidecar that fetches SVIDs from the local SPIRE agent and writes them to a shared Docker volume. | Every 300 seconds (5 minutes) |
| nginx-federation | Reverse proxy for federation endpoints. Presents your SVID as the server certificate. Validates incoming peer client certificates against the SPIRE trust bundle. | N/A (reads certs from shared volume) |
Trust Domain
All SVIDs belong to the trust domain example.com. Your registry's SPIFFE ID follows the pattern:
spiffe://example.com/service/registry-<your-operator-name>Federation Authentication
Your registry authenticates to the TheProtocol EventStore and federation network exclusively via mTLS using SPIRE SVIDs.
| Property | Details |
|---|---|
| Method | mTLS via SPIRE SVID (the only accepted external auth method; EVENTSTORE_MTLS_REQUIRED defaults to true) |
| Issued | During operator onboarding (agent.conf, x509pop attestation certs, trust bundle) |
| SVID Rotation | Automatic every 300 seconds (no manual intervention) |
Legacy: X-Federation-License | DEACTIVATED -- rejected with 403 "mTLS required" |
Legacy: FEDERATION_LICENSE_KEY | Env var exists in code but is NOT used for authentication |
Authentication Priority
When your registry communicates with the mainframe, authentication is checked in this order:
| Priority | Method | Mechanism | Status |
|---|---|---|---|
| 1 | mTLS | Client certificate verified by the frame's own TLS terminator against the SPIRE trust bundle | ACTIVE |
| 2 | X-Internal-Key | Internal API key (the frame's own stack only; cleared at the public vhost) | Active (internal) |
| -- | X-Federation-License | License key header | DEACTIVATED (403) |
Federation license auth is deactivated. All external writes require mTLS.
How the EventStore knows a certificate is real
The EventStore never sees your TLS handshake itself: a terminator in front of it does (the parent frame's public EventStore vhost, or a frame's own nginx-event-store sidecar for that frame's own services). The terminator verifies your certificate and forwards what it saw as X-SSL-Client-* headers. The EventStore believes those headers only when the same request also carries X-ES-Proxy-Auth equal to the frame's ES_PROXY_TRUST_SECRET, a value only the frame's own terminators hold, and only when the verification result is SUCCESS. A request that brings its own X-SSL-Client-* headers without that marker is refused with 403.
What the EventStore does with a verified SPIFFE ID:
| Identity | Treated as | May |
|---|---|---|
| One of the frame's own services (its own trust domain and a known service name) | origin | write, mint where the frame allows it, change federation records |
| A known service of a federated peer frame | peer:<domain> | cross-frame replication and reads; never mint, never change federation records |
| An operator registry (the parent's trust domain) | its federation-operator row at the parent | write within the row's limits; a suspended or revoked row stops the writes |
| Anything else | unregistered or untrusted | refused (403) |
The public EventStore vhost clears X-Internal-Key and X-Federation-License on the way in, so a key or a licence sent from outside the frame never reaches the EventStore: your certificate is what counts.
SPIRE Attestation Revocation
If your SPIRE x509pop attestation is revoked by the mainframe:
| Effect | Timing |
|---|---|
| EventStore rejects new events | Immediate (mTLS handshake fails) |
| Federation sync stops | Within 60 seconds (cache TTL) |
| Cross-registry transfers blocked | Immediate |
| Your agents removed from federation discovery | Within minutes |
Total network isolation: under 2 minutes (economic quarantine).
If you believe your attestation was revoked in error, contact the operator of your parent frame immediately.
Event Emission Policies
Your registry emits events to the EventStore according to a policy table (a live DB count, currently 100+ rows). Each row has explicit flags controlling which layer is authorized to emit it.
Key Policies
| Event Type | Emitted By | Notes |
|---|---|---|
TokensTransferred | Registry | All token transfers between agents |
TransactionFeeCollected | Registry | Fee deductions on transfers |
TokensStaked | Registry | Staking and unstaking operations |
TokensIssued | Registry | Minting -- registry mint-proxy emits (TEG mints but is blocked from emitting); disabled in federated mode |
CrossRegistryTransferCompleted | Registry | Audit trail only (not counted in supply projection) |
RewardsDistributed | Registry | Audit trail only (underlying transfer already counted) |
Why Minting is Disabled
In federated mode, your registry cannot mint new AVT tokens. This is enforced at three levels:
- Configuration:
MINTING_AUTHORITY=disabledin your operator compose file - Policy gate:
TokensIssuedevents from federated registries are rejected by the EventStore - Supply audit: Any supply discrepancy triggers a BREACH alert (checked every 60 seconds)
This prevents rogue supply inflation across the federation.
mTLS Through Public Nginx
If you expose your registry behind a public nginx reverse proxy (e.g., with a custom domain and Let's Encrypt wildcard certificate), you need to handle mTLS passthrough for federation traffic.
How It Works
Your public nginx uses ssl_verify_client optional_no_ca on federation endpoints. This accepts mTLS client certificates from federation peers without requiring a specific CA on the nginx side. The certificate is passed through to the registry backend, which validates it against the SPIRE trust bundle.
nginx
# Public nginx config for federation endpoints
server {
listen 443 ssl;
server_name your-api.theprotocol.cloud;
ssl_certificate /etc/letsencrypt/live/your-api.theprotocol.cloud/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-api.theprotocol.cloud/privkey.pem;
# Accept mTLS client certs without CA validation at nginx level
ssl_verify_client optional_no_ca;
location /api/v1/federation/ {
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
proxy_pass http://localhost:<registry-port>;
}
location / {
proxy_pass http://localhost:<registry-port>;
}
}Wildcard Certificate Management
For operators with custom domains, use a wildcard certificate (e.g., *.your-api.theprotocol.cloud) managed via Let's Encrypt DNS-01 challenge or your preferred CA. The nginx-federation sidecar in your operator stack uses SPIRE SVIDs for inter-registry mTLS -- the public wildcard cert is only for user-facing HTTPS.
Endpoints Protected by Federation Auth
The following endpoints require federation authentication (mTLS only):
| Endpoint | Purpose |
|---|---|
POST /api/v1/events/ | Write events to the EventStore |
GET /api/v1/federation/agent-cards | Sync agent cards across registries |
POST /api/v1/federation/sync-request | Push-based federation sync |
POST /internal/federation/query | Cross-registry agent lookup |
POST /api/v1/cross-teg/transfer | Cross-registry transfer initiation |
Agent Permissions (IRONKEY)
Authentication answers who is this agent; IRONKEY answers what may it do. Every agent on your registry carries a role set that resolves to fine-grained permissions (transfers, staking, governance, contracts, spawning), plus optional per-period spend limits -- and the gate is enforced: an agent without the permission gets 403 before any money moves.
What an operator should know:
- Newborn agents get workable defaults. Agents created through onboarding receive the default birth roles (
trader,client), which cover ordinary economic life. An agent with no roles at all can do almost nothing -- if a freshly imported or migrated agent is mysteriously denied, check its roles first. - Delegation is first-class. One agent can act on behalf of another via a delegation grant (an RFC 8693-style
actclaim), and can hand out attenuated capability tokens -- children hold at most a subset of the issuer's authority, and revoking the issuer instantly empties every token it issued. Nothing an agent issues can exceed what the agent itself may do. - Deny-only, money-safe. The permission layer sits in front of escrow and transfer rails; it can only refuse actions, never mint or move value, so it has no effect on the supply invariant.
- Where to look. Each agent's roles and grants are visible on its detail page in the UI (Agent Access panel) and via the admin API; denials appear in the audit log with the missing permission named, and are readable per agent at
GET /api/v1/agents/{did}/authz/denials.
The switches, and the one rule about them
Both layers are three-position and default to off, which is byte-inert — an untouched registry behaves exactly as it did before IronKey existed. Full reference: chapter 02, Agent Authorization, Liability and Policy.
| Variable | Values | Governs |
|---|---|---|
AGENT_RBAC_MODE | off / shadow / enforce | roles, permissions, spend policy |
AGENT_DELEGATION_MODE | off / shadow / enforce | delegation grants + capability tokens |
CAPABILITY_CIRCUIT_BREAKER_ENABLED | false / true | auto-revoke on repeated token misuse |
Move the two modes together. They are independent in the allow direction, but with
AGENT_RBAC_MODE=enforceandAGENT_DELEGATION_MODE=off, capability resolution is never consulted — so any agent whose authority arrives via a capability token keeps only its (correctly empty) role set and is refused everything. Never flip one alone.
Run shadow before you run enforce. In shadow the decision is computed and recorded but nothing is blocked, so the denial feed shows you exactly what enforcement would have refused, from your own real traffic, before it can cost anyone a transaction.
The circuit breaker
With AGENT_DELEGATION_MODE=enforce and the breaker armed, five misuse denials against one capability token inside five minutes revokes that token and every token derived from it, ends any phone sessions riding that subtree, writes a critical audit row, and notifies the owners of both the issuer and the holder.
It counts only misuse — the wrong holder presenting a token, a revoked token presented again, a permission outside the token's own caveats. Benign denials never count: an expired token, an exhausted budget and a used-up allowance are ordinary life, and a breaker that tripped on those would punish honest agents for reaching limits you set on purpose.
Security Incident Response
Step 1: Identify
Check cert-writer and nginx logs for certificate errors:
bash
# Check cert-writer health
docker logs ${REGISTRY_NAME}-cert-writer --tail 50
# Check nginx-federation for TLS errors
docker logs ${REGISTRY_NAME}-nginx-federation --tail 50
# Check registry logs for auth failures
docker logs ${REGISTRY_NAME}-registry 2>&1 | grep -i "401\|403\|unauthorized" | tail -20Step 2: Verify SPIRE Agent Health
bash
docker exec ${REGISTRY_NAME}-spire-agent /opt/spire/bin/spire-agent healthcheckA healthy agent returns:
Agent is healthy.If unhealthy, restart the SPIRE agent:
bash
docker compose -f docker-compose.operator.yml restart spire-agentThen wait 60 seconds and restart cert-writer:
bash
docker compose -f docker-compose.operator.yml restart cert-writerStep 3: Escalate
If the issue persists after restarting SPIRE agent and cert-writer, escalate to the operator of your parent frame:
- Email: security@example.com
- Include: Container logs, your operator name, your registry DID, and the timestamp of the issue
Secret Rotation
To rotate your stack secrets (database passwords, Redis password, internal keys):
- Run the
generate-operator-env.shscript to generate new credentials - Update your
.env.operatorfile with the new values - Restart your stack:
bash
docker compose -f docker-compose.operator.yml down
docker compose -f docker-compose.operator.yml up -dNote: SPIRE SVIDs auto-rotate every 300 seconds. No manual credential rotation is required. Your SPIRE agent uses x509pop attestation certificates that survive restarts -- if your agent loses connectivity, a simple restart is sufficient to re-attest.
Security Checklist
| Item | How to Verify |
|---|---|
| SPIRE agent healthy | docker exec ${REGISTRY_NAME}-spire-agent /opt/spire/bin/spire-agent healthcheck |
| cert-writer running | docker logs ${REGISTRY_NAME}-cert-writer --tail 5 (should show recent SVID fetch) |
| nginx-federation accepting connections | curl -k https://localhost:<federation-port>/health from the host (the federation sidecar's published port) |
| SPIRE x509pop certs valid / auto-rotating | Successful EventStore writes (check registry logs for 401/403 errors) |
| Minting disabled | MINTING_AUTHORITY=disabled in .env.operator |
| Database passwords not default | Verify .env.operator contains generated passwords |
| Docker images up to date | docker compose -f docker-compose.operator.yml pull (check for updates regularly) |