Frontline evaluates middleware policies before proxying traffic to deployment instances. Policies are stored in the deployment’s sentinel_config column as a JSON-serialized frontline.v1.Config protobuf.
Evaluation flow
Evaluation model
Policies are evaluated in declaration order. For each policy:
- Skip if
enabled is false.
- Evaluate all match expressions. All expressions must match (AND semantics). If any expression does not match, the policy is skipped.
- Execute the policy. If it rejects the request (invalid key, rate limited, insufficient permissions), evaluation stops and Frontline returns a structured error response.
- If the policy is an auth policy and succeeds, it sets the
Principal for the request. Subsequent auth policies are skipped.
Unknown policy types are silently skipped for forward compatibility. This means a Frontline instance running an older version can load a config that references a new policy type without breaking.
Composability
Policies are composable building blocks. The policy list is not a set of independent checks but an ordered pipeline where earlier policies can establish context that later policies consume.
Auth sets context for downstream policies
Auth policies produce a Principal containing a subject and a method-specific source object. Today only KeyAuth is implemented; JWTAuth is defined in the protobuf schema but not yet executed by the engine (see implementation status). Downstream policies reference this principal. For example, a RateLimit policy can use the authenticated subject as its bucket key, giving each user their own rate limit window instead of a shared one.
Policy 1: KeyAuth → authenticates the request, sets Principal
Policy 2: RateLimit → rate limits per Principal.subject (not yet implemented)
The examples in this section illustrate the composability model. Only KeyAuth executes today; other policy types are shown to demonstrate the design.
Same policy type, different match expressions
The same policy type can appear multiple times with different match expressions. This enables path-specific or method-specific rules without requiring a single policy to handle every case.
For example, applying different rate limits to different paths:
Policy 1: RateLimit match: /v1/expensive/* → 10 req/min
Policy 2: RateLimit match: /v1/* → 1000 req/min
Policies are evaluated in order and all matching policies execute until one rejects. A request to /v1/expensive/foo matches both policies, so both rate limits apply independently. If the stricter limit rejects, evaluation stops and the broader policy never runs. Place more specific match expressions before broader ones so a rejection skips unnecessary work.
Layering multiple concerns
A typical production configuration layers firewall, auth, rate limiting, and validation:
Policy 1: Firewall match: path prefix /admin → DENY admin routes from public traffic
Policy 2: KeyAuth match: all → authenticate, set Principal
Policy 3: RateLimit match: /v1/* → 1000 req/min per subject
Policy 4: RateLimit match: /v1/expensive/* → 10 req/min per subject
Policy 5: OpenAPI match: all → validate request shape
Each policy can reject the request independently. A blocked path never reaches auth. An invalid key never reaches rate limiting. A rate-limited request never reaches schema validation. Order matters.
Full config example
The sentinel_config column on a deployment stores a JSON-serialized frontline.v1.Config protobuf. Here is a complete example that blocks /admin, authenticates with KeyAuth, and applies two rate limit tiers:
{
"policies": [
{
"id": "block-admin",
"name": "Block /admin",
"enabled": true,
"match": [
{ "path": { "path": { "prefix": "/admin" } } }
],
"firewall": { "action": "ACTION_DENY" }
},
{
"id": "api-auth",
"name": "Authenticate API keys",
"enabled": true,
"match": [],
"keyauth": {
"key_space_ids": ["ks_abc123"],
"locations": [
{ "bearer": {} }
],
"permission_query": "api.read"
}
},
{
"id": "search-ratelimit",
"name": "Strict limit on search",
"enabled": true,
"match": [
{ "path": { "path": { "prefix": "/v1/search" } } },
{ "method": { "methods": ["GET"] } }
],
"ratelimit": {
"limit": 10,
"window_ms": 60000,
"key": { "authenticated_subject": {} }
}
},
{
"id": "global-ratelimit",
"name": "Default rate limit",
"enabled": true,
"match": [
{ "path": { "path": { "prefix": "/v1/" } } }
],
"ratelimit": {
"limit": 1000,
"window_ms": 60000,
"key": { "authenticated_subject": {} }
}
}
]
}
What happens for GET /v1/search?q=test with a valid API key:
block-admin match does not apply (path is /v1/search, not /admin), so the policy is skipped.
api-auth runs. Extracts the Bearer token, verifies it against keyspace ks_abc123, checks the api.read permission, and sets the Principal.
search-ratelimit runs. Both match expressions pass (path starts with /v1/search AND method is GET). Rate limits the request at 10/min using the authenticated subject as the bucket key.
global-ratelimit match also passes (path starts with /v1/), but the request was already rate-limited by policy 3. Both policies evaluate independently.
What happens for POST /v1/keys with an invalid key:
block-admin is skipped (path does not match).
api-auth rejects the request with 401. Evaluation stops. Policies 3 and 4 never run.
Implementation status
The engine executes only KeyAuth. Other policy types are defined in the protobuf schema and can be configured, but the engine skips them at evaluation time.
| Policy | Implemented |
|---|
| KeyAuth | Yes |
| Firewall | Yes |
| JWTAuth | Schema only |
| RateLimit | Schema only |
| OpenAPI validation | Schema only |
Shared types
Config parsing
The engine handles these edge cases when parsing sentinel_config:
| Input | Behavior |
|---|
nil or empty bytes | Pass-through (no policies) |
{} (empty JSON object) | Pass-through (legacy compatibility) |
| Valid JSON with policies | Parse and evaluate |
| Invalid JSON | Error with code Frontline.Internal.InvalidConfiguration |
Adding a new policy type
- Define the policy proto in
svc/frontline/proto/frontline/policies/v1/.
- Add the new type to the
config oneof in policy.proto.
- Add a case in the evaluation switch in
svc/frontline/internal/policies/engine.go.
- Implement the executor (follow
keyauth.go as a reference).
- Run
mise run generate to regenerate protobuf code.
Old Frontline versions that do not recognize the new policy type skip it silently, so rollouts are safe.