Manage Sophos Switch via CLI, local REST API, and Fusion API
The local CLI and local REST API access one Sophos Switch directly. The Sophos Fusion Switch Management API is separate: it uses tenant-level service-principal credentials and distributes central policies to switches. This runbook identifies the applicable identity, base URL, and validation for both paths.
Scope and safe preliminary decisions
This runbook covers the following:
- local CLI access over an approved device-management path, particularly SSH;
- orientation and diagnosis in the CLI;
- signing in to the device-local REST API;
- the lifecycle of the session-specific bearer token;
- a documented, non-modifying API test with
GET /api/ports; - safe validation, troubleshooting, rollback, and session termination.
It deliberately does not provide a complete catalog of API endpoints or CLI commands. The central documentation provides guidance, but you must verify availability on the target device before using the API. Paths shown there, such as /ports, are relative to the server base /api, so the full request path is /api/ports. CLI commands, modes, and parameters can also vary by model and firmware. See Firmware and model variability for the required checks.
Decide the following before access:
- Is Sophos Fusion or local management the system of record?
- Which switch, model, firmware version, and management IP are affected?
- Is read-only access sufficient, or is an approved change required?
- What are the current state, success criteria, and rollback procedure?
- Is there an independent management path if the change interrupts normal access?
Keep identities and roles separate
The accounts under People are local accounts of the switch:
| Local privilege type | Rights on the switch | Typical use |
|---|---|---|
| Admin | View and change all switch functions | Approved local administration and write API requests |
| User | View settings, but do not change them | Diagnostics and verification according to the principle of least privilege |
These local roles are not the same as administrator roles in Sophos Fusion. A Fusion role does not automatically grant local CLI or API rights, and local credentials are not Fusion credentials. Send the credentials of a local switch account to /api/system/login.
Local accounts are managed in the local interface under People:
- Select Add to create an account, or choose Edit next to an account.
- Set Username, Password, and Privilege type.
- Select Privilege type as either Admin or User.
- Save with Apply.
A local password must meet all of the following requirements:
- at least 10 and not more than 32 characters;
- at least one letter and a number;
- at least one of these special characters:
@ ~ % * # + - =.
A local Admin account can change the passwords of other accounts, but not the password of the default admin account. Only the admin account itself or Sophos Fusion can change that password. For routine operation, use individual local accounts instead of a shared admin account.
Prepare access
Before a session:
- Check management IP, model, firmware status, location and serial number against the change ticket.
- Allow access only from an administrative management network and from an authorized admin host.
- Do not make management services accessible from user VLANs or the Internet.
- Check the time source on the switch and administrator host; incorrect time complicates log and API analysis.
- For a change, have a current configuration backup and an independent recovery path available.
- For Fusion-managed switches, preserve the central target state and explicitly document the local exception.
- Never store credentials or bearer tokens in ticket text, chats, screenshots, shell history, or source code.
Replace the following placeholders:
| Placeholder | Meaning |
|---|---|
<Switch-IP address> | Management IP or a trusted management hostname of the target device |
<LOCAL-USERNAME> | Local switch account, not a Fusion user |
your-password | Local password; a placeholder only, never a real password |
xxxxxxxx.yyyyyyyy.zzzzzz | Masked example of a bearer token, not a real token |
Use CLI safely
Connect to the switch
SSH is one of the local management services. It must be configured on the installed firmware and accessible from the management network. A general client command is:
ssh <LOCAL-USERNAME>@<Switch-IP-address>
This is a client-side example: replace <LOCAL-USERNAME> and <Switch-IP-address> completely, including the angle brackets. Before accepting the host key, compare its fingerprint through an independent, trusted channel. Never ignore a changed-host-key warning; first rule out device replacement, a factory reset, an IP conflict, or a possible man-in-the-middle attack.
If the model has a physical console port, it can provide an independent maintenance path. Use the connection and serial parameters for that model; do not copy them from another Sophos Switch model.
Orienting in the CLI
First, note the current prompt and therefore the current command mode. Do not assume that a command is available in every mode. The documented help features and keys are:
| Input | Effect |
|---|---|
? | List available commands |
TAB | Complete the command |
| Arrow up / down | Show previously executed commands |
| Left / right arrow | Move within the current line |
Backspace or Ctrl + H | Delete a character |
History | Show the command history |
Q | Close the current output and return to the switch prompt |
Check capitalization and exact availability with ? or TAB on the target device. Q closes paginated or active output; it does not end the SSH session.
Secure CLI sequence
- Start with a local User account when read-only access is sufficient.
- Verify the target device, prompt, and command mode.
- Use
?to display the commands available in that mode. - Begin with status or display operations only.
- Before a change, capture the complete current state and exact rollback procedure.
- Make only one technical change at a time and verify it immediately.
- Return to the prompt when output is page-by-page with
Q. - End the session cleanly with the exit or logout command shown by
?on the target device, then confirm that the SSH connection has closed.
Treat the command history as sensitive information: it can contain management addresses, usernames, or entered parameters. Never enter passwords or tokens as CLI parameters unless the switch prompts for them interactively without displaying them.
REST API: documented login
The API is available over HTTPS at the switch’s management address. The switch creates a new bearer token for every session. When the current session ends, you must obtain a new token.
The documented login uses PATCH /api/system/login. The following example preserves the published syntax while using neutral placeholders:
curl -k https://<Switch-IP address>/api/system/login -X PATCH -H 'Content-Type:application/json' -d '{"user":"admin","password":"your-password"}'
A successful example response has this structure:
{
"restful_res": {
"token": "xxxxxxxx.yyyyyyyy.zzzzzz",
"utctimestamp": "##########",
"timeout": 900,
"errCode": 0,
"message": "OK"
}
}
The following points apply:
tokenis a secret with the same protection requirements as a password.utctimestampandtimeoutare part of the specific session.- The documented example response shows
timeout: 900; the static description does not specify the unit. For automation, check its meaning in the published API help, validate it for the firmware in use, and do not hard-code it. errCode: 0andmessage: "OK"identify the successful response shown. Check the HTTP status as well.- Request a new token after the session ends or a rejected, expired session; do not reuse an old token.
Hardened cURL pattern
The following pattern keeps the password and token out of process arguments, verifies the TLS certificate, and does not write a secrets file. It requires curl, jq, and a shell that supports here strings:
read -r -p 'Local switch username: ' SW_USER
read -r -s -p 'Local switch password: ' SW_PASSWORD
printf '\n'
LOGIN_RESPONSE="$({
printf '%s\n%s\n' "$SW_USER" "$SW_PASSWORD" |
jq -Rn '[inputs] | {user: .[0], password: .[1]}' |
curl --disable --silent --show-error --fail-with-body \
--cacert /path/to/switch-ca.pem \
--request PATCH \
--header 'Content-Type:application/json' \
--data-binary @- \
'https://<Switch-IP-address>/api/system/login'
})"
unset SW_PASSWORD
if [ "$(jq -r '.restful_res.errCode' <<<"$LOGIN_RESPONSE")" != "0" ]; then
printf '%s\n' 'API login failed.' >&2
unset LOGIN_RESPONSE SW_USER
exit 1
fi
SW_TOKEN="$(jq -er '.restful_res.token' <<<"$LOGIN_RESPONSE")" || exit 1
unset LOGIN_RESPONSE
case "$SW_TOKEN" in
''|*[!A-Za-z0-9._~+/=-]*)
printf '%s\n' 'API login did not return a valid bearer token.' >&2
unset SW_TOKEN SW_USER
exit 1
;;
esac
Replace /path/to/switch-ca.pem and <Switch-IP-address>. Use the hostname for which the certificate was issued. In an interactive shell, first ensure that debug tracing such as set -x is disabled. Do not expose variables through env, export, debug output, or core dumps.
REST API: request and verification
The documented sample call reads the ports and passes the token in the Authorization header:
curl -k https://<Switch IP Address>/api/ports -H 'authorization:Bearer <Token>'
<Token> is deliberately only a placeholder for the bearer token. This published example is therefore non-executable: do not replace <Token> with the real token or enter such a command in the shell history. The executable call below instead uses the SW_TOKEN variable that was set during login.
For a real session, use the protected token and certificate verification. --config - reads the curl configuration from standard input, so the Authorization header is not passed as a process argument and no temporary file is created:
printf 'header = "Authorization: Bearer %s"\n' "$SW_TOKEN" |
curl --disable --silent --show-error --fail-with-body \
--config - \
--cacert /path/to/switch-ca.pem \
'https://<Switch-IP-address>/api/ports'
GET /api/ports is the appropriate first function test because the documented call requests status instead of demonstrating a configuration change. The test is only successful if:
- TLS verification and the connection succeed;
- no HTTP error is returned;
- the response is syntactically and technically plausible;
- the port count, port names, and expected states match the intended target device;
- no access data or tokens appear in the output or in logs.
curl --fail-with-body returns a failure status for HTTP errors while retaining the response body for local diagnosis. Before sharing that response, check it for tokens, addresses, serial numbers, and other internal data.
Control write API requests
Make write requests only as approved, limited, and reversible changes. Do not reuse a payload from another model, firmware version, or old script without validation.
For each write request:
- Check the method, path, parameters, data types, and response schema in the target device’s Swagger/OpenAPI schema, and confirm availability for its running firmware.
- Capture the affected state immediately beforehand with an appropriate read operation and store it securely.
- Send only the minimum required fields; do not guess unknown defaults.
- Limit the request to one switch and a small, reversible scope.
- Evaluate HTTP status and application-specific fields such as
errCodeandmessage. - Confirm the resulting state with an independent
GETand, where relevant, a functional test. - Stop if the result differs from expectations; do not send further changes in a retry loop.
A successful HTTP connection alone does not prove a successful change. Similarly, a plausible JSON body does not prove that the intended data path continues to work. For example, port, VLAN or management changes must also be tested from the affected network segment.
Manage bearer tokens securely throughout their lifecycle
- Generate: Obtain a new token for each API session through
PATCH /api/system/login. - Verify: Check the HTTP result,
errCode,message, token field, and session values without printing the token. - Use: Send the token only in the
Authorization: Bearer <token>header and only to the intended switch.<Token>is a non-executable placeholder; the executable examples generate the header fromSW_TOKEN. - Limit: Do not export, persist, or share tokens, or write them to files, Git, CI logs, or tickets. Use a separate controlled session for each parallel job.
- Renew: After the session ends or the token is rejected, do not continue with the same token; establish a new session. Avoid endless automatic login loops.
- Close: Invoke the documented authenticated logout operation,
PATCH /api/system/logout. Here, too, the header reaches curl through standard input rather than through process arguments:
printf 'header = "Authorization: Bearer %s"\n' "$SW_TOKEN" |
curl --disable --silent --show-error --fail-with-body \
--config - \
--cacert /path/to/switch-ca.pem \
--request PATCH \
'https://<Switch-IP-address>/api/system/logout'
- Discard locally: Remove the local variables after logout. If logout is no longer possible because the connection was interrupted or the session is already invalid, still discard the secrets locally and verify session termination as specified for the firmware in use:
unset SW_TOKEN SW_USER LOGIN_RESPONSE SW_PASSWORD
- Check: Inspect shell history, temporary files, and job logs for accidentally exposed secrets. Treat an exposed token as compromised, end the session, and do not send any further requests with it.
Tenant-level Sophos Fusion Switch Management API
This section does not use https://<Switch-IP-address>/api/.... The Fusion API authenticates a service principal against the regional Sophos Fusion (formerly Sophos Central) host and acts on the specified tenant. Local People accounts, Admin/User roles, local session tokens, and the device Swagger schema do not apply.
Prerequisites, roles, and credentials
The switch must be registered in the correct tenant and managed by Sophos Fusion. The executable workflow below applies only to direct API credentials issued for that tenant (client_id and client_secret). Only a Super Admin of the direct tenant can create them under Global Settings > Access Control > API Credentials; the assigned service-principal role must permit the required reads and writes. Do not use Partner or Enterprise credentials with these shell examples. For those credentials, first follow the separate tenant-selection procedure in Manage Sophos Fusion API credentials securely. Return to this workflow only after credentials have been issued and validated specifically for the selected direct tenant.
Keep the secret and JWT in a secret store, never in scripts, tickets, shell history, or CI output. You need curl, jq, Bash, an approved change window, and a recorded tenant ID, region, current list, complete target list, and rollback plan. API credentials do not replace a license or Support Subscription.
Authenticate the service principal and discover the regional host
The identity provider (IDP) issues the JWT through POST https://id.sophos.com/api/v2/oauth2/token. For the direct-tenant credentials required here, GET https://api.central.sophos.com/whoami/v1 returns id and apiHosts.dataRegion. The workflow stops if whoami does not return a tenant identity. Never send a partner or organization ID as X-Tenant-ID; do not guess the regional host or copy it from another tenant.
read -r -p 'Service principal client ID: ' SP_CLIENT_ID
read -r -s -p 'Service principal client secret: ' SP_CLIENT_SECRET
printf '\n'
TOKEN_RESPONSE="$({
jq -rn --arg id "$SP_CLIENT_ID" --arg secret "$SP_CLIENT_SECRET" \
'"grant_type=client_credentials&client_id=\($id|@uri)&client_secret=\($secret|@uri)&scope=token"' |
curl --disable --silent --show-error --fail-with-body \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-binary @- \
'https://id.sophos.com/api/v2/oauth2/token'
})"
unset SP_CLIENT_SECRET
FUSION_TOKEN="$(jq -er '
select(.token_type == "bearer") |
.access_token | select(type == "string" and length > 0)
' <<<"$TOKEN_RESPONSE")" || exit 1
unset TOKEN_RESPONSE
WHOAMI_RESPONSE="$(
printf 'header = "Authorization: Bearer %s"\n' "$FUSION_TOKEN" |
curl --disable --silent --show-error --fail-with-body \
--config - \
'https://api.central.sophos.com/whoami/v1'
)"
UUID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
FUSION_TENANT_ID="$(jq -er --arg re "$UUID_RE" \
'select(.idType == "tenant") | .id | select(type == "string" and test($re))' \
<<<"$WHOAMI_RESPONSE")" || exit 1
FUSION_DATA_REGION="$(jq -er \
'.apiHosts.dataRegion | select(type == "string" and test("^https://api-[a-z0-9-]+\\.central\\.sophos\\.com$"))' \
<<<"$WHOAMI_RESPONSE")" || exit 1
unset WHOAMI_RESPONSE UUID_RE
Every Switch request requires both the Authorization: Bearer <token> and X-Tenant-ID: <tenant-id> headers, together with the discovered <data-region> host. The documented success statuses are 200 or 201; they prove only that the request was accepted.
Read and completely replace MAC filters safely
GET /switch/v1/settings/mac-filtering reads the tenant-wide list of blocked MAC addresses. PUT /switch/v1/settings/mac-filtering does not append: macAddresses must contain the complete desired list, which replaces the existing list. {"macAddresses":[]} clears every blocked entry.
The example adds a synthetic, locally administered 02: address. Do not publish a real client MAC address. The working files contain operational data and must be protected.
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-before.json
jq -e '.macAddresses | type == "array"' mac-filter-before.json >/dev/null || exit 1
jq '{macAddresses: (.macAddresses + ["02:00:00:00:00:51"] | unique)}' \
mac-filter-before.json > mac-filter-desired.json
jq -S '.macAddresses' mac-filter-before.json mac-filter-desired.json
Write only after the complete diff has been approved. Between capturing the task baseline and saving the uniquely correlated task ID, no other macFilters change may run in the tenant:
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/tasks?type=macFilters&pageSize=50&pageTotal=true" \
> mac-filter-tasks-before.json
jq -e '
(.items | type == "array") and
(.pages.current == 1) and
(.pages.total >= 0) and (.pages.total <= 1) and
((.items | length) <= 50)
' mac-filter-tasks-before.json >/dev/null || exit 1
CHANGE_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
--request PUT \
--header 'Content-Type: application/json' \
--data-binary @mac-filter-desired.json \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-put-response.json
Read-back, task polling, and validation
A successful PUT does not prove that every switch applied the policy. Read back the exact setting, then query GET /switch/v1/tasks. Tasks are deleted after 30 days and are not a permanent audit archive. The documented filters are type, pageSize, and pageTotal. Because this workflow does not assume another documented page parameter, it processes no more than one complete page of 50 tasks and fails when pages.total > 1 instead of silently ignoring later pages.
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-after.json
jq -e '.macAddresses | type == "array"' mac-filter-after.json >/dev/null || exit 1
diff -u \
<(jq -S '.macAddresses' mac-filter-desired.json) \
<(jq -S '.macAddresses' mac-filter-after.json) || exit 1
CHANGE_TASK_ID=''
CHANGE_TASK_DONE=false
for CHANGE_POLL_ATTEMPT in $(seq 1 30); do
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/tasks?type=macFilters&pageSize=50&pageTotal=true" \
> mac-filter-tasks.json
jq -e '
(.items | type == "array") and
(.pages.current == 1) and
(.pages.total >= 0) and (.pages.total <= 1) and
((.items | length) <= 50)
' mac-filter-tasks.json >/dev/null || exit 1
if [ -z "$CHANGE_TASK_ID" ]; then
jq -e -s --arg started "$CHANGE_STARTED_AT" '
def epoch: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
[.[0].items[].id] as $before |
[.[1].items[] |
select(
.type == "macFilters" and
(.id as $id | ($before | index($id) | not)) and
((.createdAt | epoch) >= ($started | epoch)) and
((.updatedAt | epoch) >= ($started | epoch))
)
] | if length > 1 then error("multiple matching tasks") else . end
' mac-filter-tasks-before.json mac-filter-tasks.json \
> mac-filter-task-matches.json || exit 1
if jq -e 'length == 1' mac-filter-task-matches.json >/dev/null; then
CHANGE_TASK_ID="$(jq -er '.[0].id | strings | select(length > 0)' \
mac-filter-task-matches.json)" || exit 1
fi
fi
if [ -n "$CHANGE_TASK_ID" ]; then
jq -e --arg id "$CHANGE_TASK_ID" '
[.items[] | select(.id == $id)] |
select(length == 1) | .[0]
' mac-filter-tasks.json > mac-filter-task-current.json || exit 1
if jq -e '.status.pending > 0' mac-filter-task-current.json >/dev/null; then
:
else
jq -e '
(.status.total | type == "number") and (.status.total > 0) and
(.status.pending == 0) and (.status.failed == 0) and
((.status.noSupportSubscription // 0) == 0) and
(.status.succeeded == .status.total) and
(.switches | type == "array") and
((.switches | length) == .status.total) and
all(.switches[];
(.id | type == "string" and length > 0) and
(.status == "succeeded") and
(.error == null)
)
' mac-filter-task-current.json >/dev/null || exit 1
CHANGE_TASK_DONE=true
break
fi
fi
[ "$CHANGE_POLL_ATTEMPT" -lt 30 ] && sleep 10
done
[ "$CHANGE_TASK_DONE" = true ] || exit 1
jq '{id, type, createdAt, updatedAt, status, switches}' mac-filter-task-current.json
The workflow correlates exactly one new task by CHANGE_STARTED_AT, type: "macFilters", and the task IDs saved before the PUT. It then queries the collection at most 30 times at ten-second intervals and selects only the saved task ID. Success requires both a successful aggregate status and a terminal status of succeeded for every entry in switches[]; ambiguity, pagination, timeout, failures, and noSupportSubscription all cause the workflow to stop. The PUT is not repeated.
Handle Fusion API errors precisely
- 401/403: Check JWT expiry, service principal, tenant context, and headers; local roles do not help.
- Wrong tenant or regional host: Rediscover the tenant ID and host from
whoamior the list of managed tenants. - HTTP 200/201 without effect: Check read-back and the matching task; continue bounded polling, but do not blindly repeat PUT.
noSupportSubscription: Correct the Support Subscription and device state in the correct tenant; there is no local workaround.- Code
10905–Duplicate MAC filter policy: Inspectswitches[].error, reread the state, and resolve a duplicate or stale request; never retry blindly. - Code
10906–MAC filter list is exhausted: Stop and approve a smaller complete list; never send a subset as an append. - Code
10908–MAC address already allowed in the static MAC table: Resolve the conflict and security impact; never remove static allow entries without review.
For failures, capture HTTP status, task ID, switch ID, status, error, message, and code together, and redact sensitive data.
Rollback and restore boundaries
Rollback requires another complete PUT of the saved list. Reread the current list first and rule out concurrent changes, then use the same read-back and task workflow until every switch reaches the required terminal status.
# Hold an exclusive change window before this fresh read.
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-pre-restore.json
jq -e '.macAddresses | type == "array"' mac-filter-pre-restore.json >/dev/null || exit 1
diff -u \
<(jq -S '.macAddresses' mac-filter-desired.json) \
<(jq -S '.macAddresses' mac-filter-pre-restore.json) || exit 1
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/tasks?type=macFilters&pageSize=50&pageTotal=true" \
> mac-filter-restore-tasks-before.json
jq -e '
(.items | type == "array") and
(.pages.current == 1) and
(.pages.total >= 0) and (.pages.total <= 1) and
((.items | length) <= 50)
' mac-filter-restore-tasks-before.json >/dev/null || exit 1
jq '{macAddresses}' mac-filter-before.json > mac-filter-restore.json
RESTORE_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
--request PUT \
--header 'Content-Type: application/json' \
--data-binary @mac-filter-restore.json \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-restore-response.json
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/settings/mac-filtering" \
> mac-filter-restored.json
jq -e '.macAddresses | type == "array"' mac-filter-restored.json >/dev/null || exit 1
diff -u \
<(jq -S '.macAddresses' mac-filter-restore.json) \
<(jq -S '.macAddresses' mac-filter-restored.json) || exit 1
RESTORE_TASK_ID=''
RESTORE_TASK_DONE=false
for RESTORE_POLL_ATTEMPT in $(seq 1 30); do
printf 'header = "Authorization: Bearer %s"\nheader = "X-Tenant-ID: %s"\n' \
"$FUSION_TOKEN" "$FUSION_TENANT_ID" |
curl --disable --silent --show-error --fail-with-body \
--config - \
"$FUSION_DATA_REGION/switch/v1/tasks?type=macFilters&pageSize=50&pageTotal=true" \
> mac-filter-restore-tasks.json
jq -e '
(.items | type == "array") and
(.pages.current == 1) and
(.pages.total >= 0) and (.pages.total <= 1) and
((.items | length) <= 50)
' mac-filter-restore-tasks.json >/dev/null || exit 1
if [ -z "$RESTORE_TASK_ID" ]; then
jq -e -s --arg started "$RESTORE_STARTED_AT" '
def epoch: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
[.[0].items[].id] as $before |
[.[1].items[] |
select(
.type == "macFilters" and
(.id as $id | ($before | index($id) | not)) and
((.createdAt | epoch) >= ($started | epoch)) and
((.updatedAt | epoch) >= ($started | epoch))
)
] | if length > 1 then error("multiple matching rollback tasks") else . end
' mac-filter-restore-tasks-before.json mac-filter-restore-tasks.json \
> mac-filter-restore-task-matches.json || exit 1
if jq -e 'length == 1' mac-filter-restore-task-matches.json >/dev/null; then
RESTORE_TASK_ID="$(jq -er '.[0].id | strings | select(length > 0)' \
mac-filter-restore-task-matches.json)" || exit 1
fi
fi
if [ -n "$RESTORE_TASK_ID" ]; then
jq -e --arg id "$RESTORE_TASK_ID" '
[.items[] | select(.id == $id)] |
select(length == 1) | .[0]
' mac-filter-restore-tasks.json > mac-filter-restore-task-current.json || exit 1
if jq -e '.status.pending > 0' mac-filter-restore-task-current.json >/dev/null; then
:
else
jq -e '
(.status.total | type == "number") and (.status.total > 0) and
(.status.pending == 0) and (.status.failed == 0) and
((.status.noSupportSubscription // 0) == 0) and
(.status.succeeded == .status.total) and
(.switches | type == "array") and
((.switches | length) == .status.total) and
all(.switches[];
(.id | type == "string" and length > 0) and
(.status == "succeeded") and
(.error == null)
)
' mac-filter-restore-task-current.json >/dev/null || exit 1
RESTORE_TASK_DONE=true
break
fi
fi
[ "$RESTORE_POLL_ATTEMPT" -lt 30 ] && sleep 10
done
[ "$RESTORE_TASK_DONE" = true ] || exit 1
jq '{id, type, createdAt, updatedAt, status, switches}' \
mac-filter-restore-task-current.json
unset FUSION_TOKEN SP_CLIENT_ID FUSION_TENANT_ID FUSION_DATA_REGION \
CHANGE_STARTED_AT CHANGE_TASK_ID RESTORE_STARTED_AT RESTORE_TASK_ID
mac-filter-before.json is only a snapshot of this setting, not a full switch backup. If the baseline is lost, never send an empty list as a reset: it clears every blocked entry. This API neither restores local CLI/REST configuration nor releases Active Threat Response isolation. Finally, unset the token variables, protect or securely delete the files, and record the task result.
Troubleshoot by symptom
TLS or certificate error
- Check management name, IP, validity, certificate chain and system time.
- Supply the correct CA with
--cacert, or replace the device certificate through the approved management process. - Do not use
-kas a permanent workaround. It encrypts the transport but does not authenticate the switch. - If the certificate or SSH host key has changed, first confirm that you are connecting to the intended switch.
Connection rejected, timeout or no route
- Check the management IP, management VLAN, routing, ACL, and whether the service is enabled.
- Test from an authorized host in the intended management network.
- Do not bypass by opening the service for all networks or the Internet.
- If a recent change interrupted access, use the independent management path and prepared rollback.
Login fails
- Ensure that you are using a local switch account, not a Sophos Fusion account.
- Check username, password requirements, account status and local role.
- Do not make repeated automated guesses; they can trigger lockouts and obscure the cause.
- For the standard account
admin, note that its password is only changed byadminitself or Sophos Fusion.
HTTP 401 or 403
- For
401, check whether the token or session has expired and sign in again if necessary. - For
403, check the local role and whether it permits the operation; do not grant broader rights by default. - Request a new token at most once in a controlled manner. If the error persists, securely capture the response and firmware version, consult the published API help, and compare the target device’s model and firmware.
HTTP 400, 404 or 405
- Compare the path, method, headers, and JSON with the published API help, and validate them for the target device’s model and firmware.
- Check capitalization and the
/apiprefix. - A
404or405may indicate an unavailable endpoint or one defined differently in this firmware. Do not respond by trying similar write operations.
HTTP error with a response, or an errCode other than 0
- Capture the HTTP status and response body together.
- Record
message, redact internal information before sharing, and do not blindly repeat an identical change. - If a change may have been partially applied, determine the actual state first with a
GET, CLI output, and a functional test.
CLI command missing or rejected
- Use
?to check whether the command exists in the current mode. - Use
TABto complete the syntax offered by the device. - Check the local role, command mode, model, and firmware.
- Do not use a similar-sounding command from another firmware version.
Rollback, logout, and session termination
A GET /api/ports test does not change the configuration and therefore requires no configuration rollback. The API login does create a session, however, so end it and clear the local variables as described in the token lifecycle.
For a configuration change, define the rollback in advance:
- Export the current state and affected objects, or capture them with read operations.
- Prepare the exact inverse operation using the published API help validated for the installed firmware, or the target device’s CLI help.
- Define conditions that require an immediate abort, such as loss of management access, the uplink, VLAN connectivity, or PoE power.
- If an error occurs, do not attempt further optimization; restore the previous value through the path that still works.
- Then verify management access, port status, uplinks, and affected services again.
- If the normal path is lost, roll back through the previously verified independent management path, possibly the model-specific console port. A factory reset is not a normal rollback because it deletes the configuration.
For a switch managed in Sophos Fusion, a local rollback alone is not sufficient. Check the authoritative target state in Sophos Fusion, then reconcile an approved emergency change there or remove it completely from the local configuration. Do not alternate configuration changes between local and central management channels.
To finish:
- exit a CLI output with
Qand use the exit/logout command declared on the device at the prompt; - close the API session with the documented
PATCH /api/system/logout; - discard local token and password variables with
unset; - check that no temporary files or debug logs contain secrets;
- document the result, firmware, management path used, verification, and any rollback in the ticket.
Safety hardening
- Dedicated management VLAN with ACLs limited to a few admin hosts and required protocols.
- Enable HTTPS and SSH only when needed; disable insecure or unused management services.
- Use trusted certificates and verified SSH host keys.
- Use individual local accounts: User for read-only access and Admin only for approved changes.
- Replace default password immediately; avoid common passwords and rotate after personnel or service provider changes.
- Keep API secrets out of source code,
.envfiles, shell history, process arguments, and CI outputs. - Never run API clients with extensive header debugging as long as an Authorization header is set.
- Keep sessions short, use a new token for each session, and then unset the variables.
- Keep configuration backups protected and recoverable outside the switch.
- Correlate local logs, central events, and change tickets by timestamp; verify the switch’s clock.
- Regularly recertify local accounts, management ACLs, certificates, SSH keys and automation accesses.
Firmware and model variability
The static REST API page documents the login path /api/system/login, the read test /api/ports, and the Authorization header; the published API help also documents the logout path /api/system/logout. These central resources provide examples and guidance, but not a schema that applies universally to every device. The running target switch generates its own Swagger/OpenAPI schema for its model and firmware. The published CLI documents confirm the help features described above, but they do not guarantee that every additional command is identical across firmware versions.
Before production use, complete the following for each model and firmware version:
- document the exact firmware version and hardware model;
- Check CLI mode and syntax with
?andTAB; - check the target device’s Swagger/OpenAPI schema for the methods, paths, and schemas provided by the running firmware;
- test login and
GET /api/portsfirst in a controlled session; - validate all write automation against that exact target schema and in a non-production or clearly limited environment;
- after firmware updates, retest login, certificate verification, the response schema, token handling, and every endpoint used;
- if behavior differs, do not simply “make the script work” before understanding the new semantics and rollback requirements.
Final audit
- Correct model, firmware, and management IP confirmed.
- System of record—Sophos Fusion or local management—documented.
- Local User or Admin account appropriate to the task used.
- TLS certificate or SSH host key verified; no persistent insecure option used.
- Token used only for its session and never disclosed.
- HTTP status,
errCode,message, and functional state checked. - For changes, current state, rollback, and independent access available.
- Result verified through
GET, CLI output, and any required functional test. - Session closed, variables deleted, and logs checked for secrets.
- Local exception reconciled with Sophos Fusion and closed in the ticket.