Skip to content

Checking before you delete something

Every configuration change on a firewall is preceded by the same question: what breaks if I touch this. find_references answers it in one call.

Is the address object DMZ-SERVERS safe to delete?

The answer comes from the appliance’s own reference lookup — the same one the web UI’s reference counter uses — rather than from guessing at a handful of tables. That matters more than it sounds: on FortiOS 7.0.14 there are 234 tables that can hold a reference to an interface and 74 for a firewall address, so any hand-rolled scan is checking a rounding error’s worth of them.

Alongside the authoritative answer, the tool still reads policies, groups, virtual IPs, and static routes directly, because those carry readable detail the lookup does not — a policy’s name, whether it is on, whether it accepts or denies.

{
"object": "DMZ-SERVERS",
"verdict": "referenced",
"resolved_as": ["address"],
"total_references": 3,
"candidate_tables": 74,
"references": [
{ "table": "firewall.policy", "object": "9", "attribute": "dstaddr" },
{ "table": "firewall.addrgrp", "object": "ALL-SERVERS", "attribute": "member" }
],
"sources_checked": {
"object_usage": "ok",
"policies": "ok", "address_groups": "ok", "service_groups": "ok",
"vips": "ok", "routes": "ok"
},
"safe_to_delete": false,
"policies": [
{ "id": 9, "name": "block-legacy-smb", "enabled": true, "action": "deny", "referenced_as": ["destination"] },
{ "id": 12, "name": "dmz-mgmt", "enabled": false, "action": "accept", "referenced_as": ["destination", "source"] }
],
"groups": [{ "name": "ALL-SERVERS", "kind": "address_group" }],
"vips": [],
"routes": []
}

verdict has five values and it is the field to read. safe_to_delete is present only on referenced and no_references, because a lookup that could not run cannot support a conclusion either way. The other three are below.

references is the authoritative half and policies / groups / vips / routes are the readable half, so the same reference can legitimately appear in both — once as a row naming the table and attribute, once with the policy’s name and action attached.

referenced_as names the field the object turned up in, so a policy listing it as both source and destination reports both rather than being counted twice.

Disabled policies count. Policy 12 above is off, and it is still a reference. A disabled policy is one somebody intends to turn back on, and deleting the object underneath it turns that into a surprise at the worst possible moment.

Group membership is not expanded. DMZ-SERVERS is in ALL-SERVERS. Anything referencing ALL-SERVERS therefore depends on DMZ-SERVERS indirectly, and none of those policies appear in this result — they reference the group, not the object. The tool does not follow the chain for you. When groups is non-empty, run find_references again on each group name, and check that the model actually did rather than assuming it would.

safe_to_delete appears on referenced and no_references only. The other three verdicts omit it, because each one means the question was not actually settled — and omitting the key forces you to read the verdict, where a false would invite you to stop.

No address, group, service, virtual IP, or interface carries that name.

{
"object": "DMZ-SERVRES",
"verdict": "object_not_found",
"resolved_as": [],
"total_references": 0,
"note": "No address, group, service, virtual IP, or interface named 'DMZ-SERVRES' exists on this appliance, so this answer is about a name rather than an object. Check the spelling."
}

Look closely at the object name. Without this verdict, a typo reports zero references and safe_to_delete: true — a confident yes to a question nobody actually asked. This is the cheapest possible bug to hit and one of the more expensive ones to act on.

The appliance’s own lookup was unavailable, the fallback scan of four tables ran, and it found nothing.

{
"object": "DMZ-SERVERS",
"verdict": "no_references_in_checked_scopes",
"total_references": 0,
"sources_checked": { "object_usage": "denied: http=403", "policies": "ok" },
"note": "The appliance's own reference lookup was unavailable, so this covers only policies, groups, virtual IPs, and static routes. FortiOS reports many more tables that can hold a reference, and they were not consulted."
}

This is a fact about four tables, not about the firewall. Of the 74 tables that can reference an address, four came back clean. Treating that as nothing references this is how an object in a web-proxy profile gets deleted.

Something the tool needed could not be read at all.

total_references here is a lower bound: some references were found and an unknown number were never looked at.

Two ways forward, either grant the profile read on the source that failed and ask again, or check that source by hand. What you cannot do is treat a lower bound as a count.

Without these distinctions the failure is silent and severe: an empty list from a refused read looks exactly like an empty list from a clean table. How the denial case is handled throughout.

It means the appliance’s own reference lookup answered, and said nothing points at this object. That is a far stronger claim than it used to be, and it is worth knowing exactly how strong.

It still does not mean deleting it is a good idea.

The honest framing: this tool now answers what does this appliance say points at this object, which is the right question and the one the web UI answers too. It does not replace knowing your own configuration.

There is a second kind of reference that no configuration read can see: things outside the firewall that depend on the object existing.

A monitoring system polling a VIP by name. A runbook that says add the host to DMZ-SERVERS. A FortiManager policy package that will push the object back, or flag the appliance as out of sync when it disappears. A colleague’s half-finished change.

safe_to_delete is a statement about the configuration, and the configuration is not the whole system.

Ask in this order, and the answer at each step tells you whether the next one matters.

Start with the object itself. find_references on the name, and read the verdict before anything else. object_not_found means fix the spelling and start over. indeterminate or no_references_in_checked_scopes mean the rest of this workflow is premature, because you do not yet know what you are looking at. referenced gives you both the answer and the list of what to fix first.

Follow the groups. Every name under groups gets its own find_references call, because membership is not expanded transitively. This is where indirect dependencies live, and it is the step most often skipped.

Check what else shares the value. search_config on the object’s IP or subnet finds other objects covering the same addresses, which is the common case where a duplicate was created years ago and half the policies use each. Deleting one and leaving the other looks clean and leaves the configuration inconsistent.

Check it is not doing something live. If the object is a subnet, ask get_arp_table or list_dhcp_leases whether anything is actually on it. An object with no references and no live hosts is genuinely dead. An object with no references and forty active devices behind it is a documentation gap, not cruft.

This server cannot make the change. It reads, and that is all it does — so the deletion happens in the web UI or the CLI, by a person, with the reference list in front of them.

That division is the point rather than a limitation. Why read-only is structural here.