Writing ACL policies
QuickZTNA ACLs are attribute-based — rules match on user, tag, device posture, time, country, protocol, and port. This guide walks through writing rules, testing them before rollout, versioning, and AI-generated policies.
The ACL model
A rule has src (who's allowed), dst (what they can reach), and optional conditions. A connection is allowed iff at least one rule matches.
{
"name": "engineers-to-prod-db",
"src": "tag:engineer",
"dst": "tag:prod-db",
"proto": "tcp",
"ports": "5432"
} Matcher syntax
Both src and dst accept:
Any machine with this tag. Preferred — scales as fleet grows.
Specific user. E.g. user:alice@acme.com.
IdP group name (case-insensitive). Resolved from OIDC claims or SCIM sync.
Specific machine by tailnet IP. Fragile — prefer tags.
Subnet range. Use for advertised subnet routes.
Any — use sparingly, only for well-scoped admin rules.
Conditions
Transport protocol. Default *.
Comma-separated list or ranges: "22,443,5000-5010". Default *.
UTC hour range: "09-18". Applies at connection evaluation time.
Day range or specific list: "mon,wed,fri".
GeoIP-resolved country: "IN", "US", or list "IN,US,GB".
Match by source machine OS.
Source machine must be in compliant posture state.
Examples
Engineers SSH to prod (business hours, India only)
{
"name": "eng-ssh-prod-business-hours",
"src": "tag:engineer",
"dst": "tag:prod",
"proto": "tcp",
"ports": "22",
"time_hour": "09-18",
"day_of_week": "mon-fri",
"source_country": "IN",
"require_posture": true
} DB admins to Postgres on multiple ports
{
"name": "dba-full-db-access",
"src": "group:dba",
"dst": "tag:prod-db",
"proto": "tcp",
"ports": "5432,5433,6432,9990-9999"
} CI runners to any test machine (no production)
{
"name": "ci-to-test",
"src": "tag:ci",
"dst": "tag:test",
"proto": "*",
"ports": "*"
} AI-generated rules (natural language)
Type in English — get ACL JSON. Available free on all plans via /api/ai-assist action generate_acl.
$ curl https://login.quickztna.com/api/ai-assist \
-H "Authorization: Bearer $TOKEN" \
-d '{
"action": "generate_acl",
"org_id": "org_9fX2kR",
"description": "Finance team can access the accounting server during work hours from the office IP range"
}' Review the generated rule, adjust if needed, and apply.
Test before rolling out
/api/acl-evaluate endpoint (ztna acl test) is free and instant.
$ ztna acl test --src laptop-alex --dst db-primary --port 5432 --proto tcp
✓ ALLOW
Matched rule: acl_7K9xq (eng-ssh-prod-business-hours)
src: tag:laptop matches tag:engineer ✓
dst: tag:prod matches ✓
port 5432: in "5432" ✓
time: 14:23 UTC (Thursday) ✓
source_country: IN ✓ Apply a rule
ACLs → New rule → paste JSON → "Test" → "Save". Versioning + rollback
Every change to the ACL set creates a new version via /api/governance action policy_version_create. Roll back at any time with policy_version_rollback.
$ curl https://login.quickztna.com/api/governance \
-H "Authorization: Bearer $QZ_API_KEY" \
-d '{
"action": "policy_version_rollback",
"org_id": "org_9fX2kR",
"target_version": 42
}' JIT overrides
If a user needs time-bounded access outside normal ACL rules, they request JIT. Approver grants → temporary ACL rule is injected → auto-expires at the end of the window. See API: jit_request.
Common pitfalls
By default, no rules = no restrictions (full mesh connectivity). Set org setting default_deny to flip — then you must write explicit allow rules for everything.
ACL engine doesn't warn if you reference tag:prood (typo). Rule just never matches. Use ztna acl test to verify.
time_hour is UTC. "9-5 IST business hours" is 03-12 UTC. Use AI-generated rules — it handles the conversion.
require_posture: true means the SOURCE must be compliant. A non-compliant laptop can still be a destination, unless you add the condition to another rule.
See also
- Posture policies — what makes a machine "compliant"
- API: acl-evaluate — the evaluator behind
ztna acl test - CLI: ztna acl test — CLI wrapper