Authorization Policies
Naylence provides a powerful policy-based authorization system that controls what actions nodes and messages can perform within the fabric. Authorization policies are defined in YAML/JSON files and evaluated at runtime using a first-match-wins approach.
Authorization policies complement admission control. While admission controls who can join the fabric, authorization policies control what they can do once connected.
Quick Start
Here’s a minimal authorization policy that allows all connections but restricts message delivery:
version: '1'
# Default effect when no rule matches
default_effect: deny
rules:
# Allow all nodes to connect
- id: allow-connect
action: Connect
effect: allow
# Allow messages to public addresses
- id: allow-public-addresses
address: public.**
effect: allow
# Allow messages from authenticated clients with proper scope
- id: allow-with-scope
address: api.**
scope: api.access
effect: allowPolicy Structure
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Schema version (currently "1") |
default_effect | "allow" | "deny" | No | Effect when no rule matches (default: "deny") |
rules | array | Yes | List of authorization rules |
type | string | No | Policy type: BasicAuthorizationPolicy or AdvancedAuthorizationPolicy |
Rule Fields
Each rule in the rules array can contain:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for debugging/audit |
description | string | Human-readable description |
effect | "allow" | "deny" | Required. What happens when rule matches |
action | string | string[] | Action type(s) to match |
address | string | string[] | Address pattern(s) using glob syntax |
origin_type | string | string[] | Message origin: downstream, upstream, peer, local |
scope | object | string | Scope requirement (see Scope Matching) |
frame_type | string | string[] | Frame type filter (Advanced only) |
when | string | Expression condition (Advanced only) |
Actions
Actions represent routing decisions in the fabric. They describe what will happen to an envelope:
| Action | Description |
|---|---|
Connect | Node connection handshake (NodeAttach) |
ForwardUpstream | Envelope will be forwarded to parent node |
ForwardDownstream | Envelope will be forwarded to a child route |
ForwardPeer | Envelope will be forwarded to a peer node |
DeliverLocal | Envelope will be delivered to a local address handler |
* | Matches all actions (wildcard) |
Actions are case-insensitive and support snake_case:
# All of these are equivalent
action: ForwardDownstream
action: forward_downstream
action: FORWARD_DOWNSTREAMYou can specify multiple actions as an array (implicit any-of):
action:
- ForwardUpstream
- ForwardPeerAddress Patterns
Address patterns use glob syntax to match envelope destination addresses:
Glob Syntax
| Pattern | Matches | Examples |
|---|---|---|
* | Single segment (stops at ., /, @) | api.* matches api.users but not api.users.list |
** | Any segments (crosses all separators) | api.** matches api.users and api.users.list.detail |
? | Single character (not a separator) | api.v? matches api.v1, api.v2 |
| Literal | Exact characters | api.users matches only api.users |
Multi-Separator Semantics: Glob patterns treat ., /, and @ as equivalent segment separators.
The * wildcard matches any characters except these separators, while ** matches across all separators.
This provides clean semantics for both logical and physical addresses.
Logical Address Patterns
Logical addresses follow the format name@domain.fabric:
# Match specific logical address
address: math@fame.fabric
# Match all names at fame.fabric domain
address: '*@fame.fabric'
# Match all addresses under api hierarchy
address: api.**
# Match v1 or v2 endpoints
address:
- api.v1.**
- api.v2.**
# Match any direct child of users
address: users.*
# Match all addresses at any subdomain of fabric
address: '*@*.fabric'
# Match names at any multi-level domain ending in .fabric
address: '*@**.fabric'Physical Address Patterns
Physical addresses follow the format name@/path/to/node:
# Match any name at any physical path
address: '*@/**'
# Match any name at root-level paths (e.g., math@/region)
address: '*@/*'
# Match any name at two-level paths (e.g., math@/region/us)
address: '*@/*/*'
# Match specific physical path
address: '*@/region/us/datacenter-1'
# Match all names under a specific path prefix
address: '*@/region/us/**'Special Patterns
# Match RPC reply addresses (no @ separator)
address: __rpc__**
# Match system messages
address: __sys__**
# Match all addresses (wildcard)
address: '**'Examples
# Match specific address
address: math@fame.fabric
# Match all addresses under api
address: api.**
# Match v1 or v2 endpoints
address:
- api.v1.**
- api.v2.**
# Match any direct child of users
address: users.*
# Match RPC reply addresses
address: __rpc__**
# Match system messages
address: __sys__**Basic policy only supports glob patterns. Patterns starting with ^ (regex)
are rejected and will cause a policy load error.
Origin Types
Filter rules based on where the message came from:
| Origin | Description |
|---|---|
downstream | Message came from a child node |
upstream | Message came from the parent node |
peer | Message came from a peer node |
local | Message originated locally on this node |
# Only allow forwarding from downstream clients
- id: client-messages
origin_type: downstream
action: ForwardDownstream
effect: allow
# Allow messages from any downstream or peer
- id: incoming-messages
origin_type:
- downstream
- peer
effect: allowScope Matching
Scopes provide fine-grained access control based on OAuth2/OIDC claims. Naylence extracts scopes from:
context.security.authorization.grantedScopes(explicit array)context.security.authorization.claims.scope(space-separated string)context.security.authorization.claims.scopes(array)context.security.authorization.claims.scp(Azure AD convention)
Simple Scope
# Require exact scope match
scope: api.readLogical Operators
Use any_of, all_of, and none_of for complex requirements:
# Require ANY of these scopes
scope:
any_of:
- admin
- api.write
# Require ALL of these scopes
scope:
all_of:
- api.read
- api.write
# Require NONE of these scopes (deny list)
scope:
none_of:
- banned
- suspended
# Nested operators
scope:
all_of:
- api.access
- any_of:
- api.read
- api.write
- none_of:
- temporaryGlob Patterns in Scopes
Scope patterns support glob syntax:
# Match any API scope
scope: api.*
# Match any admin scope at any depth
scope: admin.**Basic vs Advanced Policies
Naylence offers two policy implementations:
| Feature | Basic (OSS) | Advanced (BSL) |
|---|---|---|
| Action filtering | âś“ | âś“ |
| Address patterns (glob) | âś“ | âś“ |
| Origin type filtering | âś“ | âś“ |
| Scope matching | âś“ | âś“ |
| Frame type filtering | âś— | âś“ |
Expression conditions (when) | âś— | âś“ |
| Security posture checks | âś— | âś“ |
| Claims inspection | âś— | âś“ |
Using Basic Policy
version: '1'
# type: BasicAuthorizationPolicy # Optional, this is the default
rules:
- id: allow-connect
action: Connect
effect: allow
scope: node.connectUsing Advanced Policy
version: '1'
type: AdvancedAuthorizationPolicy
rules:
- id: require-encryption
frame_type: Data
effect: allow
when: is_encrypted() && claims.aud == "/" + node.idPolicy Evaluation
Authorization policies follow first-match-wins semantics:
- Rules are evaluated in order (top to bottom)
- The first matching rule determines the decision
- If no rule matches,
default_effectis applied (default:deny)
Evaluation Trace
Each decision includes an evaluation trace for debugging:
{
"effect": "allow",
"reason": "Matched rule: allow-api-access",
"matchedRule": "allow-api-access",
"evaluationTrace": [
{
"ruleId": "block-banned",
"result": false,
"expression": "scope: requirement not satisfied"
},
{
"ruleId": "allow-api-access",
"result": true,
"expression": "all conditions matched"
}
]
}Complete Examples
Public API with Rate Limiting Tiers
version: '1'
default_effect: deny
rules:
# Always allow connections
- id: allow-connect
action: Connect
effect: allow
# Premium users get full access
- id: premium-access
address: api.**
scope: tier.premium
effect: allow
# Basic users limited to public endpoints
- id: basic-access
address: api.public.**
scope: tier.basic
effect: allow
# Anonymous users can only access docs
- id: anonymous-docs
address: api.docs.**
effect: allowMulti-Tenant Isolation
version: '1'
default_effect: deny
rules:
- id: allow-connect
action: Connect
effect: allow
# Tenant A can only access their namespace
- id: tenant-a-access
address: tenants.a.**
scope: tenant.a
effect: allow
# Tenant B can only access their namespace
- id: tenant-b-access
address: tenants.b.**
scope: tenant.b
effect: allow
# Shared services accessible to all tenants
- id: shared-services
address: shared.**
scope:
any_of:
- tenant.a
- tenant.b
effect: allowInternal vs External Traffic
version: '1'
default_effect: deny
rules:
- id: allow-connect
action: Connect
effect: allow
# Local messages always allowed
- id: local-traffic
origin_type: local
effect: allow
# Peer messages allowed for specific addresses
- id: peer-sync
origin_type: peer
address: __sync__**
effect: allow
# Downstream needs authentication
- id: downstream-auth
origin_type: downstream
scope: client.authenticated
effect: allowConfiguration
Policy Sources
Naylence supports two built-in policy sources for loading authorization policies:
| Source | Profile | Package | Description |
|---|---|---|---|
| Local File | policy-localfile | @naylence/runtime | Loads policies from local YAML/JSON files |
| HTTP(S) | policy-http | @naylence/advanced-security | Loads policies from HTTP endpoints with caching |
Local File Source (policy-localfile)
The simplest way to enable authorization policies is by loading them from a local file:
# Enable policy-based authorization from local file
FAME_AUTHORIZATION_PROFILE=policy-localfileBy default, policy-localfile looks for auth-policy.yml in the current directory. You can specify a custom policy file location:
# Set custom policy file path
FAME_AUTH_POLICY_PATH=config/auth-policy.yamlComplete example:
# Security profile
FAME_SECURITY_PROFILE=gated
FAME_ADMISSION_PROFILE=direct
# Authorization policy
FAME_AUTHORIZATION_PROFILE=policy-localfile
FAME_AUTH_POLICY_PATH=config/auth-policy.yaml
# OAuth2 settings
FAME_ADMISSION_TOKEN_URL=https://oauth2-server/oauth/token
FAME_ADMISSION_CLIENT_ID=your-client-id
FAME_ADMISSION_CLIENT_SECRET=your-client-secretHTTP Policy Source (policy-http)
The HTTP policy source (policy-http) requires the BSL-licensed @naylence/advanced-security
package for TypeScript or naylence-advanced-security for Python.
For production deployments requiring centralized policy management, dynamic updates with ETag caching, and bearer authentication, see the HTTP Policy Source section in the Advanced Authorization Policies documentation.
Programmatic Usage
TypeScript
import {
BasicAuthorizationPolicy,
loadPolicyFromFile
} from '@naylence/runtime';
// Load from file
const policy = await loadPolicyFromFile('./auth-policy.yaml');
// Or construct programmatically
const policy = new BasicAuthorizationPolicy({
policyDefinition: {
version: '1',
default_effect: 'deny',
rules: [
{
id: 'allow-all',
effect: 'allow',
},
],
},
});Python
from naylence.runtime import BasicAuthorizationPolicy, load_policy_from_file
# Load from file
policy = load_policy_from_file('./auth-policy.yaml')
# Or construct programmatically
policy = BasicAuthorizationPolicy(
policy_definition={
'version': '1',
'default_effect': 'deny',
'rules': [
{
'id': 'allow-all',
'effect': 'allow',
},
],
}
)Best Practices
1. Use Explicit Deny-by-Default
default_effect: deny # Always explicit2. Order Rules from Specific to General
rules:
# Most specific rules first
- id: block-suspicious
address: api.admin.**
origin_type: downstream
scope:
none_of:
- admin.verified
effect: deny
# Then general allow rules
- id: allow-admin
address: api.admin.**
scope: admin
effect: allow
# Catch-all last (if needed)
- id: allow-public
address: api.public.**
effect: allow3. Use Descriptive Rule IDs
# Good
- id: deny-unauthenticated-api-access
- id: allow-premium-tier-full-access
# Bad
- id: rule1
- id: r24. Document Complex Rules
- id: tenant-isolation-check
description: |
Ensures tenants can only access their own namespace.
This is a critical security boundary - do not modify
without security review.
address: tenants.**
scope: tenant.*
effect: allow5. Test Policies Before Deployment
Use the evaluation trace to verify policy behavior:
const decision = await policy.evaluateRequest(node, envelope, context, action);
console.log('Decision:', decision.effect);
console.log('Matched rule:', decision.matchedRule);
console.log('Trace:', JSON.stringify(decision.evaluationTrace, null, 2));Next Steps
- Advanced Authorization Policies — Expression-based policies with security posture checks
- Gated Security — OAuth2/OIDC admission control
- Overlay Security — Envelope signing with public keys
Complete runnable examples are available in both naylence-examples-ts and naylence-examples-python
under the examples/security/ directory.