The Access Document
One HuJSON file per fleet decides which of your own principals may reach which targets, on which ports.
On this page (12)
The Access Document Documentation
One document governs a whole fleet. It is written in HuJSON, so comments and trailing commas are accepted, and it is compiled against your current roster before it is stored. What is stored is strict canonical JSON, hashed, with the case you wrote preserved.
Three things hold over everything below.
- Tenancy is evaluated first. Another customer's agent cannot reach yours whether you publish a document or not.
- The document narrows and never widens. Publishing one can only take reachability away from what tenancy already allows inside your fleet.
- The first matching grant wins. Order your grants accordingly.
A document
{
// Aliases you can name as a destination.
"hosts": {
"metrics": "2a04:2a01:9::5",
},
"grants": [
{ "src": ["tag:ci"], "dst": ["tag:prod"], "ip": ["tcp:22"] },
{ "src": ["group:sre"], "dst": ["metrics"], "ip": ["tcp:8000-8100"] },
{ "src": ["tag:prod"], "dst": ["autogroup:internet"], "ip": ["tcp:443"],
"where": { "dst.rpki": { "eq": "valid", "onUnknown": "deny" } } },
],
"tests": [
{ "src": "tag:ci", "accept": ["tag:prod:22"], "deny": ["tag:prod:5432"] },
],
"default": "deny",
}
Top-level keys
Any other top-level key is a 400, and the error names the key it did not recognise.
| Key | Contract |
|---|---|
grants | The list of permissions. Everything else in the document exists to be named from here. |
hosts | Aliases mapping a name to an address you can then use as a destination. |
groups | Named sets of principals, referenced elsewhere as group:<label>. |
tagOwners | Which principals may apply a given tag:<label>. |
tests | Assertions that must pass before the document is stored. |
default | What happens when no grant matched. "deny" is how you lock a fleet down. |
groups and tagOwners are both objects whose keys carry their own prefix and whose values are lists of members:
{
"groups": { "group:oncall": ["autogroup:tagged", "group:platform"] },
"tagOwners": { "tag:database": ["group:platform"] }
}
A key must have a name after its prefix, and a list must name at least one member. An empty list is refused rather than stored, because a set that grants nothing reads on the page as a set that grants something. A member may be a node, an autogroup token, or another group, which is resolved in turn.
An empty document, {}, is rejected rather than treated as a lock-down. The lock-down is {"default": "deny"}.
grants
A grant carries src, dst, ip, and optionally where.
src
Always one of your own principals: a group: label, a tag: label, an autogroup token, a member label, an agent id, or a /128. A CIDR or a public host as a source is a 400, because a source you do not hold is not yours to grant.
dst
Your own nodes, an alias from hosts, a single address, a CIDR, *, ::/0, or autogroup:internet.
ip
A protocol, a port, or both.
| Form | Means |
|---|---|
tcp:22 | One port over one protocol. |
udp:53 | The same, over UDP. |
tcp:8000-8100 | A port range. |
tcp, udp, icmp, sctp | That protocol, any port. |
tcp:* | TCP, any port. |
* | Anything. |
A malformed value is a 400 whose body carries a JSON pointer to the offending element, for example grants[2].ip[0].
where predicates
A where clause adds a condition the Whisper graph answers, evaluated once when the connection is opened.
| Predicate | Form | Accepts |
|---|---|---|
dst.rpki | { "eq": "valid" } | The route-origin validation state of the destination. |
dst.band | { "notin": ["HIGH", "CRITICAL"] } | NONE, LOW, MEDIUM, HIGH, CRITICAL. |
dst.band is the destination's reputation band, which the graph derives from what it has actually looked at; read coverage before you write a rule that turns on one.
Every predicate carries onUnknown, either allow or deny, and it is allow when you do not write it. The value you get is rendered back in acl show, so the document you read is the document that is enforced, and taking the unknown leg is recorded.
tests
Each assertion names a source and two lists: destinations that must be accepted and destinations that must be denied.
{ "src": "tag:ci", "accept": ["tag:prod:22"], "deny": ["tag:prod:5432"] }
An assertion that fails, or one whose source or destination resolves to nothing at all, aborts the write. The failure is reported by JSON pointer, and the document already in force stays exactly where it was. This is the gate that makes a document prove it governs what you think it governs before it can replace the one in force.
Autogroup tokens
| Token | Selects | May appear as |
|---|---|---|
autogroup:member | Every node on your roster. | Source or destination. |
autogroup:self | Every node on your roster. The same set as member, under the word that reads better in a grant about a node reaching itself. | Source or destination. |
autogroup:tagged | Every node carrying at least one tag. | Source or destination. |
autogroup:nested | Every node registered underneath another identity. | Source or destination. |
autogroup:internet | Everything outside your own address space. | Destination only. |
A source is always one of your own nodes, so naming autogroup:internet as a source is refused with a message saying so. An unknown token is refused and the error lists the ones that exist.
The verbs
whisper whale acl show
whisper whale acl show --raw > policy.hujson
whisper whale acl test group:sre tag:prod:22
whisper whale acl set policy.hujson --base <hash>
whisper whale acl withdraw
| Verb | Contract |
|---|---|
acl show | The document in force, the hash the control plane knows it by, and what it compiled to. --raw prints the canonical document and nothing else, ready to edit and re-publish. |
acl test <src> <dst> | What the compiled document says about one source and one target, answered from the artifact the connection path itself reads. Exit status is 1 when the flow is refused, so it can gate a deploy. |
acl set <file> | A full replace, never a merge. Pass - to read from stdin. Pass --base with the hash show gave you, so a second editor cannot overwrite your work unseen, or --force to accept last-writer-wins. |
acl withdraw | Removes the document, leaving your fleet governed by tenancy alone. Takes the same --base and --force. |
Publishing takes dns:whale:write, a scope that is never granted automatically, because one document decides reachability for every node you hold. Reading is an ordinary read. See keys and scopes.
Over the API
The document travels on op:policy in the whale sub-map, alongside base, which names the document you edited exactly as --base does. Passing acl: null withdraws it. Set policy has the operation contract.
To ask the same question acl test asks, read it back:
CALL whisper.agents({
op: 'list',
args: { kind: 'whale', view: 'acltest',
src: 'group:sre', dst: 'tag:prod:22' } })
With nothing published the reply is state: "no_document". Otherwise it returns a verdict row per flow with src, src_name, dst, proto, port, verdict and why.
The two hashes
show reports a document hash and an artifact hash, and they move for different reasons.
| Hash | Changes when |
|---|---|
| Document | Someone edits the document. |
| Artifact | The document changes, or the roster does. |
Watch the artifact hash. It is the one that tells you what the connection path is actually reading, and a node minted or released changes it without anyone touching the file. East-west reachability covers the order in which the artifact is consulted.