OPNsenseXLII · API and AutomationAPI operations
API alias automation — hosts, networks, ports, URL-tables, and nested tables
What you'll learn
- Describe the alias types and what each can hold
- Add and update each alias type via the API with valid payloads, using read-modify-write for updates
- Recognise the failure modes that leave an alias wrong while the API reports success
- Combine aliases into network groups and verify the resulting pf table
Prerequisites
Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14
Aliases are the firewall’s named vocabulary. An alias is a table that the firewall expands when matching a rule — address="office_subnets" means “pass traffic to whatever is in the office_subnets alias right now”. The API drives aliases through CRUD operations; URL-tables (dynamic address lists fetched from a remote source) are also aliases. The discipline is straightforward until the script creates aliases that reference other aliases, fetch URL-table content that does not exist, or combine overlapping CIDRs in ways the framework does not accept.
This lesson covers the alias types, the API payloads for each, the validation traps (overlaps, reference cycles, dead URL-tables), and the nested-alias composition pattern that makes rules readable at fleet scale.
Alias types
The alias model defines fourteen types. The ones an automation script writes are these:
| Type | Content | Example |
|---|---|---|
host | Hosts by IP or FQDN, or other host aliases | 192.0.2.10, build.example.com |
network | Networks in CIDR, or other network aliases | 10.0.0.0/8, 172.16.0.0/12 |
port | Port numbers and ranges | 22, 443, 8000:8999 |
mac | MAC addresses, resolved periodically | 00:1b:44:11:3a:b7 |
url | A URL whose contents are fetched once | https://example.com/lists/blocked.txt |
urltable | A URL refetched on an interval | https://example.com/lists/blocked.txt |
urljson | As urltable, with a jq expression selecting the addresses | https://example.com/feed.json |
geoip | Country codes | RU, CN |
networkgroup | Names of other network-type aliases | office_a, office_b |
asn | Autonomous system numbers, expanded to their networks | AS64496 |
The remaining types — dynipv6host, authgroup, internal, external — exist for cases automation rarely drives, and internal in particular is reserved for aliases the product manages itself. Internal aliases are prefixed __ so they cannot collide with a name an operator chooses.
type is mandatory on add. content is a list; over the API a newline-separated string is the usual way to send it, and the framework splits and validates each entry against the type. Content that fails validation fails the whole call — there is no partial acceptance where the good entries save and the bad ones are dropped.
$ curl -sk -u "$KEY:$SECRET" https://localhost/api/firewall/alias/getAliasUUID/office_subnets{
"uuid": "5c4d3e2f-1a0b-9c8d-7e6f-5a4b3c2d1e0f"
}Illustrative output
Adding each alias type
The add payload differs slightly per type. The fields the script must populate:
{
"alias": {
"name": "office_subnets",
"type": "network",
"content": "10.0.0.0/8\n172.16.0.0/12\n192.168.0.0/16",
"enabled": "1",
"description": "Office subnets (CHG-2026-1314)"
}
}
For a URL table (dynamic address list):
{
"alias": {
"name": "blocked_ips",
"type": "urltable",
"content": "https://example.com/lists/blocked.txt",
"updatefreq": "0.5",
"enabled": "1",
"description": "External blocklist (CHG-2026-1314)"
}
}
There is no separate url field. The URL goes in content, exactly where the addresses go for a host or network alias — the type is what tells the firewall to treat the value as a source to fetch rather than as an address. updatefreq is the refresh interval in days, and it takes fractions, so 0.5 is twelve-hourly and 0.04 is roughly hourly. It applies to urltable and urljson; a plain url alias is fetched once and never refreshed, which makes it the wrong choice for a feed that changes.
The file the firewall fetches should contain one address per line. Lines beginning with whitespace, a colon, a semicolon, a pipe or a hash are skipped, which is how most published blocklists carry their comments.
For a port alias:
{
"alias": {
"name": "consumer_ports",
"type": "port",
"content": "80\n443\n8000:8099\n8443",
"enabled": "1",
"description": "Ports used by consumer services"
}
}
Port ranges use a colon — 8000:8099 above — and can be mixed with individual ports in the same alias.
$ curl -sk -u "$KEY:$SECRET" -X POST -H "Content-Type: application/json" https://localhost/api/firewall/alias/addItem -d '{"alias":{"name":"ci_runners","type":"host","content":"192.0.2.50\n192.0.2.51\n192.0.2.52","enabled":"1","description":"CI runners (CHG-2026-1314)"}}'{
"result": "saved",
"uuid": "a8b9c0d1-e2f3-4567-89ab-cdef01234567"
}Illustrative output
Update vs replace
A common pattern is to update an alias’s content — the CI runners grew from three to five, the office subnets added a new branch. The right call is setItem, not addItem, and the UUID goes in the URL rather than in the body:
curl -sk -u "$KEY:$SECRET" -X POST \
-H "Content-Type: application/json" \
https://localhost/api/firewall/alias/setItem/a8b9c0d1-e2f3-4567-89ab-cdef01234567 \
-d '{"alias":{
"name":"ci_runners",
"type":"host",
"enabled":"1",
"description":"CI runners (CHG-2026-1314)",
"content":"192.0.2.50\n192.0.2.51\n192.0.2.52\n192.0.2.53\n192.0.2.54"
}}'
Two things about that payload are easy to get wrong.
First, the content is replaced, not merged. There is no “append an address” call; the script sends the whole list it wants the alias to end up holding. An automation that adds one runner by posting only the new address has just deleted the other four.
Second, setItem writes the whole record. Fields the payload omits are set to the model’s default rather than left as they were, so a payload carrying only content also blanks the description and resets anything else that had been configured. The safe sequence is read-modify-write: getItem/{uuid}, change the fields you mean to change in the object you got back, and send that whole object.
Nested aliases (aliases that reference aliases)
An alias can reference other aliases, and the reference is just the other alias’s name in the content — there is no wrapping syntax. Host and network aliases both accept nested references, and the networkgroup type exists specifically for the case where an alias contains nothing but other aliases:
{
"alias": {
"name": "approved_sources",
"type": "networkgroup",
"content": "office_subnets\nvpn_clients\ncorporate_wan",
"enabled": "1",
"description": "All approved sources for restricted traffic"
}
}
The firewall expands the references when it builds the tables, so a rule whose source is approved_sources matches any address in any of the three. Composition is what keeps rules readable at fleet scale: a rule that names three well-chosen aliases is more auditable than one that lists fifty CIDRs, and updating the membership is a change to one alias rather than to every rule.
The practical reason to prefer networkgroup over stuffing names into a network alias is validation. A networkgroup accepts only names of existing aliases, so a typo fails at the API call. A network alias accepts both CIDRs and alias names, so the same typo may well save.
Validation traps
Four traps that catch automation scripts in production:
- A partial content update is a deletion.
setItemreplaces the whole record, and content is a whole list. There is no append. This is the single most common way an alias automation destroys data, and it is silent — the call returns saved. - A dead URL table keeps its last good contents. A
urltablewhose URL starts returning 404, or HTML, or nothing, does not empty itself: the pf table holds whatever was fetched last time it worked. The alias looks fine and the rules that use it keep matching yesterday’s list. Nothing in the save-and-apply path reports this, because from the configuration’s point of view nothing is wrong. The check is on the running table, not the configuration. - The name is the interface into rules, so it is not free to change. Rules reference aliases by name. A rename is a breaking change to every rule that used the old name, and unlike a delete it is not refused.
- Types are not interchangeable after the fact. A
hostalias holds addresses; aportalias holds ports; a rule field expects one or the other. Changing the type of an alias that is already referenced changes what the referencing rules mean — assuming the content survives revalidation at all.
$ curl -sk -u "$KEY:$SECRET" -X POST https://localhost/api/firewall/alias/reconfigure; curl -sk -u "$KEY:$SECRET" https://localhost/api/firewall/alias_util/list/ci_runners | jq '{total, rows: [.rows[].ip]}'{
"status": "ok"
}
{
"total": 5,
"rows": [
"192.0.2.50",
"192.0.2.51",
"192.0.2.52",
"192.0.2.53",
"192.0.2.54"
]
}Illustrative output
Summary
- Aliases are driven through
/api/firewall/alias/*:searchItem,getItem/{uuid},addItem,setItem/{uuid},delItem/{uuid}, thenreconfigureto load the tables. getAliasUUID/{name}turns the name a rule uses into the UUID the API needs, and doubles as an existence check.- The add payload is
name,type,content,enabled,description. Forurltablethe URL goes incontentandupdatefreqis the refresh interval in days, fractions allowed. setItemreplaces the whole record, so update by read-modify-write. There is no append, and omitted fields lose their values.- Nested aliases reference other aliases by bare name;
networkgroupis the type for an alias made only of other aliases, and it validates the names. - Verify against the running table with
alias_util/list/{name}, not against the configuration. A staleurltablelooks perfectly healthy in the configuration.
Knowledge check · 4 questions
Q1. You want an alias whose addresses come from a remote file that is refetched every 12 hours. Which payload is correct?
Q2. Posting {"alias":{"content":"192.0.2.53"}} to setItem/{uuid} adds one address to an alias that already holds four.
Q3. Which of these failures are silent — the API call succeeds and nothing reports a problem? Select all that apply.
Q4. A change-set creates a networkgroup alias named approved_sources whose content is office_subnets and vpn_clients, then creates those two aliases. The first call fails validation. Why?
Passing score: 75%. Answers are checked in this browser.