# Tenuo Security Model

Source: https://tenuo.ai/security

This page covers what Tenuo protects against, how Proof-of-Possession works, integration safety mechanisms, and deployment best practices.

## Core Security Properties

| Property | How It Works |
|----------|--------------|
| **Scoped** | Warrants specify exactly which tools and constraints are allowed |
| **Temporal** | TTL checked on every authorization; expired warrants are rejected |
| **Bound** | Proof-of-Possession (PoP) required; stolen warrant is useless without private key |
| **Delegatable** | Parent warrants mint narrower children; signature chain proves lineage |
| **Revocable** | Signed revocation lists (SRL) checked locally by the authorizer |

---

## Proof-of-Possession (PoP)

Warrants are **bound to keys**. To use a warrant, you must prove you hold the private key.

```python
# Attenuate with explicit capability (POLA)
warrant = (root_warrant.grant_builder()
    .capability("protected_tool", path=Subpath("/data"))
    .holder(worker_key.public_key)
    .grant(root_key))  # Root key signs (they hold the parent warrant)

with warrant_scope(warrant), key_scope(worker_key):
    await protected_tool(...)
```

If an attacker steals the warrant token alone, they can't use it without the private key.

### Replay Protection & Statelessness

Tenuo uses time-windowed PoP signatures (~2 minutes) to allow for **stateless verification** and distributed clock skew. 

> [!IMPORTANT]
> **Residual Replay Risk**: Because the scheme is stateless (no nonce tracking in core), a valid PoP signature can be replayed **within the ~2 minute window**.

This is an intentional design trade-off for scalability. The protection prevents an attacker from using a stolen warrant *after* the window closes, but does not prevent immediate replay of the exact same request.

**Mitigation**: For sensitive tools, implement **application-level deduplication**:

```python
# Use the built-in helper to generate a deterministic cache key
dedup_key = warrant.dedup_key(tool, args)

if cache.exists(dedup_key):  # Redis, memcached, or in-memory
    raise ReplayError("Duplicate request")
    
authorizer.authorize(warrant, tool, args)
cache.set(dedup_key, "1", ttl=120)  # 120s covers the ~2min window
```

