# ORLY Policy System Usage Guide The ORLY relay implements a comprehensive policy system that provides fine-grained control over event storage and retrieval. This guide explains how to configure and use the policy system to implement custom relay behavior. ## Overview The policy system allows relay operators to: - Control which events are stored and retrieved - Implement custom validation logic - Set size and age limits for events - Define access control based on pubkeys - Use scripts for complex policy rules - Filter events by content, kind, or other criteria ## Quick Start ### 1. Enable the Policy System Set the environment variable to enable policy checking: ```bash export ORLY_POLICY_ENABLED=true ``` ### 2. Create a Policy Configuration Create the policy file at `~/.config/ORLY/policy.json`: ```json { "default_policy": "allow", "global": { "max_age_of_event": 86400, "max_age_event_in_future": 300, "size_limit": 100000 }, "rules": { "1": { "description": "Text notes - basic validation", "max_age_of_event": 3600, "size_limit": 32000 } } } ``` ### 3. Restart the Relay ```bash # Restart your relay to load the policy sudo systemctl restart orly ``` ## Configuration Structure ### Top-Level Configuration ```json { "default_policy": "allow|deny", "kind": { "whitelist": ["1", "3", "4"], "blacklist": [] }, "global": { ... }, "rules": { ... } } ``` ### default_policy Determines the fallback behavior when no specific rules apply: - `"allow"`: Allow events unless explicitly denied (default) - `"deny"`: Deny events unless explicitly allowed ### kind Filtering Controls which event kinds are processed: ```json "kind": { "whitelist": ["1", "3", "4", "9735"], "blacklist": [] } ``` - `whitelist`: Only these kinds are allowed (if present) - `blacklist`: These kinds are denied (if present) - Empty arrays allow all kinds ### Global Rules Rules that apply to **all events** regardless of kind: ```json "global": { "description": "Site-wide security rules", "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, "privileged": false } ``` ### Kind-Specific Rules Rules that apply to specific event kinds: ```json "rules": { "1": { "description": "Text notes", "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, "privileged": false } } ``` ## Policy Fields ### Access Control #### write_allow / write_deny Control who can publish events: ```json { "write_allow": ["npub1allowed...", "npub1another..."], "write_deny": ["npub1blocked..."] } ``` - `write_allow`: Only these pubkeys can write (empty = allow all) - `write_deny`: These pubkeys cannot write #### read_allow / read_deny Control who can read events: ```json { "read_allow": ["npub1trusted..."], "read_deny": ["npub1suspicious..."] } ``` - `read_allow`: Only these pubkeys can read (empty = allow all) - `read_deny`: These pubkeys cannot read ### Size Limits #### size_limit Maximum total event size in bytes: ```json { "size_limit": 32000 } ``` Includes ID, pubkey, sig, tags, content, and metadata. #### content_limit Maximum content field size in bytes: ```json { "content_limit": 10000 } ``` Only applies to the `content` field. ### Age Validation #### max_age_of_event Maximum age of events in seconds (prevents replay attacks): ```json { "max_age_of_event": 3600 } ``` Events older than `current_time - max_age_of_event` are rejected. #### max_age_event_in_future Maximum time events can be in the future in seconds: ```json { "max_age_event_in_future": 300 } ``` Events with `created_at > current_time + max_age_event_in_future` are rejected. ### Advanced Options #### privileged Require events to be authored by authenticated users or contain authenticated users in p-tags: ```json { "privileged": true } ``` Useful for private content that should only be accessible to specific users. #### script Path to a custom script for complex validation logic: ```json { "script": "/path/to/custom-policy.sh" } ``` See the script section below for details. ## Policy Scripts For complex validation logic, use custom scripts that receive events via stdin and return decisions via stdout. ### Script Interface **Input**: JSON event objects, one per line: ```json { "id": "event_id", "pubkey": "author_pubkey", "kind": 1, "content": "Hello, world!", "tags": [["p", "recipient"]], "created_at": 1640995200, "sig": "signature" } ``` Additional fields provided: - `logged_in_pubkey`: Hex pubkey of authenticated user (if any) - `ip_address`: Client IP address **Output**: JSONL responses: ```json {"id": "event_id", "action": "accept", "msg": ""} {"id": "event_id", "action": "reject", "msg": "Blocked content"} {"id": "event_id", "action": "shadowReject", "msg": ""} ``` ### Actions - `accept`: Store/retrieve the event normally - `reject`: Reject with OK=false and message - `shadowReject`: Accept with OK=true but don't store (useful for spam filtering) ### Example Scripts #### Bash Script ```bash #!/bin/bash while read -r line; do if [[ -n "$line" ]]; then event_id=$(echo "$line" | jq -r '.id') # Check for spam content if echo "$line" | jq -r '.content' | grep -qi "spam"; then echo "{\"id\":\"$event_id\",\"action\":\"reject\",\"msg\":\"Spam detected\"}" else echo "{\"id\":\"$event_id\",\"action\":\"accept\",\"msg\":\"\"}" fi fi done ``` #### Python Script ```python #!/usr/bin/env python3 import json import sys def process_event(event): event_id = event.get('id', '') content = event.get('content', '') pubkey = event.get('pubkey', '') logged_in = event.get('logged_in_pubkey', '') # Block spam if 'spam' in content.lower(): return { 'id': event_id, 'action': 'reject', 'msg': 'Content contains spam' } # Require authentication for certain content if 'private' in content.lower() and not logged_in: return { 'id': event_id, 'action': 'reject', 'msg': 'Authentication required' } return { 'id': event_id, 'action': 'accept', 'msg': '' } for line in sys.stdin: if line.strip(): try: event = json.loads(line) response = process_event(event) print(json.dumps(response)) sys.stdout.flush() except json.JSONDecodeError: continue ``` ### Script Configuration Place scripts in a secure location and reference them in policy: ```json { "rules": { "1": { "script": "/etc/orly/policy/text-note-policy.py", "description": "Custom validation for text notes" } } } ``` Ensure scripts are executable and have appropriate permissions. ### Script Requirements and Best Practices #### Critical Requirements **1. Output Only JSON to stdout** Scripts MUST write ONLY JSON responses to stdout. Any other output (debug messages, logs, etc.) will break the JSONL protocol and cause errors. ```javascript // ❌ WRONG - This will cause "broken pipe" errors console.log("Policy script starting..."); // This goes to stdout! console.log(JSON.stringify(response)); // Correct // ✅ CORRECT - Use stderr or file for debug output console.error("Policy script starting..."); // This goes to stderr (OK) fs.appendFileSync('/tmp/policy.log', 'Starting...\n'); // This goes to file (OK) console.log(JSON.stringify(response)); // Stdout for JSON only ``` **2. Flush stdout After Each Response** Always flush stdout after writing a response to ensure immediate delivery: ```python # Python print(json.dumps(response)) sys.stdout.flush() # Critical! ``` ```javascript // Node.js (usually automatic, but can be forced) process.stdout.write(JSON.stringify(response) + '\n'); ``` **3. Run as a Long-Lived Process** Scripts should run continuously, reading from stdin in a loop. They should NOT: - Exit after processing one event - Use batch processing - Close stdin/stdout prematurely ```javascript // ✅ CORRECT - Long-lived process const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); rl.on('line', (line) => { const event = JSON.parse(line); const response = processEvent(event); console.log(JSON.stringify(response)); }); ``` **4. Handle Errors Gracefully** Always catch errors and return a valid JSON response: ```javascript rl.on('line', (line) => { try { const event = JSON.parse(line); const response = processEvent(event); console.log(JSON.stringify(response)); } catch (err) { // Log to stderr or file, not stdout! console.error(`Error: ${err.message}`); // Return reject response console.log(JSON.stringify({ id: '', action: 'reject', msg: 'Policy script error' })); } }); ``` **5. Response Format** Every response MUST include these fields: ```json { "id": "event_id", // Must match input event ID "action": "accept", // Must be: accept, reject, or shadowReject "msg": "" // Required (can be empty string) } ``` #### Common Issues and Solutions **Broken Pipe Error** ``` ERROR: policy script /path/to/script.js stdin closed (broken pipe) ``` **Causes:** - Script exited prematurely - Script wrote non-JSON output to stdout - Script crashed or encountered an error - Script closed stdin/stdout incorrectly **Solutions:** 1. Remove ALL `console.log()` statements except JSON responses 2. Use `console.error()` or log files for debugging 3. Add error handling to catch and log exceptions 4. Ensure script runs continuously (doesn't exit) **Response Timeout** ``` WARN: policy script /path/to/script.js response timeout - script may not be responding correctly ``` **Causes:** - Script not flushing stdout - Script processing taking > 5 seconds - Script not responding to input - Non-JSON output consuming a response slot **Solutions:** 1. Add `sys.stdout.flush()` (Python) after each response 2. Optimize processing logic to be faster 3. Check that script is reading from stdin correctly 4. Remove debug output from stdout **Invalid JSON Response** ``` ERROR: failed to parse policy response from /path/to/script.js WARN: policy script produced non-JSON output on stdout: "Debug message" ``` **Solutions:** 1. Validate JSON before outputting 2. Use a JSON library, don't build strings manually 3. Move debug output to stderr or files #### Testing Your Script Before deploying, test your script: ```bash # 1. Test basic functionality echo '{"id":"test123","pubkey":"abc","kind":1,"content":"test","tags":[],"created_at":1234567890,"sig":"def"}' | node policy-script.js # 2. Check for non-JSON output echo '{"id":"test123","pubkey":"abc","kind":1,"content":"test","tags":[],"created_at":1234567890,"sig":"def"}' | node policy-script.js 2>/dev/null | jq . # 3. Test error handling echo 'invalid json' | node policy-script.js ``` Expected output (valid JSON only): ```json {"id":"test123","action":"accept","msg":""} ``` #### Node.js Example (Complete) ```javascript #!/usr/bin/env node const fs = require('fs'); const readline = require('readline'); // Use stderr or file for debug logging const logFile = '/tmp/policy-debug.log'; function debug(msg) { fs.appendFileSync(logFile, `${Date.now()}: ${msg}\n`); } // Create readline interface const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); debug('Policy script started'); // Process each event rl.on('line', (line) => { try { const event = JSON.parse(line); debug(`Processing event ${event.id}`); // Your policy logic here const action = shouldAccept(event) ? 'accept' : 'reject'; // ONLY JSON to stdout console.log(JSON.stringify({ id: event.id, action: action, msg: action === 'reject' ? 'Policy rejected' : '' })); } catch (err) { debug(`Error: ${err.message}`); // Still return valid JSON console.log(JSON.stringify({ id: '', action: 'reject', msg: 'Policy script error' })); } }); rl.on('close', () => { debug('Policy script stopped'); }); function shouldAccept(event) { // Your policy logic return !event.content.toLowerCase().includes('spam'); } ``` #### Event Fields Scripts receive additional context fields: ```json { "id": "event_id", "pubkey": "author_pubkey", "kind": 1, "content": "Event content", "tags": [], "created_at": 1234567890, "sig": "signature", "logged_in_pubkey": "authenticated_user_pubkey", "ip_address": "127.0.0.1", "access_type": "read" } ``` **access_type values:** - `"write"`: Event is being stored (EVENT message) - `"read"`: Event is being retrieved (REQ message) Use this to implement different policies for reads vs writes. ## Policy Evaluation Order Events are evaluated in this order: 1. **Global Rules** - Applied first to all events 2. **Kind Filtering** - Whitelist/blacklist check 3. **Kind-specific Rules** - Rules for the event's kind 4. **Script Rules** - Custom script logic (if configured) 5. **Default Policy** - Fallback behavior The first rule that makes a decision (allow/deny) stops evaluation. ## Event Processing Integration ### Write Operations (EVENT) When `ORLY_POLICY_ENABLED=true`, each incoming EVENT is checked: ```go // Pseudo-code for policy integration func handleEvent(event *Event, client *Client) { decision := policy.CheckPolicy("write", event, client.Pubkey, client.IP) if decision.Action == "reject" { client.SendOK(event.ID, false, decision.Message) return } if decision.Action == "shadowReject" { client.SendOK(event.ID, true, "") return } // Store event storeEvent(event) client.SendOK(event.ID, true, "") } ``` ### Read Operations (REQ) Events returned in REQ responses are filtered: ```go func handleReq(filter *Filter, client *Client) { events := queryEvents(filter) filteredEvents := []Event{} for _, event := range events { decision := policy.CheckPolicy("read", &event, client.Pubkey, client.IP) if decision.Action != "reject" { filteredEvents = append(filteredEvents, event) } } sendEvents(client, filteredEvents) } ``` ## Common Use Cases ### Basic Spam Filtering ```json { "global": { "max_age_of_event": 86400, "size_limit": 100000 }, "rules": { "1": { "script": "/etc/orly/scripts/spam-filter.sh", "max_age_of_event": 3600, "size_limit": 32000 } } } ``` ### Private Relay ```json { "default_policy": "deny", "global": { "write_allow": ["npub1trusted1...", "npub1trusted2..."], "read_allow": ["npub1trusted1...", "npub1trusted2..."] } } ``` ### Content Moderation ```json { "rules": { "1": { "script": "/etc/orly/scripts/content-moderation.py", "description": "AI-powered content moderation" } } } ``` ### Rate Limiting ```json { "global": { "script": "/etc/orly/scripts/rate-limiter.sh" } } ``` ### Follows-Based Access Combined with ACL system: ```bash export ORLY_ACL_MODE=follows export ORLY_ADMINS=npub1admin1...,npub1admin2... export ORLY_POLICY_ENABLED=true ``` ## Monitoring and Debugging ### Log Messages Policy decisions are logged: ``` policy allowed event policy rejected event : reason policy filtered out event for read access ``` ### Script Health Script failures are logged: ``` policy rule for kind is inactive (script not running), falling back to default policy (allow) policy rule for kind failed (script processing error: timeout), falling back to default policy (allow) ``` ### Testing Policies Use the policy test tools: ```bash # Test policy with sample events ./scripts/run-policy-test.sh # Test policy filter integration ./scripts/run-policy-filter-test.sh ``` ### Debugging Scripts Test scripts independently: ```bash # Test script with sample event echo '{"id":"test","kind":1,"content":"test message"}' | ./policy-script.sh # Expected output: # {"id":"test","action":"accept","msg":""} ``` ## Performance Considerations ### Script Performance - Scripts run synchronously and can block event processing - Keep script logic efficient (< 100ms per event) - Consider using `shadowReject` for non-blocking filtering - Scripts should handle malformed input gracefully ### Memory Usage - Policy configuration is loaded once at startup - Scripts are kept running for performance - Large configurations may impact startup time ### Scaling - For high-throughput relays, prefer built-in policy rules over scripts - Use script timeouts to prevent hanging - Monitor script performance and resource usage ## Security Considerations ### Script Security - Scripts run with relay process privileges - Validate all inputs in scripts - Use secure file permissions for policy files - Regularly audit custom scripts ### Access Control - Test policy rules thoroughly before production use - Use `privileged: true` for sensitive content - Combine with authentication requirements - Log policy violations for monitoring ### Data Validation - Age validation prevents replay attacks - Size limits prevent DoS attacks - Content validation prevents malicious payloads ## Troubleshooting ### Policy Not Loading Check file permissions and path: ```bash ls -la ~/.config/ORLY/policy.json cat ~/.config/ORLY/policy.json ``` ### Scripts Not Working Verify script is executable and working: ```bash ls -la /path/to/script.sh ./path/to/script.sh < /dev/null ``` ### Unexpected Behavior Enable debug logging: ```bash export ORLY_LOG_LEVEL=debug ``` Check logs for policy decisions and errors. ### Common Issues 1. **Script timeouts**: Increase script timeouts or optimize script performance 2. **Memory issues**: Reduce script memory usage or use built-in rules 3. **Permission errors**: Fix file permissions on policy files and scripts 4. **Configuration errors**: Validate JSON syntax and field names ## Advanced Configuration ### Multiple Policies Use different policies for different relay instances: ```bash # Production relay export ORLY_APP_NAME=production # Policy at ~/.config/production/policy.json # Staging relay export ORLY_APP_NAME=staging # Policy at ~/.config/staging/policy.json ``` ### Dynamic Policies Policies can be updated without restart by modifying the JSON file. Changes take effect immediately for new events. ### Integration with External Systems Scripts can integrate with external services: ```python import requests def check_external_service(content): response = requests.post('http://moderation-service:8080/check', json={'content': content}, timeout=5) return response.json().get('approved', False) ``` ## Examples Repository See the `docs/` directory for complete examples: - `example-policy.json`: Complete policy configuration - `example-policy.sh`: Sample policy script - Various test scripts in `scripts/` ## Support For issues with policy configuration: 1. Check the logs for error messages 2. Validate your JSON configuration 3. Test scripts independently 4. Review the examples in `docs/` 5. Check file permissions and paths ## Migration from Other Systems ### From Simple Filtering Replace simple filters with policy rules: ```json // Before: Simple size limit // After: Policy-based size limit { "global": { "size_limit": 50000 } } ``` ### From Custom Code Migrate custom validation logic to policy scripts: ```json { "rules": { "1": { "script": "/etc/orly/scripts/custom-validation.py" } } } ``` The policy system provides a flexible, maintainable way to implement complex relay behavior while maintaining performance and security.