ACL Management
The Ominis Cluster Manager uses Access Control Lists (ACLs) to manage IP-based authentication for FreeSWITCH registration. The ACL system provides fine-grained control over which IP addresses can register without password authentication (cluster IPs) versus which must authenticate via the database (external IPs).
Overview
The ACL (Access Control List) management system provides database-backed IP whitelisting with real-time updates. This enables secure differentiation between trusted cluster IPs and external SIP clients requiring authentication.
Key Features
- Database-backed: All ACL entries stored in PostgreSQL for persistence
- Real-time updates: Changes via
mod_xml_curltake effect immediately - CRUD API: Full lifecycle management at
/v1/acl - Cluster-aware: Automatic trust for internal Kubernetes networks
- Token-secured: External IP queries require authentication token
- Live reload:
reloadaclcommand triggers instant database query
Architecture
The ACL system uses a three-tier architecture: API management, database storage, and FreeSWITCH enforcement.
System Components
ACL Check Flow
The following sequence diagram shows how ACL checks are performed during SIP registration:
Network Security Model
The Ominis Cluster Manager implements a two-tier security model based on network segmentation:
Network Segmentation
| Network Type | CIDR Range | Authentication | Use Case |
|---|---|---|---|
| Cluster Pods | 10.42.0.0/16 | Blind registration (no password) | Queue pods, IVR pods, registrar |
| Cluster Services | 10.43.0.0/16 | Blind registration (no password) | Service ClusterIPs |
| Localhost | 127.0.0.1/32 | Blind registration (no password) | Local testing |
| External IPs | All others | Database authentication required | Remote SIP clients, softphones |
Security Boundaries
ACL Enforcement Points
The ACL system enforces access control at multiple layers:
-
SIP Registration (FreeSWITCH
apply-inbound-acl)- Cluster IPs: Trusted, no password required
- External IPs: 401 challenge, database validation
-
XML-CURL API (
/v1/freeswitch/acl,/v1/freeswitch/directory)- Private IPs (RFC 1918): Token validation skipped
- Public IPs:
X-FreeSWITCH-Tokenheader required
-
Management API (
/v1/acl/*)- All requests:
X-API-Keyheader required - No IP-based exceptions
- All requests:
ADR: Database-Backed ACL System
Status: Accepted
Date: 2025-01-15
Decision Makers: Engineering Team
Context
The initial ACL implementation used Kubernetes ConfigMaps with API-driven updates. This approach had several limitations:
- ConfigMap propagation delays: Changes could take 30-60 seconds to reach pods
- Helm overwrite issues:
helm upgradewould reset ConfigMap to template state - No persistence: Runtime API changes lost on deployment
- Inconsistent architecture: Extensions used database, ACLs used ConfigMaps
The system needed a single source of truth that persisted across all deployments and provided real-time updates.
Decision
Migrate ACL storage from Kubernetes ConfigMaps to PostgreSQL with mod_xml_curl queries.
Implementation:
- Create
acl_entriestable in PostgreSQL - Add
configurationbinding to mod_xml_curl - Implement
/v1/freeswitch/aclendpoint returning XML - Modify management API to trigger
reloadaclafter changes - Remove static
acl.conf.xmlfrom ConfigMap
Alternatives Considered
Alternative 1: ConfigMap with GitOps
Description: Keep ConfigMap approach but enforce all changes via Git commits and Helm
Pros:
- Infrastructure as code
- Full audit trail via Git history
- No database dependency for ACLs
Cons:
- Slow (git commit → CI/CD → Helm upgrade)
- No runtime changes without deployment
- Inconsistent with extension management
- Requires Helm redeploy for every ACL change
Why rejected: Too slow for operational needs. Adding a temporary IP for debugging would require a full CI/CD cycle.
Alternative 2: Redis for ACL Cache
Description: Use Redis as fast cache layer between API and FreeSWITCH
Pros:
- Sub-millisecond reads
- Native key expiration support
- Pub/sub for real-time updates
Cons:
- Additional infrastructure dependency
- Another service to monitor/backup
- Redis failure = registration failure
- Over-engineering for simple use case
Why rejected: PostgreSQL query performance (5-20ms) is sufficient. Adding Redis complexity not justified.
Consequences
Positive
- ✅ Real-time updates: Database queries complete in < 100ms
- ✅ Persistence: ACL entries survive pod restarts, rollouts, Helm upgrades
- ✅ Consistent architecture: Matches directory, callcenter, extensions patterns
- ✅ Auditability: Timestamps on every entry (created_at, updated_at)
- ✅ No file operations: Pure database queries, no kubectl exec or YAML parsing
Negative
- ⚠️ Database dependency: FreeSWITCH ACL loading requires database connectivity
- Mitigation: Database is already required for extensions and queues
- ⚠️ mod_xml_curl overhead: Extra HTTP call on FreeSWITCH startup
- Mitigation: Cached in FreeSWITCH memory after first load, only requeried on
reloadacl
- Mitigation: Cached in FreeSWITCH memory after first load, only requeried on
Neutral
- ℹ️ Migration required: Existing ConfigMap entries must be seeded into database
- Handled: Automatic seeding of cluster IPs on first deployment
Implementation Example
Database Schema:
CREATE TABLE IF NOT EXISTS acl_entries (
id SERIAL PRIMARY KEY,
list_name VARCHAR(64) NOT NULL DEFAULT 'trusted_users',
cidr VARCHAR(128) NOT NULL,
permission VARCHAR(10) NOT NULL DEFAULT 'allow',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(list_name, cidr)
);
mod_xml_curl Configuration:
<binding name="configuration">
<param name="gateway-url" value="http://api:8000/v1/freeswitch/acl"/>
<param name="gateway-credentials" value="X-FreeSWITCH-Token: ${XMLCURL_TOKEN}"/>
</binding>
API Response:
<document type="freeswitch/xml">
<section name="configuration">
<configuration name="acl.conf">
<network-lists>
<list name="trusted_users" default="deny">
<node type="allow" cidr="10.42.0.0/16"/>
<node type="allow" cidr="10.43.0.0/16"/>
<node type="allow" cidr="127.0.0.1/32"/>
</list>
</network-lists>
</configuration>
</section>
</document>
References
- Implementation:
routers/acl.py - Database helpers:
adapters/database.py - Schema:
charts/tenant-infra/init-schema.sql - Related: Directory XML-CURL Integration
ADR: apply-inbound-acl vs apply-register-acl
Status: Accepted
Date: 2025-01-20
Decision Makers: Engineering Team
Context
FreeSWITCH provides multiple ACL-related Sofia profile parameters:
apply-register-acl- ACL for REGISTER messagesapply-inbound-acl- ACL for all inbound trafficaccept-blind-reg- Global blind registration toggleauth-calls- Require authentication for calls
Initial implementation used apply-register-acl="trusted_users" with accept-blind-reg="true". This configuration caused external IPs to receive 403 Forbidden responses instead of 401 authentication challenges.
The Problem
With apply-register-acl:
- Cluster IP (10.42.0.5) sends REGISTER → ✅ Success (in ACL)
- External IP (203.0.113.50) sends REGISTER → ❌ 403 Forbidden (not in ACL)
- FreeSWITCH never sends 401 challenge
mod_xml_curlnever called for directory lookup- External clients cannot register even with valid credentials
Root Cause: apply-register-acl is a hard whitelist that blocks non-ACL IPs entirely.
Decision
Use apply-inbound-acl instead of apply-register-acl to allow external IP authentication.
Configuration:
<param name="apply-inbound-acl" value="trusted_users"/>
<param name="accept-blind-reg" value="false"/>
<param name="auth-calls" value="true"/>
<!-- DO NOT use apply-register-acl -->
Behavior:
- Cluster IPs (in
trusted_usersACL): Blind registration (no password) - External IPs (not in ACL): 401 challenge →
mod_xml_curl→ database authentication - Invalid credentials: 403 Forbidden
Alternatives Considered
Alternative 1: Keep apply-register-acl, Add All IPs to ACL
Description: Add external IPs to trusted_users ACL list
Pros:
- Simpler FreeSWITCH configuration
- No authentication challenges
Cons:
- Security hole: Any IP in ACL can register without password
- Defeats purpose of authentication
- Cannot distinguish between cluster and external IPs
Why rejected: Unacceptable security risk. External IPs must always authenticate.
Alternative 2: Use accept-blind-reg=true Without ACL
Description: Enable blind registration globally, rely only on IP whitelist
Pros:
- Simplest configuration
- No authentication overhead
Cons:
- Completely insecure: Anyone can register from any IP
- No way to validate credentials
- Cannot use database-backed extensions
Why rejected: Production-unacceptable security posture.
Consequences
Positive
- ✅ External clients can authenticate: 401 challenge allows password validation
- ✅ Cluster IPs still trusted: No password required for internal pods
- ✅ Proper security model: Cluster = trust, external = verify
- ✅ Works with mod_xml_curl: Directory lookups triggered correctly
Negative
- ⚠️ Documentation required: Non-obvious FreeSWITCH behavior needs explanation
- Mitigation: This ADR and troubleshooting guide
Neutral
- ℹ️ FreeSWITCH quirk:
apply-register-aclname suggests it should work but doesn't for our use case
Implementation
Sofia Profile Configuration (internal.xml):
<profile name="internal">
<settings>
<!-- Correct ACL parameter -->
<param name="apply-inbound-acl" value="trusted_users"/>
<!-- Disable blind registration globally -->
<param name="accept-blind-reg" value="false"/>
<!-- Require authentication -->
<param name="auth-calls" value="true"/>
<param name="challenge-realm" value="auto_from"/>
<!-- DO NOT USE apply-register-acl -->
<!-- <param name="apply-register-acl" value="trusted_users"/> ❌ -->
</settings>
</profile>
Testing Validation:
# Test 1: External IP (should get 401, then authenticate)
# From public IP (e.g., home office)
sip_client register --user 1001 --password secret --server registrar.ominis.ai
# Expected: 401 Challenge → Credentials sent → 200 OK
# Test 2: Cluster IP (should get 200 immediately)
# From inside Kubernetes cluster
kubectl exec -it queue-pod -- fs_cli -x "sofia profile internal register"
# Expected: 200 OK (no password required)
# Test 3: Invalid credentials (should get 403)
sip_client register --user 1001 --password wrong --server registrar.ominis.ai
# Expected: 401 Challenge → Invalid credentials → 403 Forbidden
References
- Sofia configuration:
freeswitch-registrar/conf/sip_profiles/internal.xml - FreeSWITCH ACL docs: FreeSWITCH ACL
- Related troubleshooting: ACL Troubleshooting Guide
API Reference
The Ominis Cluster Manager provides a RESTful API for managing ACL entries at /v1/acl.
Authentication
All ACL management endpoints require the X-API-Key header:
curl -H "X-API-Key: your-api-key-here" https://demo-client-api.app.ominis.ai/v1/acl
List ACL Entries
Endpoint: GET /v1/acl
Lists all ACL entries in the trusted_users list.
Example Request:
curl -X GET "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: demo-key"
Example Response:
{
"entries": [
{
"cidr": "10.42.0.0/16",
"description": "Kubernetes pod network - cluster internal",
"index": 0
},
{
"cidr": "10.43.0.0/16",
"description": "Kubernetes service network - cluster internal",
"index": 1
},
{
"cidr": "127.0.0.1/32",
"description": "Localhost",
"index": 2
}
],
"total": 3
}
Add ACL Entry
Endpoint: POST /v1/acl
Adds a new IP address or CIDR range to the whitelist. Changes take effect immediately via reloadacl.
Request Body:
{
"cidr": "203.0.113.50/32",
"description": "Office network - temporary access"
}
Example Request:
curl -X POST "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: demo-key" \
-H "Content-Type: application/json" \
-d '{
"cidr": "203.0.113.50/32",
"description": "Office network - temporary access"
}'
Example Response:
{
"cidr": "203.0.113.50/32",
"description": "Office network - temporary access",
"index": 3
}
Validation:
- CIDR must be valid IPv4 address or range (uses Python
ipaddresslibrary) - Duplicate entries rejected with 400 error
- Changes trigger FreeSWITCH
reloadaclautomatically
Update ACL Entry
Endpoint: PUT /v1/acl/{index}
Updates an existing ACL entry by index. Use the index from GET /v1/acl response.
Request Body:
{
"cidr": "203.0.113.0/24",
"description": "Office network - entire subnet"
}
Example Request:
curl -X PUT "https://demo-client-api.app.ominis.ai/v1/acl/3" \
-H "X-API-Key: demo-key" \
-H "Content-Type: application/json" \
-d '{
"cidr": "203.0.113.0/24",
"description": "Office network - entire subnet"
}'
Example Response:
{
"cidr": "203.0.113.0/24",
"description": "Office network - entire subnet",
"index": 3
}
Notes:
- If CIDR changes, old entry deleted and new entry created
- Index may change if CIDR is modified
- Changes trigger FreeSWITCH
reloadaclautomatically
Delete ACL Entry
Endpoint: DELETE /v1/acl/{index}
Removes an ACL entry by index. Warning: Be careful not to lock yourself out!
Example Request:
curl -X DELETE "https://demo-client-api.app.ominis.ai/v1/acl/3" \
-H "X-API-Key: demo-key"
Example Response:
{
"message": "ACL entry deleted successfully",
"deleted_entry": {
"cidr": "203.0.113.50/32",
"description": "Office network - temporary access",
"index": 3
}
}
Error Response (404):
{
"detail": "ACL entry at index 3 not found"
}
Error Scenarios
Duplicate Entry (400)
Request:
curl -X POST "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: demo-key" \
-H "Content-Type: application/json" \
-d '{
"cidr": "10.42.0.0/16",
"description": "Duplicate cluster network"
}'
Response:
{
"detail": "ACL entry for 10.42.0.0/16 already exists"
}
Invalid CIDR (422)
Request:
curl -X POST "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: demo-key" \
-H "Content-Type: application/json" \
-d '{
"cidr": "not-an-ip",
"description": "Invalid CIDR"
}'
Response:
{
"detail": [
{
"loc": ["body", "cidr"],
"msg": "Invalid IP address or CIDR range: not-an-ip",
"type": "value_error"
}
]
}
Unauthorized (401)
Request:
curl -X GET "https://demo-client-api.app.ominis.ai/v1/acl"
# Missing X-API-Key header
Response:
{
"detail": "Missing API Key"
}
Database Schema
The acl_entries table stores all ACL configuration:
CREATE TABLE IF NOT EXISTS acl_entries (
id SERIAL PRIMARY KEY,
list_name VARCHAR(64) NOT NULL DEFAULT 'trusted_users',
cidr VARCHAR(128) NOT NULL,
permission VARCHAR(10) NOT NULL DEFAULT 'allow', -- 'allow' or 'deny'
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(list_name, cidr)
);
CREATE INDEX IF NOT EXISTS idx_acl_entries_list ON acl_entries(list_name);
Default Entries (seeded on first deployment):
INSERT INTO acl_entries (list_name, cidr, permission, description) VALUES
('trusted_users', '10.42.0.0/16', 'allow', 'Kubernetes pod network - cluster internal'),
('trusted_users', '10.43.0.0/16', 'allow', 'Kubernetes service network - cluster internal'),
('trusted_users', '127.0.0.1/32', 'allow', 'Localhost')
ON CONFLICT (list_name, cidr) DO NOTHING;
Query Performance:
- List all entries: 5-10ms (indexed query)
- Add entry: 10-20ms (INSERT + UNIQUE check)
- Delete entry: 5-15ms (DELETE by CIDR)
Security Best Practices
IP Whitelisting Strategy
✅ DO:
- Keep cluster IPs (10.42.0.0/16, 10.43.0.0/16) in ACL
- Use specific /32 entries for single IPs when possible
- Document every ACL entry with clear description
- Review ACL entries regularly (monthly audit)
- Remove temporary entries when no longer needed
- Let external IPs authenticate via database
❌ DON'T:
- Add external IPs to ACL (they should use database auth)
- Use 0.0.0.0/0 or overly broad ranges
- Leave undocumented ACL entries
- Forget to remove temporary test IPs
- Use
apply-register-aclparameter (blocks authentication)
Configuration Parameters
Correct FreeSWITCH Configuration:
<param name="apply-inbound-acl" value="trusted_users"/>
<param name="accept-blind-reg" value="false"/>
<param name="auth-calls" value="true"/>
<!-- DO NOT use apply-register-acl -->
Why This Works:
apply-inbound-acl: Trusted IPs bypass authenticationaccept-blind-reg="false": No blind registration for non-ACL IPsauth-calls="true": Require authentication for calls
Token Security
XML-CURL Token (FREESWITCH_XMLCURL_TOKEN):
- Rotate regularly (quarterly minimum)
- Use strong random strings (32+ characters)
- Never expose in logs or public docs
- Private IPs (RFC 1918) skip token validation automatically
API Key (API_KEY):
- Required for all management endpoints
- Separate from FreeSWITCH token
- Rotate independently
Audit Logging
Monitor ACL changes via database timestamps:
-- Recent ACL changes
SELECT cidr, description, created_at, updated_at
FROM acl_entries
WHERE updated_at > NOW() - INTERVAL '7 days'
ORDER BY updated_at DESC;
-- ACL modification history (requires audit table)
SELECT * FROM acl_audit_log
ORDER BY timestamp DESC
LIMIT 50;
Troubleshooting
External IPs Get 403 Forbidden
Symptom: External SIP clients receive immediate 403 Forbidden without 401 authentication challenge.
Diagnosis:
# Check FreeSWITCH configuration
kubectl exec -n demo-client deployment/freeswitch-registrar -- \
cat /etc/freeswitch/sip_profiles/internal.xml | grep -E "apply-register-acl|apply-inbound-acl"
If you see:
<param name="apply-register-acl" value="trusted_users"/> ❌ PROBLEM
Solution: Replace with apply-inbound-acl:
<param name="apply-inbound-acl" value="trusted_users"/> ✓ CORRECT
<param name="accept-blind-reg" value="false"/>
<param name="auth-calls" value="true"/>
Fix:
- Edit Helm chart:
charts/tenant-infra/templates/configmap-freeswitch-registrar.yaml - Change
apply-register-acltoapply-inbound-acl - Deploy:
make helm-apply - Restart registrar:
kubectl rollout restart deployment/freeswitch-registrar -n demo-client
Why It Happens: apply-register-acl is a hard whitelist that blocks all non-ACL IPs with 403. FreeSWITCH never offers authentication challenge. See ADR: apply-inbound-acl vs apply-register-acl for full explanation.
ACL Changes Not Taking Effect
Symptom: Added ACL entry but FreeSWITCH still rejecting/accepting old IPs.
Diagnosis:
# 1. Check database
kubectl exec -n demo-client deployment/postgres-0 -- \
psql -U postgres -d freeswitch -c "SELECT * FROM acl_entries ORDER BY created_at;"
# 2. Check FreeSWITCH ACL in memory
kubectl exec -n demo-client deployment/freeswitch-registrar -- \
fs_cli -p ClueCon -x "acl_show"
# 3. Check API logs for reload errors
kubectl logs -n demo-client deployment/api | grep -i "reloadacl"
Solutions:
If database has entry but FreeSWITCH doesn't:
# Manual reload
kubectl exec -n demo-client deployment/freeswitch-registrar -- \
fs_cli -p ClueCon -x "reloadxml"
kubectl exec -n demo-client deployment/freeswitch-registrar -- \
fs_cli -p ClueCon -x "reloadacl"
If API reload failed:
# Check registrar pod exists
kubectl get pods -n demo-client -l app=freeswitch-registrar
# If pod not found, API couldn't execute reload
# Entry is saved in database and will load on next pod restart
If mod_xml_curl errors:
# Check FreeSWITCH logs for XML-CURL failures
kubectl logs -n demo-client deployment/freeswitch-registrar | grep -i "xml.curl"
# Common issues:
# - Invalid token: Check FREESWITCH_XMLCURL_TOKEN matches
# - Network error: Verify API service accessible
# - Database error: Check PostgreSQL connection
API Returns 500 Error
Symptom: ACL operations return HTTP 500 Internal Server Error.
Common Causes:
- Database connection failure:
kubectl logs -n demo-client deployment/api | grep -i "database"
# Check PostgreSQL pod status
kubectl get pods -n demo-client -l app=postgres
- Cannot find registrar pod:
kubectl logs -n demo-client deployment/api | grep -i "registrar"
# Verify registrar exists
kubectl get pods -n demo-client -l app=freeswitch-registrar
- kubectl exec permission denied:
# API service account needs permissions
kubectl auth can-i exec pods --as=system:serviceaccount:demo-client:api -n demo-client
# Should return: yes
ACL Entry Deleted, But IP Still Works
Symptom: Deleted ACL entry from database, but IP can still register without password.
Possible Causes:
- FreeSWITCH cache not reloaded:
# Force reload
kubectl exec -n demo-client deployment/freeswitch-registrar -- \
fs_cli -p ClueCon -x "reloadacl"
- Multiple registrar pods (load balancer):
# Check all registrar pods
kubectl get pods -n demo-client -l app=freeswitch-registrar
# Reload all pods
for pod in $(kubectl get pods -n demo-client -l app=freeswitch-registrar -o name); do
kubectl exec -n demo-client $pod -- fs_cli -p ClueCon -x "reloadacl"
done
- Existing SIP registration (not re-challenged):
# SIP registrations persist for registration timeout (default 3600s)
# Client won't re-register until expiration
# Force re-registration by restarting client or waiting for timeout
Cannot Delete ACL Entry (Index Not Found)
Symptom: DELETE /v1/acl/3 returns 404, but entry exists in GET /v1/acl.
Cause: Indices are 0-based and may shift after deletions.
Solution: Always fetch current list before deleting:
# 1. Get current list
curl https://demo-client-api.app.ominis.ai/v1/acl \
-H "X-API-Key: demo-key" | jq '.entries'
# 2. Note the index of entry to delete
# 3. Delete immediately (don't wait, indices may change)
curl -X DELETE https://demo-client-api.app.ominis.ai/v1/acl/3 \
-H "X-API-Key: demo-key"
Common Workflows
Add Temporary Developer Access
Scenario: Developer needs SIP registration access from home office for testing.
# 1. Get developer's public IP
DEV_IP=$(curl -s https://api.ipify.org)
echo "Developer IP: $DEV_IP"
# 2. Add to ACL temporarily
curl -X POST "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"cidr\": \"${DEV_IP}/32\",
\"description\": \"John Doe - home office - temporary (remove after 2025-01-31)\"
}"
# 3. Developer can now register without password
# Uses: Debugging, testing, development
# 4. Remove when testing complete
curl -X GET "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" | jq '.entries[] | select(.description | contains("John Doe"))'
# Get index, then delete
curl -X DELETE "https://demo-client-api.app.ominis.ai/v1/acl/5" \
-H "X-API-Key: ${API_KEY}"
Audit Current ACL Entries
Scenario: Monthly security audit of ACL whitelist.
# 1. Fetch all entries
curl "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" | jq '.'
# 2. Check database for timestamps
kubectl exec -n demo-client deployment/postgres-0 -- \
psql -U postgres -d freeswitch -c \
"SELECT cidr, description, created_at, updated_at FROM acl_entries ORDER BY created_at DESC;"
# 3. Identify entries older than 30 days without description updates
# 4. Review with team and remove obsolete entries
# 5. Export audit report
curl "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" > acl-audit-$(date +%Y-%m-%d).json
Emergency: Remove All External IPs
Scenario: Security incident requiring immediate lockdown to cluster-only access.
# 1. List current ACL
curl "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" | jq '.entries'
# 2. Identify non-cluster entries (not 10.42.*, 10.43.*, 127.0.0.1)
# 3. Delete all external IPs
# Example: Delete index 3, 4, 5 (external IPs)
for idx in 5 4 3; do # Reverse order to avoid index shifts
curl -X DELETE "https://demo-client-api.app.ominis.ai/v1/acl/${idx}" \
-H "X-API-Key: ${API_KEY}"
done
# 4. Verify only cluster IPs remain
curl "https://demo-client-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${API_KEY}" | jq '.entries[].cidr'
# Expected output:
# "10.42.0.0/16"
# "10.43.0.0/16"
# "127.0.0.1/32"
Migrate ACL to New Cluster
Scenario: Export ACL from old cluster, import to new cluster.
# 1. Export from old cluster
curl "https://old-cluster-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${OLD_API_KEY}" > old-acl.json
# 2. Extract non-cluster entries (cluster networks will differ)
jq '.entries[] | select(.cidr | startswith("10.42.") or startswith("10.43.") or startswith("127.0.0.1") | not)' old-acl.json > external-acl.json
# 3. Import to new cluster
jq -c '.cidr, .description' external-acl.json | while read cidr; read description; do
curl -X POST "https://new-cluster-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${NEW_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"cidr\": $cidr, \"description\": $description}"
done
# 4. Verify migration
curl "https://new-cluster-api.app.ominis.ai/v1/acl" \
-H "X-API-Key: ${NEW_API_KEY}" | jq '.entries'
Performance
Database Query Performance
| Operation | Typical Time | Notes |
|---|---|---|
| List ACL entries | 5-10ms | SELECT with index |
| Add ACL entry | 10-20ms | INSERT + UNIQUE check |
| Update ACL entry | 10-25ms | DELETE + INSERT |
| Delete ACL entry | 5-15ms | DELETE by CIDR |
| FreeSWITCH reload | 50-100ms | reloadxml + reloadacl |
FreeSWITCH ACL Loading
- Initial load (mod_xml_curl query): 50-150ms
- Cached in memory: After first load, no database query
- Reload on demand:
reloadaclre-queries database - No per-registration overhead: ACL checked in FreeSWITCH memory
Scaling Considerations
- ACL size: Up to 1000 entries, no performance impact
- Query optimization: Index on
list_namecolumn - Kubernetes: API uses pod service account for kubectl exec
- High availability: Multiple API pods share same database
Related Documentation
- Directory XML-CURL Integration - How mod_xml_curl queries the API for user authentication
- Extension Management - Managing SIP extensions that authenticate via database
- Helm Deployment - How ACL configuration is deployed via Helm charts
- Database Schema - Complete database schema including acl_entries table
- Cluster Infrastructure - Kubernetes network architecture and service mesh
Summary
The Ominis Cluster Manager ACL system provides:
✅ Database-backed persistence - ACL entries survive all deployments and restarts
✅ Real-time updates - Changes take effect in < 100ms via reloadacl
✅ Network segmentation - Cluster IPs trusted, external IPs authenticated
✅ Full CRUD API - Complete lifecycle management at /v1/acl
✅ Security best practices - Token authentication, audit logging, proper FreeSWITCH configuration
Key Takeaway: Use apply-inbound-acl (not apply-register-acl) to enable both cluster IP trust and external IP authentication.