Security notes
What is stored where, how secrets are kept out of output, TLS, the limits of operating modes and the risks of autonomous writes.
Security notes
What forti stores and where, how it keeps secrets out of output, and what its operating modes do and do not protect against.
What is stored where
| Data | Where | Protection |
|---|---|---|
| Profiles (host, port, VDOM, paths, mode, credential reference) | ~/.forti/config.json | 0600 file in a 0700 directory. No secrets: a config file containing one is rejected |
| API keys and certificate passwords (secret-store source) | macOS: login Keychain (service forti). Elsewhere: ~/.forti/credentials/ | Keychain ACL; or 0600 files that forti refuses to read when group or others can access them. Bound to the profile's host:port |
| API keys (env / command source) | not stored | only the variable name or the command's argv is saved |
Certificate passwords from forti cert create | ~/.forti/certs/<profile>/client.pass, or the secret store with --keychain | 0600 file in a 0700 directory, read through the file source (refused when group or others can access it); never printed |
| Client certificate keys and the private CA key | ~/.forti/certs/<profile>/client.p12 (encrypted), ca/ca.key | 0600 files in 0700 directories; the CA key never leaves the machine |
| Plans, approvals, audit log | ~/.forti/state/ | 0600 files. Plan files keep real values (apply needs them) |
| Pre-change configuration backups (critical plans) | ~/.forti/state/backups/<plan id>.conf | 0600 file in a 0700 directory, streamed to disk, never logged or printed; the 20 most recent are kept |
Paths and overrides: Configuration files.
forti's own credentials (API keys, certificate passwords) never appear in arguments, output, logs,
plans, audit records, error messages or the MCP client configuration. forti profile show and
forti doctor report where a key comes from and whether it resolves, never its value.
forti configure refuses a key as an argument value.
Device secrets are redacted too. FortiOS returns stored secrets in its encrypted form (ENC ...).
Admin passwords are hashes, but pre-shared keys and RADIUS/LDAP/TACACS+ secrets are reversibly
encrypted, by default with a key built into FortiOS (CVE-2019-6693) unless private-data-encryption
is enabled on the device. So:
- Reads (
forti get, forti-mcp'sforti_read) replace every string value starting withENC, and the value of every attribute whose name marks it as secret, with"<redacted>", at any depth.meta.redactedlists where (JSON pointers,*for list items, e.g./*/psksecret). An unset secret stays visibly unset (""ornull). This is the default for every caller, and forti-mcp cannot turn it off. Onlyforti get --revealshows the values, for a person who asks: it needs a terminal (or--json, chosen deliberately) and warns on stderr. - Plan diffs, request previews, audit records and error details redact the same attribute names
(password, psk, secret, private-key, key-string, api-key,
*-key, ...; checked against every attribute the FortiOS 7.4.8 schema types as a password). Audit records hold attribute names only, never values. - Plans refuse
"<redacted>"as a value, so copying a read object into a plan cannot overwrite a secret with the placeholder. - Stored plan files keep the real values, because apply writes them and compares the device's current state against them. That is why they are 0600.
Name-based redaction can miss an attribute a later FortiOS adds; ENC values are caught whatever
their name.
Stored keys are bound to their device
A key in the secret store is saved with the host:port of the profile it was configured for (the
Keychain item's generic attribute; the first line of a credential file). forti releases it only to a
profile that still points there. Otherwise it fails with config ("the stored key belongs to
host:port; run forti configure") and sends nothing. So another config.json that reuses a profile
name (a different FORTI_HOME or FORTI_CONFIG_DIR, for example one set by a repository's MCP
configuration) cannot send your key to another host. forti configure writes the binding, and
forti configure and forti profile set re-bind a kept key when you change the profile's host or
port; forti profile rename keeps the binding. Keys from env, command and file sources are not
stored, so they are not bound.
Other hardening
config.jsondecides each profile's mode and may name a credential command that forti runs, so it is refused when another user owns it or when group or others can write it (or its directory). Credential files, and files named by afilesource, are refused when they are symbolic links, owned by another user, or accessible by group or others.FORTI_HOME,FORTI_CONFIG_DIR,FORTI_STATE_DIRandFORTI_CACHE_DIRmust be absolute paths, so profiles, credentials and modes never depend on the working directory.forti cert createrunsopensslwith an argument vector, never a shell, and passes the certificate password to it as afile:reference, never in argv.- A credential command runs without a shell, with its standard input from
/dev/nulland only descriptors 0 to 2 inherited. fortiandforti-mcpdisable core dumps at startup, because their memory holds keys and device data.- Text output escapes terminal control characters in everything read from a device or a plan, so a crafted object name or comment cannot redraw the terminal (for example while you approve a plan).
- Before its first change, apply checks that
audit.jsonlcan be opened for appending; if it cannot, nothing is sent (config). If the record cannot be written after the changes, the result says so (audit_error) instead of dropping it silently.
TLS is always verified
The SDK always verifies the peer certificate and host name, and no option, environment variable or
config key turns that off. For a self-signed or private-CA device, give the profile the CA with
--ca-bundle, or use --resolve-ip to connect to an address while verifying the certificate's name.
Authentication failures are not retried
FortiOS locks an API user out after repeated authentication failures. forti configure and
forti doctor make one attempt and stop on HTTP 401/403 with advice: check the key, check that
this machine is within the API user's trusted hosts, and check its admin profile.
Operating modes
An API user can do exactly what its admin profile (accprofile), VDOM assignment and trusted hosts allow, whatever forti's settings are.
A profile's mode (read-only, the default; guarded; autonomous) is the operator's policy on top
of that boundary. It is stored in config.json, and the MCP server cannot change it. The modes, the
safeguards that apply in every mode that writes, and the limits of rollback plans and backups are in
Planning and applying changes.
Default
read-only
Plans can be written and previewed; every apply is refused. Suits monitoring, auditing and planning.
Middle ground
guarded
The agent prepares and validates changes, and a person approves each plan before it runs.
No approval
autonomous
Plans run without interactive approval. Suits a capable agent doing real administration, within a tightly scoped FortiOS account.
The rollback plan and the configuration backup both need a working management path to the device. If a change cuts off management access, forti cannot undo it. You need an independent path: the console port, an out-of-band management interface, or a device-side automatic revert. Plan for that before letting an agent change critical settings.
A configuration backup holds the device's complete configuration, including FortiOS-encrypted secrets, most of them reversible (see above). Keep the forti home on an encrypted disk (FileVault or LUKS).
forti keeps the 20 most recent backups (by modification time) and deletes older ones after each new backup. Copy any backup you want to keep longer somewhere safe, and delete ones you no longer need:
1rm ~/.forti/state/backups/<plan id>.conf
Choosing autonomous mode
Autonomous mode suits a capable agent doing real administration where approving every plan would defeat the purpose.
With autonomous mode, the agent can make any change the FortiOS API account is allowed to make, including changes that cut off your own management access. The safeguards make changes visible and verified, and reversible while the management path works, but they do not limit what may be changed. That limit is the account.
Scope the account before you choose autonomous mode:
- Give the API user an admin profile that grants only what the agent manages (for example
read-write on firewall objects and policy, read-only on system). Avoid
super_admin.forti doctorwarns when an autonomous profile appears to use a super_admin-class account. - Restrict the API user's trusted hosts to the machine the agent runs on.
- Use a VDOM-scoped account when the agent should manage only one VDOM.
- Keep an out-of-band path to the device (console or a separate admin account) when the agent may touch interfaces, routing or administrative access.
What a mode does and does not stop
A mode governs what the forti-mcp tools can do: nothing an MCP client sends can change a mode, approve a plan, or apply a plan in a read-only profile.
An agent that can also run shell commands as you (Claude Code's Bash tool, Codex's shell) could run
forti plan approve or forti configure --mode autonomous, or edit config.json and the approval
files, exactly as you can. forti uses no passphrase or terminal-only approval, by design.
For guarded mode to mean "a person approved this":
- keep the client's shell permission prompts on for
forticommands, or deny them outright (for Claude Code, in your settings, as shown below); - read the plan (
forti plan show <id>, whichforti plan approveprints first) before you approve it.
1{2 "permissions": {3 "deny": ["Bash(forti plan approve:*)", "Bash(forti configure:*)", "Bash(forti profile:*)"]4 }5}
The installed agent skill and the workspace AGENTS.md tell agents never to approve plans or change
modes, but that instruction is not enforcement. Neither are the deny rules of the
forti workspace: they keep
the agent's file tools away from credentials/, certificate keys and config.json, but a script the
agent runs opens files itself. The FortiOS account's privileges remain the boundary that holds in every case.