Skip to content

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:

config firewall address
    edit "<object-name>"
        set allow-routing enable
    next
end

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 /logincheck round-trip entirely), or
  • Increase the FortiOS admin's concurrent session 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:

  1. Member ordering — should not happen anymore (both adapters sort members), but if you see this on Policy/AddressGroup/ServiceGroup members, file a bug.
  2. 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).
  3. 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:

  1. The FortiGate's monitor/wifi/client endpoint scope is different from the requested VDOM. Try setting vdom to whatever VDOM the wireless config lives in.
  2. The REST API admin profile lacks read access to monitor/*. Verify in the FortiGate UI under the admin's profile permissions.
  3. On the FortiWiFi-61E specifically, clients sometimes take 10–30s to appear in monitor/wifi/client after association. Re-run the Job.

Live e2e harnesses

If make e2e-live-firewall or make e2e-live-wireless fails:

  1. Check connectivity first: docker compose exec nautobot-web curl -ksI --max-time 5 https://<fortigate-ip>/api/v2/monitor/system/status
  2. Check the auth setup: make seed re-prints the credential stack status; confirm env vars are seen as ✓ set.
  3. 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:

  1. Navigate to Extensibility → Jobs
  2. Find the Job (e.g. "FortiGate -> Nautobot (firewall)")
  3. Click the pencil icon → check Enabled → save
  4. Now the Run Job button is active