Skip to Content
Naylence Docs are in active development. Share feedback in Discord.
SecurityAuthorization Policies

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:

auth-policy.yaml
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: allow

Policy Structure

Top-Level Fields

FieldTypeRequiredDescription
versionstringYesSchema version (currently "1")
default_effect"allow" | "deny"NoEffect when no rule matches (default: "deny")
rulesarrayYesList of authorization rules
typestringNoPolicy type: BasicAuthorizationPolicy or AdvancedAuthorizationPolicy

Rule Fields

Each rule in the rules array can contain:

FieldTypeDescription
idstringUnique identifier for debugging/audit
descriptionstringHuman-readable description
effect"allow" | "deny"Required. What happens when rule matches
actionstring | string[]Action type(s) to match
addressstring | string[]Address pattern(s) using glob syntax
origin_typestring | string[]Message origin: downstream, upstream, peer, local
scopeobject | stringScope requirement (see Scope Matching)
frame_typestring | string[]Frame type filter (Advanced only)
whenstringExpression condition (Advanced only)

Actions

Actions represent routing decisions in the fabric. They describe what will happen to an envelope:

ActionDescription
ConnectNode connection handshake (NodeAttach)
ForwardUpstreamEnvelope will be forwarded to parent node
ForwardDownstreamEnvelope will be forwarded to a child route
ForwardPeerEnvelope will be forwarded to a peer node
DeliverLocalEnvelope 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_DOWNSTREAM

You can specify multiple actions as an array (implicit any-of):

action: - ForwardUpstream - ForwardPeer

Address Patterns

Address patterns use glob syntax to match envelope destination addresses:

Glob Syntax

PatternMatchesExamples
*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
LiteralExact charactersapi.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:

OriginDescription
downstreamMessage came from a child node
upstreamMessage came from the parent node
peerMessage came from a peer node
localMessage 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: allow

Scope Matching

Scopes provide fine-grained access control based on OAuth2/OIDC claims. Naylence extracts scopes from:

  1. context.security.authorization.grantedScopes (explicit array)
  2. context.security.authorization.claims.scope (space-separated string)
  3. context.security.authorization.claims.scopes (array)
  4. context.security.authorization.claims.scp (Azure AD convention)

Simple Scope

# Require exact scope match scope: api.read

Logical 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: - temporary

Glob 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:

FeatureBasic (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

auth-policy.yaml
version: '1' # type: BasicAuthorizationPolicy # Optional, this is the default rules: - id: allow-connect action: Connect effect: allow scope: node.connect

Using Advanced Policy

auth-policy.yaml
version: '1' type: AdvancedAuthorizationPolicy rules: - id: require-encryption frame_type: Data effect: allow when: is_encrypted() && claims.aud == "/" + node.id

Policy Evaluation

Authorization policies follow first-match-wins semantics:

  1. Rules are evaluated in order (top to bottom)
  2. The first matching rule determines the decision
  3. If no rule matches, default_effect is 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: allow

Multi-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: allow

Internal 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: allow

Configuration

Policy Sources

Naylence supports two built-in policy sources for loading authorization policies:

SourceProfilePackageDescription
Local Filepolicy-localfile@naylence/runtimeLoads policies from local YAML/JSON files
HTTP(S)policy-http@naylence/advanced-securityLoads 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-localfile

By 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.yaml

Complete example:

.env
# 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-secret

HTTP 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 explicit

2. 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: allow

3. Use Descriptive Rule IDs

# Good - id: deny-unauthenticated-api-access - id: allow-premium-tier-full-access # Bad - id: rule1 - id: r2

4. 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: allow

5. 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

Complete runnable examples are available in both naylence-examples-ts and naylence-examples-python under the examples/security/ directory.

Last updated on