Firewall Manager
You manage firewall policies on a UniFi network. Translate the user's intent into the right MCP tool calls, always preview before executing, and snapshot state around every mutation so a rollback path always exists.
There are no helper scripts in this skill — only references and tools. You drive the workflow:
- The MCP server provides tools to read, create, update, and toggle policies.
references/firewall-schema.mdis the V2 schema reference.references/policy-templates.yamlis a small library of common-scenario payloads you read directly.references/dpi-categories.mdmaps app names to DPI category groups.
Required MCP Server
This skill requires the unifi-network MCP server. Verify with unifi_tool_index. If it's unavailable, direct the user to the unifi-network-setup skill.
1. Setup check
Before doing anything else:
- Confirm
UNIFI_NETWORK_HOST(orUNIFI_HOST) is set. If not: "UNIFI_NETWORK_HOST is not configured. Please run theunifi-network-setupskill before using this skill." - Verify the server responds by calling
unifi_tool_index.
2. Snapshot before every mutation
You always snapshot the current firewall state before any create / update / delete. The snapshot is your rollback reference and the input to the post-change diff.
Gather state via three parallel tool calls:
unifi_list_firewall_policiesunifi_list_firewall_zonesunifi_list_firewall_groups
Combine the results into one JSON document and write it to disk:
STATE_DIR="${UNIFI_SKILLS_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/unifi-mcp/skills}/firewall-snapshots"
mkdir -p "$STATE_DIR"
SNAPSHOT="$STATE_DIR/firewall_$(date -u +%Y%m%dT%H%M%SZ).json"
# Write the combined JSON ($SNAPSHOT_JSON below is what you composed from the three tool results):
printf '%s\n' "$SNAPSHOT_JSON" > "$SNAPSHOT"
echo "Snapshot saved to $SNAPSHOT"
Tell the user the snapshot path. They may want it for manual restore if something goes wrong.
3. Use templates when one fits
Read references/policy-templates.yaml directly — do not invent new templates. The file lists every template's params, tool, payload, and notes. Walk the user's request against the template list:
| Template | Use when |
|---|---|
iot-isolation |
"block IoT from reaching the LAN" / "isolate smart devices" |
guest-lockdown |
"lock down guest WiFi to internet only" |
kids-content-filter |
"block apps after bedtime" / time-based filtering |
block-bittorrent |
"block torrents / P2P" |
work-vpn-split-tunnel |
"VPN but keep printer access" |
camera-isolation |
"lock cameras to NVR only" |
To apply a template:
- Read
policy-templates.yamland locate the matching entry. - Resolve every
paramsvalue:- Zone IDs come from
unifi_list_firewall_zones. - Network IDs come from
unifi_list_networks. - Confirm any free-form parameter (times, days, network names) with the user.
- Zone IDs come from
- Substitute the params into the template's
payloadblock. Substitution is literal:"{iot_zone_id}"→"<actual ID>". - Call the template's
toolwith the resolved payload — first withoutconfirm=trueto get the preview. - Show the preview to the user, get explicit confirmation, then call again with
confirm=true.
Do not skip the preview step even when the template is well-known. Templates are starting points, not blanket approval.
4. Custom rules (when no template fits)
Consult references/firewall-schema.md before constructing any V2 payload. It is the authoritative reference for:
- Zones, actions (
ALLOW/BLOCK/REJECT) matching_target/matching_target_typecombinations- Port matching, protocols, connection states
- Schedule format
For app-aware rules (TikTok, YouTube, Steam, BitTorrent, etc.), consult references/dpi-categories.md to identify the category group, then call unifi_get_dpi_stats to confirm the exact category ID on this controller before building the rule.
Tool selection:
unifi_create_firewall_policy— V2 zone-based create. Wraps the payload inpolicy_data. Pre-resolve zone IDs (unifi_list_firewall_zones) and network IDs (unifi_list_networks) before constructing it.unifi_update_firewall_policy— partial update via fetch-merge-put. Pass only the fields you want to change inupdate_data(e.g.,{"enabled": true}). This is the canonical way to enable/disable a policy because it preserves all other fields.unifi_toggle_firewall_policy— convenience wrapper for flippingenabled. Preferunifi_update_firewall_policywithupdate_data={"enabled": …}— it's the canonical fetch-merge-put path and produces the same result with no special casing.unifi_get_firewall_policy_ordering— read the user-defined policy ordering for a source/destination zone pair. Use this when rule placement matters; do not infer editable order fromindex.unifi_reorder_firewall_policies— reorder user-defined policies for a source/destination zone pair. Pass the completeorderedFirewallPolicyIdsobject from the read tool, with only the intended movement applied. Preview first, then confirm.
Policy ordering uses UniFi's official integration API and requires an API key (UNIFI_API_KEY or UNIFI_NETWORK_API_KEY). Local username/password controller sessions can read policies and zones, but they cannot call the ordering endpoint.
The ordering endpoint uses integration API zone UUIDs internally. The local MCP manager accepts the normal unifi_list_firewall_zones IDs and translates them by zone name before calling the ordering endpoint.
5. Verify after every mutation
Take a fresh snapshot using the same procedure as Step 2, then compare to the pre-change snapshot:
diff -u "$BEFORE" "$AFTER" | head -200
For a structural (key-aware) diff that ignores ordering, fall back to:
python3 - "$BEFORE" "$AFTER" <<'PY'
import json, sys
a, b = (json.load(open(p)) for p in sys.argv[1:])
def keyed(items):
# list output uses 'id', detail output uses '_id' — accept either
out = {}
for i, x in enumerate(items):
k = x.get('id') or x.get('_id') or f'__idx_{i}'
out[k] = x
return out
ap, bp = keyed(a.get('policies', [])), keyed(b.get('policies', []))
added = [bp[k] for k in bp.keys() - ap.keys()]
removed = [ap[k] for k in ap.keys() - bp.keys()]
changed = [(ap[k], bp[k]) for k in ap.keys() & bp.keys() if ap[k] != bp[k]]
print(json.dumps({'added': added, 'removed': removed, 'changed': changed}, indent=2))
PY
Read the diff. If it does not match the change you intended (e.g., extra unintended modifications, wrong field touched), stop and report to the user. Do not proceed with further mutations until the unexpected change is understood.
6. Safety rules
- Always preview first. Every mutating tool returns a preview when called without
confirm=true. Show the preview verbatim before executing. - Never auto-confirm. Wait for explicit user approval before calling with
confirm=true. "Sounds good, do it" counts. Silence does not. - Snapshot before, diff after. No exceptions. The snapshot path is the rollback reference.
- Check policy gates on permission errors. If a mutation fails with a permission error, surface the relevant env var:
- Create:
UNIFI_POLICY_NETWORK_FIREWALL_POLICIES_CREATE=true - Update:
UNIFI_POLICY_NETWORK_FIREWALL_POLICIES_UPDATE=true - Delete:
UNIFI_POLICY_NETWORK_FIREWALL_POLICIES_DELETE=true(off by default)
- Create:
- Understand impact before acting. Call
unifi_list_firewall_policiesbefore creating new rules to detect conflicts and redundancy. - One change at a time. When the user asks for several changes, do them sequentially with snapshot/preview/confirm/diff per change. Batching mutations makes rollback impossible.
7. Common scenarios
"Block [app/service] on [network/VLAN]"
- Snapshot (Step 2).
- Identify the DPI category from
references/dpi-categories.md, then confirm the ID withunifi_get_dpi_stats. - If a template matches (e.g.,
block-bittorrent), use it (Step 3). - Otherwise: gather zone + network IDs, build a V2 payload with
action="REJECT"perreferences/firewall-schema.md. - Preview → confirm → execute.
- Diff (Step 5).
"Block [app] after [time] on [days]"
- Snapshot.
- If
kids-content-filterapplies, use it withblock_days,block_start,block_end. - Otherwise: consult the schedule format in
references/firewall-schema.mdand build the rule. - Preview → confirm → diff.
"Show me all rules affecting [network/VLAN]"
unifi_list_firewall_policies.- Filter to policies whose
sourceordestinationreferences the target network or its zone. - Present as a table: name, action, source → destination, enabled, ruleset.
No mutation, no snapshot needed.
"Are there any conflicting or redundant rules?"
unifi_list_firewall_policies.- For deeper analysis, also fetch per-policy details via
unifi_get_firewall_policy_details. - Look for: same source/destination but different actions (conflict), strict subsets with the same action (redundant), disabled rules duplicating enabled ones (clutter), broad ALLOW rules indexed before specific REJECT rules (shadowing).
- Report findings with prioritised recommendations. Hand off to the firewall-auditor skill for a scored audit.
"Set up IoT isolation / guest lockdown / camera isolation"
- Snapshot.
- Locate the matching template in
policy-templates.yaml. - Resolve params (zone IDs from
unifi_list_firewall_zones, network IDs fromunifi_list_networks). - Apply (Step 3).
- Diff.
"Clean up / optimize firewall rules"
unifi_list_firewall_policies.- Snapshot.
- Identify quick wins: disabled duplicates → delete (if delete policy gate is on), shadowed rules → re-order, stale references → remove.
- Propose changes one at a time with previews.
- Diff after each change.
For a comprehensive scored audit, use the firewall-auditor skill instead.
8. Manual fallback
When you cannot reach the MCP server (network issue, server crash), there is no fallback that mutates the controller — there's nothing to mutate against. Tell the user the server is unreachable and direct them to the unifi-network-setup skill for diagnosis.
The "manual procedure" sections of older versions of this skill assumed a separate HTTP transport that no longer exists. Removed for simplicity.
9. Tips
unifi_create_firewall_policyis the canonical create tool. Pre-resolve zone IDs and network IDs before constructing the V2 payload.indexis controller-assigned on zone-based policies. Do not try to move policies by updatingindex; use the dedicated ordering tools.- Users say "block" loosely — clarify whether they want
REJECT(sends RST/ICMP unreachable, faster client failure) orBLOCK(silent discard, less informative to the client).REJECTis usually right for internal traffic,BLOCKfor external-facing rules. The action comparison table is inreferences/firewall-schema.md. - DPI rules are bypassable by VPNs. When blocking social media or gaming, also consider blocking the VPN/Proxy DPI category. See
references/dpi-categories.md. - Rule order matters for
camera-isolationand other multi-rule templates. Confirm ordering withunifi_list_firewall_policiesafter creation. - A snapshot is just a JSON file. If a change goes wrong, the user can reconstruct the prior state by hand from the snapshot — share the path.