Configuration¶
This page documents the v1.2.0 configuration surface. The release is backward compatible:
policies default to identity: "tenant_jwt"; add identity: "anonymous" only to endpoints
that should accept unauthenticated requests.
Sentinel loads a single strict JSON configuration file. SentinelConfig is frozen and
rejects unknown keys; every policy must be keyed by (and declare) its explicit endpoint_id,
and per-algorithm field mixing is rejected at load. A working example ships in the repo at
sentinel.example.json.
{
"app": {
"redis_url": "redis://localhost:6379/0",
"jwt_secret": "dev-only-secret-change-me-0123456789abcdef",
"jwt_algorithm_allowlist": ["HS256"]
},
"policies": {
"pdftalk.ingest": {
"endpoint_id": "pdftalk.ingest",
"algorithm": "sliding_window",
"fail_mode": "fail_closed",
"fallback_rate_per_process_micro": 5000,
"policy_version": 1,
"limit": 1000,
"window_size_micro": 60000000
},
"resumint.tailor": {
"endpoint_id": "resumint.tailor",
"algorithm": "token_bucket",
"fail_mode": "fail_open",
"fallback_rate_per_process_micro": 2000,
"policy_version": 1,
"capacity_micro": 10000000,
"refill_rate_micro_per_sec": 10000
},
"auth.login": {
"endpoint_id": "auth.login",
"identity": "anonymous",
"algorithm": "token_bucket",
"fail_mode": "fail_open",
"fallback_rate_per_process_micro": 2000,
"policy_version": 1,
"capacity_micro": 1000000,
"refill_rate_micro_per_sec": 500000
}
}
}
app section¶
Deployment-level settings, validated by AppConfig:
| Field | What it controls | Default | Constraints |
|---|---|---|---|
redis_url |
URL of the dedicated Redis instance the guard connects to | — (required) | Must start with redis:// |
jwt_secret |
Shared HMAC secret used to verify incoming bearer tokens | — (required) | Minimum 32 characters |
jwt_algorithm_allowlist |
JWT algorithms accepted for verification | — (required) | Non-empty subset of HS256, HS384, HS512. Asymmetric keys and JWKS are rejected at load (deferred to V2) |
anonymous_cookie_secret |
HMAC secret that signs anonymous client cookies | — (required iff any policy has identity: "anonymous") |
Minimum 32 characters; rejected at load if an anonymous policy exists without it |
anonymous_cookie_name |
Cookie name carrying the anonymous client id | sentinel_anon_id |
Pattern ^[a-zA-Z0-9_.-]+$ |
anonymous_cookie_ttl_seconds |
Cookie lifetime before re-issuance | 2592000 (30 days) |
3600 ≤ value ≤ 7776000 |
anonymous_cookie_secure |
Secure flag on the anonymous cookie |
true |
Set false only for local HTTP development |
jwt_secret is treated as a secret value throughout; the algorithm allowlist exists because
the token's alg header alone is not trustworthy. anonymous_cookie_secret is a separate,
dedicated secret for the anonymous device cookie — never reuse jwt_secret for both (OWASP
device-cookie guidance: one key per token type).
policies section¶
A map of endpoint_id → Policy. Each policy is keyed by (and must declare) an explicit
endpoint_id — the id is never derived from the URL path (renaming a route does not create
a new bucket). Common fields:
| Field | What it controls | Default | Constraints |
|---|---|---|---|
endpoint_id |
The explicit configured id used by guard_for(...) and in the Redis key |
— (required) | Pattern ^[a-z0-9._-]+$ |
algorithm |
Which rate-limit algorithm to use | — (required) | token_bucket or sliding_window |
fail_mode |
Behavior when the Redis store fails | — (required) | fail_closed (503 on store failure) or fail_open (capped in-process emergency limiter). See Failure Semantics |
fallback_rate_per_process_micro |
Fail-open allowance per process, in µtokens/s | — (required) | ≥ 1. The emergency limiter's capacity and refill rate are both this value: a burst of one second's worth, then sustained. N instances can admit up to N × this rate |
policy_version |
Version stamp that is part of the Redis key | — (required) | ≥ 1. Bump deliberately when a policy changes — it names a new bucket |
identity |
Identity source for this endpoint's buckets | tenant_jwt |
tenant_jwt (default; bearer-token sub) or anonymous (signed client cookie + trusted-client IP; requires app.anonymous_cookie_secret) |
An identity: "anonymous" policy is wired with guard.anonymous_guard_for(endpoint_id)
instead of guard_for(...); using the wrong factory raises at route registration. Identity is
per-endpoint: tenant and anonymous policies can coexist in one config.
Anonymous identity (v1.2.0, Phase 19)¶
Unauthenticated endpoints key on two buckets, AND-combined (allowed only if both allow):
- Cookie bucket — a server-issued device id delivered as an
HttpOnly; SameSite=laxcookie, HMAC-signed withanonymous_cookie_secret. Tampered/expired cookies are treated as missing and re-minted on the next allowed request. A denied request never receives a cookie. - IP bucket —
request.client.hostas resolved by the ASGI server's trusted-proxy layer. Forwarding headers are never read. Missing/non-IP peers collapse to one shared bucket (over-blocking, never quota bypass).
Bucket keys live in the separate sentinel:v2:{sha256(identity)}:{endpoint_id}:{policy_version}
keyspace; raw client ids and IPs never reach Redis keys, logs, or metrics.
Token-bucket fields (algorithm: "token_bucket")¶
| Field | What it controls | Default | Constraints |
|---|---|---|---|
capacity_micro |
Burst size — the maximum tokens the bucket can hold | — (required) | ≥ 1,000,000 (one token = 1,000,000 µtokens); ≤ 2^30 (Lua integer exactness) |
refill_rate_micro_per_sec |
Sustained refill rate in µtokens per second | — (required) | ≥ 0; ≤ 2^30 (Lua integer exactness). 0 = a fixed-capacity bucket with no refill |
Sliding-window fields (algorithm: "sliding_window")¶
| Field | What it controls | Default | Constraints |
|---|---|---|---|
limit |
Maximum requests admitted per window | — (required) | ≥ 1; limit × window_size_micro ≤ 2^52 (Lua integer exactness) |
window_size_micro |
The window duration in microseconds | 60000000 (60 s) |
≥ 1,000 (1 ms) |
Per-algorithm rejection¶
The two algorithm families are mutually exclusive — mixing them is rejected at load:
sliding_windowrejectscapacity_microandrefill_rate_micro_per_sec.token_bucketrejectslimitandwindow_size_micro(explicitly settingwindow_size_microon a token-bucket policy is an error even when it matches the default).
Microtokens¶
All rate values are microtokens (1 token = 1,000,000 µtokens). Integer math only — no floats anywhere in the state. Examples:
| Value | Plain meaning |
|---|---|
capacity_micro: 10000000 |
10 tokens of burst capacity |
refill_rate_micro_per_sec: 10000 |
0.01 tokens per second |
fallback_rate_per_process_micro: 2000 |
emergency allowance of 0.002 tokens/s per process |
Validation rules (summary)¶
- Unknown keys anywhere are rejected (
extra="forbid"on every model). - Policy dict keys must match the policy's
endpoint_id, or loading fails. - Configuration arithmetic must stay within Lua's integer-exactness envelope (2^53; products bounded to 2^52) — configurations that would corrupt state are rejected, not silently accepted.
- The repository ships no dynamic configuration: policies are static, validated strictly at
load, and
policy_versionexists so policy changes ship as deliberate, auditable bumps.
Related¶
- Quick Start — minimal working configuration.
- Usage — how the config is loaded and used by the guard.
- Installation — step-by-step setup including the Redis
noevictionrequirement.