Threat model
Attacker goals, the mechanism that refuses each one, and the residual risk that remains.
Written as goals an attacker would have, because that is how the defences were
chosen. Every case has a corresponding test in tests/security — 49 of them. A
threat model without tests is a wish list.
#Impersonate an agent
| Goal | Refused by |
|---|---|
| Use a stolen agent id | Ids are not credentials. A signed assertion over the private key is required |
| Replay a captured assertion | Single-use nonce → NONCE_REPLAYED |
| Use an expired assertion | Short exp, plus a timestamp window → TIMESTAMP_OUT_OF_WINDOW |
| Replay an assertion against a different environment | aud is bound and compared exactly |
| Use a key after rotation | Generation counter → IDENTITY_GENERATION_STALE |
| Use a key after revocation | AGENT_IDENTITY_REVOKED. Terminal |
| Forge a passport | Platform Ed25519 signature over canonical JSON |
| Present a stale passport | The gateway re-resolves live authority; it never authorizes from a passport |
Residual risk. An attacker who obtains the private key and the agent id can act as that agent until it is revoked. The mitigations are rotation, short-lived sessions, and the fact that the agent's authority is bounded — so what they get is the agent's bounded authority, not open access.
veefore doctor checks local key file permissions; a key readable by another
process on the host is a key that process can act with.
#Escalate authority
| Goal | Refused by |
|---|---|
| Delegate more than you hold | DELEGATION_ESCALATION at creation. Refused, not clamped |
| Widen an existing delegation | Same invariant on PATCH |
| Extend an expiry past the parent's | Same |
| Exploit a long chain | Depth bounded by max_depth; narrowest value wins at every dimension |
| Use a revoked parent's grant | Revocation cascades at every depth, immediately |
| Call a tool without the capability | CAPABILITY_NOT_GRANTED |
| Call a tool you hold the capability for but are not authorized for | TOOL_NOT_AUTHORIZED |
| Gain reach by registering a new tool | Registration grants nothing. Authorization is a separate step |
| Gain reach via MCP discovery | A discovered tool is unauthorized until a human authorizes it |
| Obtain a high-impact permission through a wildcard role | HIGH_IMPACT_PERMISSIONS must appear explicitly |
| Use an old session for an irreversible operation | STEP_UP_PERMISSIONS require fresh re-authentication |
Residual risk. An agent granted too much authority in the first place is operating within its grant. Veefore bounds and records; it cannot know your intent. Review delegations monthly and grant the narrowest ceiling that works.
#Steal a credential
| Goal | Refused by |
|---|---|
| Read a credential from the API | No endpoint returns material. Metadata only |
| Extract it from an agent's context | The agent never holds it. Only short-lived, scoped leases |
| Find it in logs | Redaction applied on ingest, not on output |
| Find it in the audit trail | Leases recorded by reference, never by value |
| Find it in an error message | Messages never contain secret values |
| Use a lease after rotation | Rotation invalidates every outstanding lease |
| Get a lease issued for a blocked action | Brokering is stage 10, after every refusal |
| Find one committed to source | npm run quality:audit fails the build |
Residual risk. A leaked lease is usable within its short lifetime for the
single action it was scoped to. A compromise of
VEEFORE_DATA_ENCRYPTION_KEY and the database would expose stored material —
which is why that key belongs in a secret manager and not in the database.
#Reach another tenant's data
| Goal | Refused by |
|---|---|
| Pass another organization's resource id | Repository layer requires a tenant scope → TENANT_MISMATCH |
| Exploit a query that forgot its scope | PostgreSQL row-level security, as an independent second layer |
| Use scope headers you have no access to | Scope is validated against the session |
| Enumerate ids | TENANT_MISMATCH rather than 404, so the attempt is distinguishable in audit |
Residual risk. A bug in both layers simultaneously. The two are independent
by design — one in application code, one in the database — so a single mistake
is not sufficient. Enable DATABASE_ENABLE_RLS in production.
→ Tenant isolation
#Make the agent do something harmful
This is the category people care most about, and the honest answer is a reduction in reward rather than prevention.
| Goal | Refused by |
|---|---|
| Inject a prompt to steal a credential | There is nothing to steal |
| Inject a prompt to exceed a ceiling | AUTHORITY_EXCEEDED at stage 6 |
| Inject a prompt to call an unauthorized tool | TOOL_NOT_AUTHORIZED at stage 5 |
| Inject a prompt to reach an unexpected host | DESTINATION_BLOCKED at the egress boundary |
| Inject a prompt to drop a table | Starter policies block DELETE, DROP, TRUNCATE |
| Talk the agent past a limit | Limits are server-side numbers the agent cannot see |
| Convince the agent it is authorized | Belief is irrelevant. The gateway decides |
Veefore does not prevent prompt injection
It makes injection much less rewarding. The agent has no credential to exfiltrate, and anything it is talked into attempting still runs the full twelve-stage pipeline against the same authority and the same policy.
Authorization in a system prompt is not authorization. "You may only refund up to ₹1,000" is an instruction, and instructions are negotiable. Put the ceiling in a delegation. → The boundary
Residual risk. An agent authorized to do something harmful, doing it. The value is bounded authority, a recorded decision and a known blast radius. Narrow grants are the mitigation.
#Evade the audit trail
| Goal | Refused by |
|---|---|
| Alter one event | Keyed hash chain; the break is detectable and localised |
| Delete an event | Same |
| Act without being recorded | Every pipeline run writes an action record |
| Export evidence unnoticed | Export requires a reason and is itself an audit event |
| Shorten retention quietly | Step-up gated, CRITICAL audit event |
| Alter an exported file | Exports are watermarked and hashed |
Residual risk. An attacker with database write access and
VEEFORE_AUDIT_CHAIN_SECRET could rewrite the chain consistently. Keep the
secret out of the database, restrict write access, and ship events to an
append-only sink you control separately.
→ Audit integrity
#Use availability as an attack
| Goal | Refused by |
|---|---|
| Make policy unreachable so everything is allowed | Fail-closed. POLICY_SERVICE_UNAVAILABLE, actions refused |
| Make identity unreachable | Same |
| Overwhelm the gateway to slip a request through | Rate limiting; refusal under pressure, not permission |
| Exhaust capacity through one broken downstream | Per-tool circuit breaker after 5 failures |
| Flood with requests | Velocity limits are themselves a policy decision |
This is why fail-closed is not negotiable
Under fail-open, every denial-of-service becomes a privilege escalation — and making a service unreachable is far easier than defeating its policy. The cheapest attack would become an attack on availability rather than on correctness. → Fail closed
Residual risk. Protected actions stop during an outage. The mitigation is redundancy, not a bypass.
#Attack the egress path
| Goal | Refused by |
|---|---|
| Point a tool at an internal host | The tool's own destination_allowlist, checked again at egress |
| Use a redirect to reach a non-allowlisted host | redirect: 'error'. Redirects are refused, not followed |
| Exfiltrate over plaintext | HTTPS required unless the host is loopback |
| Hold a connection open to exhaust the gateway | 15-second egress timeout, plus the circuit breaker |
The redirect refusal is worth calling out: following a redirect is how an allowlisted host becomes a request to a host that was never allowlisted. That is server-side request forgery with extra steps, and the only safe handling is to refuse.
#Attack a human session
| Goal | Refused by |
|---|---|
| Use a stolen session for an irreversible action | Step-up required at the moment of use |
| Use an idle session | SESSION_IDLE_TIMEOUT_SECONDS |
| Brute-force a password | MAX_LOGIN_ATTEMPTS and lockout |
| Reach the API from an unexpected network | IP allow/deny rules → NETWORK_RESTRICTED |
| Approve from an unattended browser | requireStepUp on the approval rule |
#Out of scope for this model
Physical access to the database host. Standard infrastructure security.
A compromised Veefore deployment. If an attacker controls the gateway process, they control authorization. Protect it as you would any critical service.
Your downstream's own security. Veefore brokering a credential does not make an unauthenticated API safe.
Model behaviour. Veefore bounds what an agent may do. It does not evaluate whether a model's reasoning is sound.
#Verifying this page
bashnpm run test:security # 49 cases
npm run test:chaos # 13 cases, fail-closed and recovery
npm run smoke:gateway # replay refusal, enforcement, revocation, over real HTTP