The ORLY relay includes a comprehensive policy system that allows fine-grained control over event storage and retrieval based on various criteria including event kinds, pubkeys, content, and custom script logic.
Enable the policy system by setting the environment variable:
export ORLY_POLICY_ENABLED=true
The policy configuration is loaded from $HOME/.config/ORLY/policy.json. See docs/example-policy.json for a complete example with global rules and age validation.
{
"default_policy": "allow",
"kind": {
"whitelist": [1, 3, 5, 7, 9735],
"blacklist": []
},
"global": {
"description": "Global rules applied to all events",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 100000,
"content_limit": 50000,
"max_age_of_event": 86400,
"max_age_event_in_future": 300
},
"rules": {
"1": {
"description": "Text notes - allow all authenticated users",
"write_allow": [],
"write_deny": [],
"read_allow": [],
"read_deny": [],
"size_limit": 32000,
"content_limit": 10000,
"max_age_of_event": 3600,
"max_age_event_in_future": 60
}
}
}
The default_policy field determines the default behavior when no specific rules deny an event:
"allow" (default): Events are allowed unless explicitly denied by rules"deny": Events are denied unless explicitly allowed by rulesThis applies to:
The policy system evaluates events in the following order:
The global section defines rules that apply to all events regardless of their kind. These rules are evaluated first and take precedence over kind-specific rules.
Global rules support all the same fields as kind-specific rules, allowing you to:
whitelist: If present, only these event kinds are allowed. All others are denied.blacklist: If present, these event kinds are denied. All others are allowed.description: Human-readable description of the rulescript: Path to a script for custom logic (overrides other criteria)write_allow: List of pubkeys allowed to write this kindwrite_deny: List of pubkeys denied from writing this kindread_allow: List of pubkeys allowed to read this kindread_deny: List of pubkeys denied from reading this kindmax_expiry: Maximum expiry time in seconds for eventsmust_have_tags: List of tag keys that must be presentsize_limit: Maximum total event size in bytescontent_limit: Maximum content field size in bytesprivileged: If true, event must be authored by authenticated user or contain authenticated user in p tagsrate_limit: Rate limit in bytes per second (not yet implemented)max_age_of_event: Maximum age of event in seconds (prevents replay attacks)max_age_event_in_future: Maximum time event can be in the future in seconds (prevents clock skew attacks)The policy system includes built-in timestamp validation to prevent common attacks:
created_at older than current_time - max_age_of_event are rejectedmax_age_of_event: 3600 rejects events older than 1 hour- Prevent replay of old events - Ensure events are recent and relevant - Reduce storage of stale data
created_at newer than current_time + max_age_event_in_future are rejectedmax_age_event_in_future: 300 rejects events more than 5 minutes in the future- Prevent clock manipulation attacks - Ensure reasonable timestamp accuracy - Block events with impossible future timestamps
{
"global": {
"max_age_of_event": 86400, // Reject events older than 24 hours
"max_age_event_in_future": 300 // Reject events more than 5 minutes in future
},
"rules": {
"1": {
"max_age_of_event": 3600, // Text notes: reject older than 1 hour
"max_age_event_in_future": 60 // Text notes: reject more than 1 minute in future
},
"4": {
"max_age_of_event": 604800 // Direct messages: reject older than 7 days
}
}
}
For advanced policy logic, you can use custom scripts. The script should be placed at $HOME/.config/ORLY/policy.sh and made executable.
The script receives JSON events via stdin and outputs JSON responses via stdout. Each event includes:
logged_in_pubkey: Hex-encoded authenticated user's pubkey (if any)ip_address: Client's IP address{"id": "event_id", "action": "accept|reject|shadowReject", "msg": "optional message"}
See docs/example-policy.sh for a complete example showing:
When policy is enabled, every EVENT envelope is checked using CheckPolicy("write", event, loggedInPubkey, ipAddress) before being stored. The policy evaluation follows this order:
When policy is enabled, every event returned in REQ responses is filtered using CheckPolicy("read", event, loggedInPubkey, ipAddress) before being sent to the client. The same evaluation order applies for read access.
The policy system is designed to be resilient to script failures:
When a policy script fails or is not running:
default_policydefault_policy settingPolicy decisions and script health are logged:
policy allowed event <id>policy rejected event <id>policy rule for kind <N> is inactive (script not running), falling back to default policy (<policy>)policy rule for kind <N> failed (script processing error: <error>), falling back to default policy (<policy>)policy rule for kind <N> returned unknown action '<action>', falling back to default policy (<policy>)policy script not found at <path>, will retry periodicallypolicy script crashed - events will fall back to default policy until restartpolicy filtered out event <id> for read access- Text notes (kind 1): 1-24 hours max age, 1-5 minutes future tolerance - Direct messages (kind 4): 7-30 days max age, 1-5 minutes future tolerance - Replaceable events (kind 0, 3): Longer max age, shorter future tolerance