Troubleshooting¶
FortiOS error codes¶
FortiOS answers most user-fixable mistakes with HTTP 500 and a negative
error code in the body, not a 4xx. The integration surfaces these through
check_fortios_response() as a FortiOSAPIError carrying the code, so the
number below is what you will see in the Job log.
| Code | Means | Where it bites |
|---|---|---|
-3 |
Entry not found in datasource | Creating a static route whose destination is a named AddressObject that lacks allow-routing |
-23 |
Object is referenced | Deleting an AddressObject a policy or route still points at |
-173 |
Rejected by the XSS filter | Parentheses in a comment, or a list-form dstaddr on create |
-651 |
Invalid interface for this operation | Using a switch-parent interface such as internal as a policy endpoint |
FortiOS rejects a write¶
Symptom: error -3 / "entry not found in datasource" when a static
route pushes, even though the AddressObject plainly exists
Cause: FortiOS will not let a route reference an address object unless
that object has allow-routing enabled. The object existing is not enough.
Firewall policies do not need this, which is why the same object works fine
as a policy source and fails as a route destination.
Fix: Operator action required — the integration does not set this flag. On the FortiGate:
Then re-run the push Job.
Symptom: error -23 when deleting an address object
Cause: FortiOS refuses to delete an object something still references.
Fix: Remove the references first — check policies, address groups, VIPs, and static routes. Note the referring object may be one the integration created. There is no force-delete.
Symptom: error -173 on create or update, often on a record whose only
unusual feature is its comment text
Cause: FortiOS runs an XSS filter over POST bodies and rejects parentheses. The integration's own auto-generated comments use square brackets for this reason, but a comment typed into Nautobot by hand goes through unmodified.
Fix: Replace (text) with [text] in the Nautobot-side comment and
re-run. The same filter rejects the list-of-dict form of dstaddr on
create; the integration already uses the plain-string form, so you should
only meet that if you are calling the API yourself.
Symptom: error -651 when pushing a policy that uses internal
Cause: On the FortiWiFi/FortiGate desktop models internal is a
hardware-switch parent, not a policy endpoint.
Fix: Use the member ports (internal1 through internal7) or define a
zone containing them and reference the zone.
Symptom: A VAP delete is skipped or fails
Cause: FortiOS holds a circular dependency between a VAP and its quarantine interface that blocks removal over REST. This is a FortiOS limitation, not an integration bug.
Fix: Delete the VAP in the FortiGate web UI. The push Job's VAP DELETE phase is intentionally not implemented for this reason.
Something synced, but the value is wrong¶
These are the silent ones. Nothing errors; the data is just not what the appliance holds. Each was found against real hardware.
Symptom: An interface you created is missing the end of its name —
ssot_test_vlan100 arrives as ssot_test_vlan1
Cause: FortiOS truncates interface names at 15 characters. It returns HTTP 200 with no warning and then chops the tail.
Fix: Keep created interface names to 15 characters or fewer. A longer name will appear to succeed and then silently collide with anything sharing its first 15 characters.
Symptom: Every static route shows up in Nautobot as a blackhole route
Cause: FortiOS returns blackhole as the string "enable" or
"disable", not a boolean. bool("disable") is True in Python, so a
naive read flips every normal route to blackhole.
Fix: Already mitigated — the device adapter matches the string
explicitly rather than coercing. If you see this again, look for a new code
path that wraps a FortiOS field in bool(). Treat every FortiOS
boolean-looking field as a string until proven otherwise; this is the
single most repeated defect class in this integration.
Symptom: A route's destination is 0.0.0.0/0 in Nautobot, but the
FortiGate shows a specific named destination
Cause: When a route's destination is a named address object, FortiOS
writes the all-zeros sentinel into dst and puts the real answer in
dstaddr. Reading dst first turns every named-destination route into a
bogus default route.
Fix: Already mitigated — dstaddr wins whenever it is populated.
Symptom: A relational field such as dstaddr is empty in Nautobot on
one FortiGate and populated on another running different firmware
Cause: FortiOS 7.0.x returns some relational fields as a bare string;
7.2 and later return [{"name": "X"}]. Code that assumes one shape
silently produces an empty list against the other.
Fix: Already mitigated — _normalize_dstaddr_names() handles both. If
you add a new relational field, normalize it the same way rather than
indexing the list directly.
Symptom: The FortiGate Device in Nautobot has an empty serial number
Cause: The serial, firmware version, and build number are siblings of
results in the REST envelope, not fields inside it. Helpers that unwrap
straight to results discard them, which makes the serial look absent from
the API entirely.
Fix: Already mitigated since v3.2.5 — the device adapter reads the raw
response from cmdb/system/global rather than a helper that strips the
envelope.
Symptom: The FortiGate has a default route, but the static-route sync does not import it
Cause: Not a bug. A DHCP-acquired route is never written to
cmdb/router/static; it exists only in the runtime RIB. The config table
is genuinely empty.
Fix: None needed. The sync covers operator configuration intent
(cmdb/router/static) by design. To see runtime routes, use the Live
Status Job, which reads monitor/router/ipv4.
Authentication / connectivity¶
Symptom: Job log shows ImproperlyConfigured: ExternalIntegration 'X' has no secrets_group set
Fix: The ExternalIntegration's "Secrets Group" field is unset. Edit the integration in Extensibility → External Integrations and select the SecretsGroup you created.
Symptom: ImproperlyConfigured: SecretsGroup 'X' has neither a TOKEN secret nor a USERNAME+PASSWORD pair
Fix: The SecretsGroup exists but no Secret is associated with it, or the env var that the env-var-provider Secret points at isn't set in the Nautobot worker's environment. The error message includes the hint: "If you're using the environment-variable provider, verify the named env var is actually set inside the worker container."
To verify:
docker compose exec nautobot-worker env | grep FGT_
# or, on a bare-metal install:
sudo -u nautobot env | grep FGT_
Symptom: requests.exceptions.ReadTimeout partway through a sync, only in user/password auth mode
Cause: FortiOS enforces a per-admin concurrent-session limit (default
2–4). Each unwrapped REST call in user/pass mode triggers a fresh
POST /logincheck, exhausting the limit.
Fix: Already mitigated — the integration wraps API calls in
with build_client(ext) as fgt: so login happens once per sync. If you
still hit this, you're likely running multiple Jobs concurrently against
the same FortiGate admin user. Either:
- Create a dedicated API token (eliminates the
/logincheckround-trip entirely), or - Increase the FortiOS admin's
concurrentsession limit:config system admin / edit <admin> / set concurrent <N>
Validation errors during push¶
Symptom: KeyError: 'i' deep in a Django ValidationError traceback
Cause: This is a bug in nautobot-firewall-models.validators.validate_port —
its error template uses %(i)s but its params dict uses {"value": i},
so when the validator rejects an unexpected port form, Django can't even
stringify the error. The triggering condition is spaces in the port
string.
Fix: Already mitigated on push side — the integration converts FortiOS-style space-separated port lists to comma-separated before emitting them. If you're seeing this from a non-integration code path, report to firewall-models upstream.
Symptom: ValidationError: Value 'IP' is not a valid choice on service push
Cause: FortiOS reports services like OSPF as protocol: "IP" with a
protocol-number. firewall-models expects the IANA-named protocol
(OSPFIGP, GRE, etc.), not "IP".
Fix: Already mitigated — fortios_service_ports() in
utils/fortios.py translates protocol numbers to IANA names via the
IP_PROTOCOL_NUMBER_TO_NAME table. If you see a service with an unmapped
protocol number, add it to that table and re-run.
Sync produces unexpected diffs¶
Symptom: Re-running a Job that previously synced cleanly now shows
update actions for records you haven't touched
Possible causes:
- Member ordering — should not happen anymore (both adapters sort members), but if you see this on Policy/AddressGroup/ServiceGroup members, file a bug.
- Hand-edits in Nautobot — additive mode preserves them; subsequent
syncs will show their differences as updates. To restore the
FortiGate's view of truth, run the pull Job with
delete_records_missing_from_source=True(DANGEROUS — review the dry-run diff first). - FortiGate config drift — the FortiGate has changed since the last sync. This is the expected case — the diff shows you what changed.
Live status Job¶
Symptom: "Wifi clients: 0" but you know clients are connected
Possible causes:
- The FortiGate's
monitor/wifi/clientendpoint scope is different from the requested VDOM. Try settingvdomto whatever VDOM the wireless config lives in. - The REST API admin profile lacks read access to
monitor/*. Verify in the FortiGate UI under the admin's profile permissions. - On the FortiWiFi-61E specifically, clients sometimes take 10–30s to
appear in
monitor/wifi/clientafter association. Re-run the Job.
Live e2e harnesses¶
If make e2e-live-firewall or make e2e-live-wireless fails:
- Check connectivity first:
docker compose exec nautobot-web curl -ksI --max-time 5 https://<fortigate-ip>/api/v2/monitor/system/status - Check the auth setup:
make seedre-prints the credential stack status; confirm env vars are seen as✓ set. - Inspect the actual data shape: many bugs come from FortiOS
versions emitting fields the integration didn't expect. The e2e
scripts wrap exceptions and print the offending record's MAC/name
when possible. See the data-quirk diagnostic patterns in
../user/external_interactions.md.
"Nothing happened" when I clicked Run Job¶
Nautobot Jobs are disabled by default after installation. To enable:
- Navigate to Extensibility → Jobs
- Find the Job (e.g. "FortiGate -> Nautobot (firewall)")
- Click the pencil icon → check Enabled → save
- Now the Run Job button is active