MUD (RFC 8520)
Every MUD file in the world is a claim about what a device intends to do. Ours is a statement of what is being enforced right now, at an address you can verify from the DNS root without asking us.
curl -s https://whisper.online/.well-known/mud/{agent-128}.json
No account, no key. The file is application/mud+json and any RFC 8520 MUD manager can read it.
What MUD is, and the one thing it never had
RFC 8520 lets a device declare the hosts it should ever talk to: "my manufacturer's cloud, my controller, the local subnet, nothing else." The device emits a URL, the network fetches a JSON file, and the file lists what is permitted. Everything not listed is denied. It is one of the sharpest ideas in device security.
The RFC is also candid about the catch. The declaration is validated and enforced by a MUD manager at the nearest hop. That manager is site-scoped, the identity behind the declaration is whatever the LAN believes, and there is no shared revocation. The file says what a device wants; whether anything enforces it is a separate question you cannot answer by reading the file.
Why ours is a different kind of statement
We issue the /128 the ACL is keyed to, and we are the egress path the traffic actually takes. So the file is not a declaration we are passing along. It is a projection of the rule set that is in force, generated from it on read:
op:firewall ──► FirewallRuleSet ──► enforced on every CONNECT
│
└───────────► rendered as the MUD file you fetch
One object, read twice. There is no second copy to drift, no publish step to forget, and a diff in the bytes means the policy moved. The same pattern as the RDAP record, which is generated from the live zone rather than maintained beside it.
And the identity the ACL is keyed to is not a LAN's opinion. It is a routable address with reverse DNS, a DANE-EE pin in DNSSEC-signed DNS and a public registration, all checkable from the IANA root by someone who has never heard of us. See Verify an agent.
A file, in full
This is the complete output of the renderer for an agent whose policy denies one hostname, then permits another and a prefix. Nothing here is abridged or hand-written; it is what the code emits.
The address is 2a04:2a01:9::50, from 2a04:2a01:9::/48, which we reserve for documentation. Fetch
it and you will get the documented 404 rather than this file, because no agent is assigned there.
That is the endpoint behaving exactly as the table below describes.
{
"ietf-mud:mud" : {
"mud-version" : 1,
"mud-url" : "https://whisper.online/.well-known/mud/2a04:2a01:9::50.json",
"last-update" : "2026-09-27T09:14:02Z",
"cache-validity" : 24,
"is-supported" : true,
"systeminfo" : "Whisper agent a0000000000000050 (plant-historian-bridge)",
"documentation" : "https://whisper.online/docs/mud",
"from-device-policy" : {
"access-lists" : {
"access-list" : [ {
"name" : "mud-a0000000000000050-v6fr"
}, {
"name" : "mud-a0000000000000050-v4fr"
} ]
}
}
},
"ietf-access-control-list:acls" : {
"acl" : [ {
"name" : "mud-a0000000000000050-v6fr",
"type" : "ipv6-acl-type",
"aces" : {
"ace" : [ {
"name" : "deny-host-updates-historian-example-plant-internal-and-subdomains",
"matches" : {
"ipv6" : {
"ietf-acldns:dst-dnsname" : "updates.historian.example-plant.internal",
"protocol" : 6
},
"tcp" : {
"ietf-mud:direction-initiated" : "from-device"
}
},
"actions" : {
"forwarding" : "drop"
}
}, {
"name" : "host-historian-example-plant-internal-and-subdomains",
"matches" : {
"ipv6" : {
"ietf-acldns:dst-dnsname" : "historian.example-plant.internal",
"protocol" : 6
},
"tcp" : {
"ietf-mud:direction-initiated" : "from-device"
}
},
"actions" : {
"forwarding" : "accept"
}
}, {
"name" : "net-2001-db8-5-48",
"matches" : {
"ipv6" : {
"destination-ipv6-network" : "2001:db8:5::/48",
"protocol" : 6
},
"tcp" : {
"ietf-mud:direction-initiated" : "from-device"
}
},
"actions" : {
"forwarding" : "accept"
}
} ]
}
}, {
"name" : "mud-a0000000000000050-v4fr",
"type" : "ipv4-acl-type",
"aces" : {
"ace" : [ {
"name" : "deny-host-updates-historian-example-plant-internal-and-subdomains",
"matches" : {
"ipv4" : {
"ietf-acldns:dst-dnsname" : "updates.historian.example-plant.internal",
"protocol" : 6
},
"tcp" : {
"ietf-mud:direction-initiated" : "from-device"
}
},
"actions" : {
"forwarding" : "drop"
}
}, {
"name" : "host-historian-example-plant-internal-and-subdomains",
"matches" : {
"ipv4" : {
"ietf-acldns:dst-dnsname" : "historian.example-plant.internal",
"protocol" : 6
},
"tcp" : {
"ietf-mud:direction-initiated" : "from-device"
}
},
"actions" : {
"forwarding" : "accept"
}
} ]
}
} ]
}
}
Each ace is one rule from the agent's egress policy. A host rule becomes ietf-acldns:dst-dnsname,
a prefix becomes destination-ipv6-network or destination-ipv4-network, a port becomes a
destination-port. ALLOW becomes forwarding: accept and DENY becomes forwarding: drop.
An ace is named after the permit it states, never after its position in the list. Remove one rule
and every other entry stays byte-identical, so a diff between two fetches shows the one thing that
moved instead of renumbering the whole tail.
Denials are in the file, and the order is the meaning
The rule set is first-match-wins: the first rule whose destination matches decides, and nothing
after it is consulted. So deny updates.historian.example-plant.internal ahead of
allow historian.example-plant.internal is not redundant under a default deny - it is the whole
carve-out, and a file that listed only the permit would have claimed the agent may reach a name we
block.
That is an overstatement of containment, which is the direction every refusal on this page exists to
avoid, so it is not something to leave to a footnote. The file states denials, in place, as
forwarding: drop.
It works because the two models are the same model. An RFC 8519 ACL, which RFC 8520 builds on, has an
ace list that is ordered-by user, and the rule is "Actions on the first matching ACE are applied
with no processing of subsequent ACEs". That is our decide(), written in YANG. RFC 8520 section 2
admits drop alongside accept for exactly this. So the ordering and the actions mean in a MUD
manager what they mean in our egress path. The matches are a separate question, and the next
section is where this file and the enforced set come apart.
One consequence worth stating: a rule that names the same destination as an earlier one can never
fire, so it is not in the file, however it is spelled - ip 203.0.113.5 and cidr 203.0.113.5/32
are one destination and appear once. A rule an earlier one merely contains is a harder case and it
is still emitted: deny ip 10.1.2.3 after allow cidr 10.0.0.0/8 cannot fire either, but a reader
evaluating the list first-match-wins reaches the same verdict we do, so the file stays faithful while
carrying an entry that never decides anything.
Why the hostname appears in both ACLs
Because a name is not an address family, and RFC 8520 makes that distinction load-bearing. The
specification replicates the ietf-acldns augment across ipv4 and ipv6 deliberately, "to allow
MUD file authors the ability to control the IP version that the Thing may utilize". So a
dst-dnsname under ipv6 alone is not shorthand: it is the positive claim "over IPv6 only".
Our enforcement makes no such claim. A host rule matches on the name with no family test, and the egress resolves prefer-IPv6 with an IPv4 fallback. Emitting the name under one family would describe a narrower permitted set than is actually enforced, and a MUD manager acting on that file would block traffic we allow. The prefix rule, by contrast, names its family and stays in it.
The same holds for a bare port rule, for the same reason: it constrains the port on any destination, which is not a statement about IPv4 or IPv6 either, so it appears in both ACLs too.
The one place this file says less than we enforce
A host rule is a suffix match: permitting api.example.com permits every name under it. RFC 8520
has no way to write that down. dst-dnsname is a plain inet:host, "domain name to be matched
against", with no wildcard and no suffix form, so the closest expressible statement is the bare
name.
We emit the bare name, which makes the file a strict subset of what is enforced. That is the
safe direction for anything acting on the file - a MUD manager enforcing it denies traffic we allow,
which inconveniences the agent rather than weakening the containment - and the unsafe direction for
anyone reading it as a complete inventory. So the entry says so in its own name:
host-api-example-com-and-subdomains.
A namespaced extension would carry it to a machine, and RFC 8520 makes that safe: "Implementations MUST ignore any node in this file that they do not understand". We do not use one, because the same section requires every declared extension to be "registered with the IANA and described in an RFC". Declaring an unregistered extension in order to fix an overstatement would be one.
What publishing this costs you
Setting a default-deny rule set publishes this file. There is no separate switch and no key on the door: the agent's id, its label and every destination it is permitted to reach become readable by anyone who can work out the address, and the address is derivable from the agent's name.
That is the design, not an oversight - a containment claim nobody can check is worth nothing, which
is the criticism this page opens with. But it is worth knowing before you set the policy, because the
destinations are often internal names. The example above is historian.example-plant.internal, and a
plant's security team may reasonably not expect that name to be world-readable.
The only way not to publish is not to govern, which is the worse trade. If you need the enforcement without the projection, say so and we will build the flag; today it does not exist, and we would rather write that down than let you find out from a search engine.
When you get a 404, and why that is the honest answer
A MUD file is a machine-readable containment claim, and other people's networks act on it. So the endpoint refuses in every case where answering would mean overstating, and says which case it is:
| you asked about | answer |
|---|---|
| an address that is not a Whisper agent | 404 - there is nothing to describe |
| an agent with no default-deny policy | 404 - see below, this is the important one |
| an agent whose policy permits nothing yet | 404 - usually an agent mid-provision |
| an agent nested behind a transit parent | 404 - see below |
a path that does not end .json |
400 - you addressed it wrong, which is a different answer |
| a tail that is not an IPv6 literal | 400 - same, and the reason names the input |
| another spelling of the same address | 301 to the one we restate - see below |
No default-deny policy. MUD is default-deny by enumeration: the file lists what is permitted and the denial of everything else is implicit. RFC 8520's own worked examples each end "Deny all other access". There is therefore no way to write "this device may talk to anything", and an empty access-list does not mean unrestricted - it means deny everything, the exact inverse. Publishing an empty file for an ungoverned agent would advertise maximum containment for the one agent that has none. So we publish nothing. Set a default-deny rule set with op:firewall and the file appears.
Nested behind a transit parent. A nested agent's effective policy is its own rule set intersected with every live ancestor's, most-restrictive-wins. Its own rule set therefore describes a larger permitted set than is actually enforced, and for a containment claim, overstating is the dangerous direction. Rather than publish a generous file we publish none, until the intersection is enumerable.
Another spelling. An IPv6 address has many spellings and RFC 8520 has a manager compare the
mud-url inside the file against the URL it fetched from. We restate one spelling, so a 200 is
served at that one and every other redirects to it: liberal in what we accept, conservative in what
we emit. Follow the Location and the file will name the URL you used.
Every one of those answers carries a JSON reason, the redirect included, because the reader is another network's MUD manager deciding whether we are broken, whether it addressed us wrongly, or whether this agent simply has nothing to declare - and those are three different answers.
What this does not do yet
mud-signature. RFC 8520 defines a detached CMS signature. The leaf is omitted rather than pointed at a signature that is not there.- Ingesting a third-party MUD URL. Handing us a manufacturer's MUD file and having its ACLs become your agent's policy is the consumer half, and it is not built.
to-device-policy. We govern egress. Inbound is a different enforcement point that we do not sit on, and claiming it would be the same overstatement as the two refusals above.
Next
- Egress governance - setting the policy this file projects
- Verify an agent - checking the identity the ACL is keyed to
- Nested identity - why a nested agent's file is withheld