FortiGate Operations — the device plane
MCP Server
- Server:
fortinet-mcp (NetClaw-authored, spec 080 / roadmap R3)
- Command:
$FORTINET_MCP_CMD
- Transport: stdio · FortiOS REST API, bearer token
- Requires:
FORTIGATE_HOST, FORTIGATE_API_TOKEN
- Mode: read-only — this skill has no write path at all
The distinction this skill exists to protect
A FortiGate knows what it is doing. It does not know what it was supposed to do.
| Question |
Plane |
Skill |
| "Is the tunnel up? What's in the routing table?" |
device |
this skill |
| "What policy is intended across the estate?" |
manager |
fortimanager-ops |
| "Has anything ever matched this rule?" |
analyzer |
fortianalyzer-ops |
| "Run a raw FortiOS CLI command" |
CLI |
multivendor-raw-cli (spec 076) |
If a device does not answer, this skill reports that it did not answer. It never
substitutes FortiManager's intended configuration as though it were observed
state — that would turn "the box is unreachable" into a confident, wrong
description of a box nobody can see.
Tools (6, all read-only)
| Tool |
What it answers |
fgt_system_status |
Hostname, serial, version, HA mode, which member answered |
fgt_list_interfaces |
Interfaces: link, addressing, speed, error counters, per VDOM |
fgt_get_routes |
Routing table as observed, optional protocol filter |
fgt_vpn_tunnels |
IPsec tunnels — phase 1 and phase 2 separately |
fgt_get_policies |
Firewall policy as running on the device |
fgt_compare_with_manager |
Divergence between intent and observed state |
Phase 1 and phase 2 are reported separately, always
This is the single most important behaviour here.
A tunnel with phase 1 up and phase 2 down is neither "up" nor "down". It is a
specific and common fault — usually a proxy-ID or selector mismatch — where IKE
negotiated fine and no traffic can actually pass. Collapsing the two into one
status field destroys the only signal that distinguishes it from a healthy tunnel
or a dead one.
fgt_vpn_tunnels therefore returns phase1_status, phase2_status, and
phase2_selectors[] per selector pair, because one down selector out of five is
still a fault worth naming.
Every response carries its plane and scope
{ "plane": "device", "scope": {"device": "FGVMEVS9GWUAOMBD", "vdom": "root"},
"source": "...", "outcome": "ok", "data": {...}, "notes": [] }
Scope is mandatory. A figure without its VDOM is ambiguous on a multi-VDOM unit,
so a response that cannot name its scope is returned as an error rather than as an
unqualified result.
Workflow: "is the tunnel up?"
fgt_vpn_tunnels — read both phases.
- Phase 1 down → IKE/peer/PSK/routing problem. Check
fgt_get_routes for a path
to the remote gateway.
- Phase 1 up, phase 2 down → selector/proxy-ID mismatch. Inspect
phase2_selectors[] for which pair failed.
- Both up but no traffic → not a tunnel problem. Move to policy
(
fgt_get_policies) or to the analyzer plane for whether anything matched.
Workflow: out-of-band change detection
fgt_compare_with_manager with the ADOM and package.
only_in_device → rules on the box that are absent from the policy package.
Someone changed the firewall directly. This is the highest-value finding this
skill produces and it is invisible from either plane alone.
only_in_manager → package not installed since those rules were added.
- If either plane is unreachable, the tool refuses to compare and names the plane
that failed. A half-comparison would be worse than none.
Notes on evaluation-licensed labs
A FortiGate-VM evaluation licence caps the unit at 1 vCPU, 2 GB RAM, 3 interfaces,
3 routes and 3 firewall policies. A small ruleset on such a device is a lab limit,
not the estate's real posture — fgt_get_policies says so in its notes.
An unlicensed FortiGate refuses REST authentication entirely: every request
returns 401 regardless of token validity, trusthost, or admin profile. If every call
fails with auth_expired, check get system status for License Status: Valid
before suspecting the token.
Integration with other skills
| Skill |
How they compose |
fortimanager-ops |
The intent this device is measured against |
fortianalyzer-ops |
Whether traffic actually matched what is configured here |
fwrule-analyzer |
Feed fgt_get_policies output to its FortiOS parser |
multivendor-raw-cli |
Raw CLI (spec 076). Use when you need command output, not structure |
pyats-troubleshoot |
Correlate firewall path findings with routing/device state elsewhere |
gait-session-tracking |
Every operation here is GAIT-audited automatically |
Important rules
- Read-only. This skill cannot change a FortiGate. Policy changes go through
fortimanager-ops and its two gates.
- Never report phase 1 and phase 2 as one status.
- Never fill an unreachable device's silence with manager data.
- Always carry the VDOM.
1---
2name: fortigate-ops
3description: "FortiGate device operations — system status, interfaces, routing, IPsec VPN tunnel state with phase 1 and phase 2 reported separately, HA member identification, per-VDOM scoping, and manager-vs-device drift detection. Use when asking what a FortiGate is ACTUALLY doing right now, whether a tunnel is up, or whether the device matches FortiManager's intent."
4version: 1.0.0
5license: Apache-2.0
6tags: [fortinet, fortigate, fortios, firewall, vpn, ipsec, vdom, ha, security, multi-vendor]
7user-invocable: true
8metadata:
9 { "openclaw": { "requires": { "bins": ["python3"], "env": ["FORTINET_MCP_CMD", "FORTIGATE_HOST", "FORTIGATE_API_TOKEN"] } } }
10---
11
12# FortiGate Operations — the device plane
13
14## MCP Server
15
16- **Server**: `fortinet-mcp` (NetClaw-authored, spec 080 / roadmap R3)
17- **Command**: `$FORTINET_MCP_CMD`
18- **Transport**: stdio · FortiOS REST API, bearer token
19- **Requires**: `FORTIGATE_HOST`, `FORTIGATE_API_TOKEN`
20- **Mode**: read-only — this skill has no write path at all
21
22## The distinction this skill exists to protect
23
24**A FortiGate knows what it is doing. It does not know what it was supposed to do.**
25
26| Question | Plane | Skill |
27|---|---|---|
28| "Is the tunnel up? What's in the routing table?" | device | **this skill** |
29| "What policy is *intended* across the estate?" | manager | `fortimanager-ops` |
30| "Has anything ever *matched* this rule?" | analyzer | `fortianalyzer-ops` |
31| "Run a raw FortiOS CLI command" | CLI | `multivendor-raw-cli` (spec 076) |
32
33If a device does not answer, this skill reports that it did not answer. **It never
34substitutes FortiManager's intended configuration as though it were observed
35state** — that would turn "the box is unreachable" into a confident, wrong
36description of a box nobody can see.
37
38## Tools (6, all read-only)
39
40| Tool | What it answers |
41|---|---|
42| `fgt_system_status` | Hostname, serial, version, HA mode, **which member answered** |
43| `fgt_list_interfaces` | Interfaces: link, addressing, speed, error counters, per VDOM |
44| `fgt_get_routes` | Routing table as observed, optional protocol filter |
45| `fgt_vpn_tunnels` | IPsec tunnels — **phase 1 and phase 2 separately** |
46| `fgt_get_policies` | Firewall policy as running on the device |
47| `fgt_compare_with_manager` | Divergence between intent and observed state |
48
49## Phase 1 and phase 2 are reported separately, always
50
51This is the single most important behaviour here.
52
53A tunnel with **phase 1 up and phase 2 down** is neither "up" nor "down". It is a
54specific and common fault — usually a proxy-ID or selector mismatch — where IKE
55negotiated fine and no traffic can actually pass. Collapsing the two into one
56`status` field destroys the only signal that distinguishes it from a healthy tunnel
57or a dead one.
58
59`fgt_vpn_tunnels` therefore returns `phase1_status`, `phase2_status`, and
60`phase2_selectors[]` per selector pair, because one down selector out of five is
61still a fault worth naming.
62
63## Every response carries its plane and scope
64
65```jsonc
66{ "plane": "device", "scope": {"device": "FGVMEVS9GWUAOMBD", "vdom": "root"},
67 "source": "...", "outcome": "ok", "data": {...}, "notes": [] }
68```
69
70**Scope is mandatory.** A figure without its VDOM is ambiguous on a multi-VDOM unit,
71so a response that cannot name its scope is returned as an error rather than as an
72unqualified result.
73
74## Workflow: "is the tunnel up?"
75
761. `fgt_vpn_tunnels` — read **both** phases.
772. Phase 1 down → IKE/peer/PSK/routing problem. Check `fgt_get_routes` for a path
78 to the remote gateway.
793. Phase 1 up, phase 2 down → selector/proxy-ID mismatch. Inspect
80 `phase2_selectors[]` for which pair failed.
814. Both up but no traffic → not a tunnel problem. Move to policy
82 (`fgt_get_policies`) or to the analyzer plane for whether anything matched.
83
84## Workflow: out-of-band change detection
85
861. `fgt_compare_with_manager` with the ADOM and package.
872. **`only_in_device`** → rules on the box that are absent from the policy package.
88 Someone changed the firewall directly. This is the highest-value finding this
89 skill produces and it is invisible from either plane alone.
903. **`only_in_manager`** → package not installed since those rules were added.
914. If either plane is unreachable, the tool refuses to compare and names the plane
92 that failed. A half-comparison would be worse than none.
93
94## Notes on evaluation-licensed labs
95
96A FortiGate-VM evaluation licence caps the unit at **1 vCPU, 2 GB RAM, 3 interfaces,
973 routes and 3 firewall policies**. A small ruleset on such a device is a lab limit,
98not the estate's real posture — `fgt_get_policies` says so in its notes.
99
100An **unlicensed** FortiGate refuses REST authentication entirely: every request
101returns 401 regardless of token validity, trusthost, or admin profile. If every call
102fails with `auth_expired`, check `get system status` for `License Status: Valid`
103before suspecting the token.
104
105## Integration with other skills
106
107| Skill | How they compose |
108|---|---|
109| `fortimanager-ops` | The intent this device is measured against |
110| `fortianalyzer-ops` | Whether traffic actually matched what is configured here |
111| `fwrule-analyzer` | Feed `fgt_get_policies` output to its FortiOS parser |
112| `multivendor-raw-cli` | Raw CLI (spec 076). Use when you need command output, not structure |
113| `pyats-troubleshoot` | Correlate firewall path findings with routing/device state elsewhere |
114| `gait-session-tracking` | Every operation here is GAIT-audited automatically |
115
116## Important rules
117
118- **Read-only.** This skill cannot change a FortiGate. Policy changes go through
119 `fortimanager-ops` and its two gates.
120- **Never report phase 1 and phase 2 as one status.**
121- **Never fill an unreachable device's silence with manager data.**
122- **Always carry the VDOM.**