> [!WARNING]
> **Distributed Deployments**: The example above requires a shared cache backend. In-memory caches (like Python's `dict`) will not work across:
> - Multiple service instances (horizontal scaling)
> - Process restarts (cache is cleared)
> - Multi-process deployments (separate memory spaces)
>
> Use Redis, Memcached, or similar for production distributed systems.

> [!NOTE]
> **Performance & Responsibility**: You are responsible for provisioning and maintaining the storage backend (e.g., Redis). Tenuo provides the deterministic key but does not manage the statestore. The latency and availability of this check depend entirely on your storage infrastructure.

**When to implement deduplication:**
- High-value operations (payments, deletions, privilege escalation)
- Environments where network interception is possible
- Multi-step workflows where replay could cause inconsistency

**When you can skip:**
- Read-only operations (replaying a "read" is usually harmless)
- Idempotent operations (replaying has no additional effect)
- Very short-lived warrants (TTL < 2 min makes PoP window irrelevant)

---

## Monotonic Attenuation

Authority can only **shrink**, never expand:

| What | Rule |
|------|------|
| **Tools** | Child can only use a subset of parent's tools |
| **Constraints** | Child constraints must be tighter than parent's |
| **TTL** | Child cannot outlive parent |
| **Depth** | `max_depth` can only decrease |

```python
# Parent has broad capabilities
parent = (Warrant.mint_builder()
    .capability("read", path=Subpath("/"))
    .capability("write", path=Subpath("/"))
    .capability("delete", path=Subpath("/"))
    .holder(key.public_key)
    .ttl(3600)
    .mint(key))

# Child can only narrow
child = (parent.grant_builder()
    .capability("read", path=Subpath("/data"))
    .grant(key))  # Key signs (they hold the parent warrant)

# This would FAIL:
child = (parent.grant_builder()
    .capability("execute")  # FAILS (parent doesn't have "execute")
    .grant(key))
```

---

## Revocation

Tenuo's protocol includes signed revocation lists (SRLs) for emergency warrant cancellation. The authorizer checks an SRL locally: Python via `Authorizer.set_revocation_list()` / Temporal `revocation_list` + `revocation_list_provider`, Rust via `RevocationMode::SignedSrl` and `RevocationTracker`. Issuers sign the list; verifiers do not fetch it on the hot path.

**Design philosophy**: Tenuo favors **short TTLs (5-15 minutes) over revocation**. A warrant that expires naturally is simpler than one that requires emergency cancellation. Use revocation when TTL alone cannot meet your security requirements (e.g., long-lived sessions where key compromise must be handled mid-session).

For technical details, see **[Revocation](./spec/protocol-spec-v1#11-revocation)** in the protocol specification (SRL wire layout, `RevocationRequest`, verifier rules).

---

## Production Deployment Policy

> [!IMPORTANT]
> **Tier 2 (Warrant + PoP) is the recommended pattern for production systems.**
>
> While Tier 1 guardrails provide effective protection against prompt injection and accidental misuse, they can be modified or bypassed by anyone with code access. For production environments where insider threats or container compromise are concerns, use Tier 2 with cryptographic warrants.

---

## Tier 1 vs Tier 2

**Tier 1** (Runtime Guardrails):
- Constraint checking at runtime
- Trust boundary: code access
- Protects against: prompt injection, LLM hallucinations, SSRF
- Does NOT protect against: insider threats, container compromise

**Tier 2** (Cryptographic Authorization):
- All of Tier 1, plus warrant signatures and PoP
- Trust boundary: cryptographic proof
- Protects against: all Tier 1 threats, plus tampering and untrusted callers
- Required for: multi-process delegation, zero-trust environments, audit compliance

**When to use**: Tier 1 for single-process trusted environments. Tier 2 for distributed systems or when you cannot trust the execution environment.

See integration docs ([OpenAI](./openai.md#tier-1-security-model), [ADK](./google-adk.md#tier-1-security-model)) for detailed threat models.

---

## Key Management

**Zeroization**: Signing keys are automatically zeroized on drop (Rust `secrecy` crate + `ed25519-dalek`). Keys stored in memory are cleared when objects are destroyed.

**Rotation**: Key rotation is achieved through **warrant expiry**. Issue new warrants with new keys; old warrants expire naturally via TTL. No manual key rotation infrastructure needed.

**Storage**: Keys in Python integrations are stored in instance variables. For production high-security deployments, consider HSM/KMS integration or key provider abstraction.

---

## Security Invariants

All Tenuo integrations enforce these invariants (see [Integration Guide](https://github.com/tenuo-ai/tenuo/blob/main/tenuo-python/docs/integration-guide.md#invariant-testing)):

1. **Monotonic Attenuation** - Authority only decreases
2. **Fail-Closed** - Unknown parameters rejected
3. **Expiry Enforced** - Expired warrants fail even with valid signatures
4. **PoP Required** - Tier 2 requires proving key possession
5. **Signature Verification** - Tampering detected
6. **Chain Validation** - Delegation chains validated from root to leaf

---

## Threat Model

For the full threat model (what Tenuo protects against and what it does not), see [Concepts](./concepts#threat-model). This section covers operational security details.

### Defense in Depth: Network Policies

Tenuo handles authorization - what an agent is *allowed* to do. For exfiltration prevention, use Kubernetes Network Policies:

```yaml
# Restrict agent egress to only approved services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: agent-egress
spec:
  podSelector:
    matchLabels:
      app: agent
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: tool-proxy
```

Tenuo prevents unauthorized tool usage *through* your API. Network policies prevent bypassing your API entirely.

### Operational Checklist

1. All tools must be protected with `@guard` or `guard()`.
2. Restrict egress to prevent data exfiltration.
3. For agent compromise resilience, deploy sidecar or gateway enforcement (see [Enforcement Architecture](./enforcement)).

---

## Denial-of-Service (DoS) Protection

Tenuo is designed to protect validation services from CPU exhaustion attacks.

### Fail-Fast Cryptography

The authorization flow is strictly ordered to reject unauthorized requests before performing expensive logic:

1. **Expiration Check** (Values comparison): $\mathcal{O}(1)$ - Fails instantly if expired.
2. **Proof-of-Possession** (Ed25519 Verify): $\mathcal{O}(1)$ - Verifies the request signature. Fails fast if signature is invalid or missing.
3. **Constraint Matching** (Regex/Looping): $\mathcal{O}(N)$ - Only executed **after** the request is cryptographically authenticated.

**Why this matters**: An attacker cannot force the server to evaluate complex regex or deep constraint trees by sending 100k requests, because looking up constraints happens *after* the signature check. If they don't have a valid private key, the request is dropped with minimal CPU cost.

### Fail-Closed Validation (Zero Trust Data)

Tenuo extends Zero Trust beyond identity (keys) to **data validation**.

**Philosophy**: Ambiguity is a vulnerability. If Tenuo encounters data it doesn't strictly understand or expect, it fails closed (denies).

1.  **Closed-World Arguments**: If you confine a tool with constraints, *any* unmentioned argument triggers a denial. Zero Trust means no "shadow arguments" can sneak past validation.
2.  **Unknown Constraints**: If the runtime encounters a constraint type it doesn't recognize (e.g., mismatched version), it defaults to **DENY**. It never fails open.
3.  **Parser Safety**: URL and Path parsers are hardened against polyglot payloads (e.g., JSON-in-URL). If a payload looks malformed or ambiguous, it is rejected.

---

## Cost Containment

Prompt injection attacks can cause financial damage by tricking agents into making expensive API calls. Tenuo provides **stateless** mechanisms to contain costs while your infrastructure handles rate limiting.

### Parameter-Level Budget Constraints

Constrain cost-driving parameters directly in the warrant:

```python
warrant = (Warrant.mint_builder()
    .capability("call_llm",
        max_tokens=Range.max_value(1000),      # Cap output tokens
        model=OneOf(["gpt-3.5-turbo"]))        # No expensive models
    .capability("search_api",
        max_results=Range.max_value(10))       # Limit results per call
    .ttl(60)  # 1 minute window
    .mint(key))
```

### Single-Use Warrants for Expensive Operations

Issue terminal warrants (cannot be delegated) for each expensive call:

```python
async def safe_expensive_call(tool_name: str, params: dict):
    single_use = (Warrant.mint_builder()
        .capability(tool_name, **params)
        .holder(worker_key.public_key)
        .ttl(30)
        .terminal()
        .mint(issuer_key))

    with warrant_scope(single_use), key_scope(worker_key):
        return await execute_tool(tool_name, params)
```

### Orchestrator-Level Budget Tracking

Track call counts in your orchestrator:

```python
class BudgetedOrchestrator:
    def __init__(self, max_calls: int = 10):
        self.remaining_calls = max_calls
    
    async def execute_with_budget(self, task):
        if self.remaining_calls <= 0:
            raise BudgetExhausted("Call limit reached")
        
        self.remaining_calls -= 1
        
        warrant = (Warrant.mint_builder()
            .capability(task.tool, **task.constraints)
            .holder(self.worker_key.public_key)
            .ttl(30)
            .terminal()
            .mint(self.key))

        with warrant_scope(warrant), key_scope(self.worker_key):
            return await task.execute()
```

### Gateway-Side Rate Limiting

Your API gateway should enforce call counts per warrant:

```yaml
# Kong rate limiting example
plugins:
  - name: rate-limiting
    config:
      minute: 10                    # 10 calls per minute per warrant
      policy: local
      header_name: X-Tenuo-Warrant-Id  # Custom header for rate limiting (distinct from auth)
```

### Strategy Summary

| Strategy | Where | Stateful? | Best For |
|----------|-------|-----------|----------|
| Parameter constraints | Warrant | No | Limiting per-call cost |
| Short TTLs + terminal | Warrant | No | Time-boxing exposure |
| Orchestrator budget | Application | In-memory | Task-level budgets |
| Gateway rate limiting | Infrastructure | Yes | Hard call limits |

**Design principle:** Tenuo handles **authorization** (what CAN be done). Your infrastructure handles **rate limiting** (how MANY times). This keeps warrant verification local, offline, and stateless. See [Performance Benchmarks](./api-reference#performance-benchmarks) for measured timings.

---

## Integration Safety

> **The Primary Attack Surface: Integration Mistakes**

Tenuo's core is cryptographically secure. But **integration bugs** are the primary attack surface:
- Forgetting to add `@guard` to a tool
- Missing `warrant_scope()` or `mint()`
- Dynamic nodes without wrappers
- Wrapper that checks tool names but skips `validate()`

### Strict Mode

**Fail-closed enforcement**: Panic if a tool is called without warrant context.

```python
from tenuo import configure, SigningKey

configure(
    issuer_key=SigningKey.generate(),
    strict_mode=True,  # Enforce warrant presence
)
```

**Behavior:**

```python
@guard(tool="read_file")
def read_file(path: str):
    return open(path).read()

# Called without warrant context
read_file("/data/test.txt")
# RuntimeError: [MISSING_CONTEXT] No warrant context available for tool 'read_file'.
```

**When to use:**
- **Development/staging** - Catch integration bugs early
- **CI/CD** - Fail tests if warrant context is missing  
- **Production** - Only if you want hard failures

### Warning Mode

**Loud warnings**: Log and warn (but don't crash) when tools are called without warrants.

```python
configure(
    issuer_key=SigningKey.generate(),
    warn_on_missing_warrant=True,
)
```

> [!CAUTION]
> **Production Safety**: Never set `TENUO_ENV="test"` in production environments.
> This environment variable enables special test-only bypass modes (like `allow_any()`) which
> completely disable authorization checks. Tenuo will emit warnings if this is detected,
> but for defense-in-depth, ensure your production manifests (Helm, Terraform) strictly avoid this variable.

> [!IMPORTANT]
> **`TENUO_REQUIRE_EXTENSION=1`**: Set this in all production deployments.
> If the native Rust extension (`tenuo_core`) is missing — for example, after a Docker image rebuild
> that drops the wheel — warrant enforcement silently degrades to a no-op without this flag.
> With it, the process exits at startup with a clear error instead. See [Enforcement Architecture: Production Hardening](./enforcement#production-hardening).

### Mode Comparison

| Mode | Missing Warrant Behavior | Use Case |
|------|-------------------------|----------|
| **Default** | Raises `Unauthorized` | Production (minimal overhead) |
| **`warn_on_missing_warrant=True`** | Warns + raises | Development/staging |
| **`strict_mode=True`** | Panics with `RuntimeError` | CI/CD, strict production |

### Common Integration Bugs

| Bug | Detection |
|-----|-----------|
| Missing `@guard` decorator | Code review, linting |
| Missing `warrant_scope()` | Strict mode catches |
| Dynamic node without wrapper | Strict mode (if tools decorated) |
| Wrapper skips `validate()` | Integration tests |

### Async Context Sharp Edges

```python
# Works correctly
async with mint(Capability("search")):
    result = await search("query")

# Context not propagated (task created BEFORE context)
task = asyncio.create_task(search("query"))
async with mint(Capability("search")):
    await task  # Task runs without context

# Fix: create task INSIDE context
async with mint(Capability("search")):
    task = asyncio.create_task(search("query"))
    await task
```

---

## Control Plane Deployment Models

The control plane holds the **root signing key** and issues the initial warrant for each agent network.

### Level 1: Embedded (Development)

```python
root_key = SigningKey.from_env("TENUO_ROOT_KEY")
warrant = (Warrant.mint_builder()...mint(root_key))
```

| Aspect | Detail |
|--------|--------|
| Pros | Zero infrastructure overhead |
| Cons | RCE on orchestrator exposes root key |
| Use case | Local dev, CI/CD, non-critical agents |

### Level 2: Isolated Signing Service (Production)

```
Orchestrator  -->  gRPC/mTLS  -->  Signing Service (holds key)
```

| Aspect | Detail |
|--------|--------|
| Pros | Key isolation; RCE can only request warrants |
| Cons | One additional service to run |
| Use case | Production Kubernetes, standard SaaS |

### Level 3: Hardware Root of Trust (High Assurance)

```
Orchestrator  -->  AWS KMS / GCP KMS / HSM  -->  Signed warrant
```

| Aspect | Detail |
|--------|--------|
| Pros | Key is non-exportable; instant revocation via IAM |
| Cons | ~50-100ms issuance latency |
| Use case | FinTech, HealthTech, regulated industries |

---

## Cycle Protection

Tenuo prevents infinite delegation cycles through multiple layers:

1. **Warrant ID Tracking**: Same ID twice → fail
2. **Depth Limits**: `MAX_DELEGATION_DEPTH = 64`
3. **Monotonic Attenuation**: Each warrant strictly weaker
4. **Self-Issuance Prevention**: Issuer warrants cannot grant execution to themselves

---

## Protocol Limits

| Limit | Value | Purpose |
|-------|-------|---------|
| `MAX_DELEGATION_DEPTH` | 64 | Prevents unbounded delegation chains |
| `MAX_WARRANT_TTL_SECS` | 90 days | Protocol ceiling for warrant lifetime |
| `MAX_WARRANT_SIZE` | 64 KB | Prevents memory exhaustion (single warrant) |
| `MAX_STACK_SIZE` | 256 KB | Max WarrantStack encoded size (chain) |
| `MAX_CONSTRAINT_DEPTH` | 32 | Prevents stack overflow in nested constraints |
| PoP Timestamp Window | ~2 min | Replay attack protection |

**TTL Note**: 90 days is the protocol maximum. Deployments should configure stricter limits (e.g., 24 hours for production). Default TTL is 5 minutes.

---

## Best Practices

### 1. Wrap All Tools

```python
# Good
@guard(tool="delete_file")
def delete_file(path: str): ...

# Bad: bypasses Tenuo
await http_client.delete(url)
```

### 2. Use Short TTLs

```python
# Good: 5 minute TTL
warrant = (Warrant.mint_builder()...ttl(300).mint(key))

# Risky: 24 hour TTL
warrant = (Warrant.mint_builder()...ttl(86400).mint(key))
```

### 3. Principle of Least Privilege

```python
# Good: only what's needed
with mint(Capability("read_file", path="/data/reports/*")):
    ...

# Risky: overly broad
with mint(
    Capability("read_file", path="/*"),
    Capability("write_file", path="/*"),
    Capability("delete_file", path="/*")
):
    ...
```

### 4. Separate SigningKeys per Trust Boundary

- Control plane: One signing key
- Each worker: Own signing key
- Don't share keys across trust boundaries

### 5. Use Strict Mode in Tests

```python
# conftest.py
@pytest.fixture(scope="session", autouse=True)
def tenuo_strict():
    configure(
        issuer_key=SigningKey.generate(),
        dev_mode=True,
        strict_mode=True,  # Fail tests if warrant missing
    )
```

---

## See Also

- [Concepts](./concepts): Problem/solution, threat model, core invariants
- [AI Agent Patterns](./ai-agents): P-LLM/Q-LLM, prompt injection defense, multi-agent security
- [Enforcement Architecture](./enforcement): Deployment patterns and proxy configurations
- [Constraints](./constraints): Constraint types, argument extraction, gateway configuration
- [Protocol Specification](./spec/protocol-spec-v1): Full protocol details
- [API Reference](./api-reference): Python SDK, CLI, and performance benchmarks
