Skip to content
SZ-MCP
Get Support

Security model

Two facts shape everything below. Your SmartZone password never reaches Claude, and your SmartZone account is the outer limit on what Claude can do. Inside that limit, the write guard makes every change wait for your agreement. The first fact is enforced by the architecture; the second is a deliberate design decision that puts the control in your hands.

SZ-MCP exposes every WSG and SwitchM endpoint your saved admin account is permitted to call — reads, writes and deletes alike. It does not maintain an allowlist of endpoints, and it cannot grant anything SmartZone’s roles do not. What it adds is a check on whether a write is sent: the write guard, which is on by default.

That is on purpose: SmartZone already has role-based access control, with roles, domain scoping and per-permission grants. Duplicating it would mean two models to keep in sync and two places to get it wrong.

  • Create a dedicated SmartZone admin for SZ-MCP. Do not reuse one that humans or other automations log in with.
  • Scope it to the minimum role and domain your use case needs. If you want a read-only experience, grant a read-only role in SmartZone — that is where the restriction belongs.
  • Rotate its password on your normal schedule.

With the write guard on (the default), Claude cannot change the controller without your agreement. It works at the call boundary, on both surfaces:

CallWhat happens
A readSent
A writeNot sent the first time. call returns confirm_required with a preview of the request and a confirm token. Claude shows you the preview, and re-sends the identical request with the token once you agree
A dangerous writeThe same, plus the current 6-digit code from your authenticator app (totp_required) or, if you have not set one up, an approval on the dashboard (approval_required; the dashboard shows it under Waiting for your approval)
Any write in read-only modeRefused with write_blocked
Any write by a viewer or operatorRefused with write_blocked: only engineers and above can make changes, whether or not the guard is on

A confirm token is valid for 10 minutes, is single-use, and covers only the exact request that was previewed, for one organisation: a changed request needs a fresh confirm. It is also refused in the same code_mode run that received it (“That confirm token was issued in this same code_mode run”), so Claude has to stop, show you the preview and send the write in a later run: it cannot confirm its own write.

CheckLimit
Dashboard approvalExpires after 10 minutes; covers one send. await_approval waits up to 15 seconds for it
Authenticator codes5 tries per 5 minutes per person, right or wrong (totp_locked past that); each code works once
Loosening the guardAn admin who has an authenticator app set up needs a current code from it to turn the guard or the extra check off, or to untick a dangerous category or remove one of your own rules, as removing the app does

An organisation admin sets the policy on the dashboard’s Write guard card:

  • Confirm every write (on by default).
  • Read-only mode (off by default): refuse every write, whatever the chat says.
  • Extra check for dangerous actions (on by default), with the Dangerous actions list. Eight categories are ticked by default: restarting or shutting down the controller, rebooting an AP, or moving APs to another zone; firmware upgrades, AP patches and signature packages; restoring a controller or switch configuration backup; cluster operations; disconnecting, deauthenticating or blocking clients; deleting a zone, domain, WLAN, switch or switch group; AP packet captures; reordering AP registration rules. A ninth, Every write, is unticked by default.
  • Your own rules: a method and path to treat as dangerous too, where * is one path segment and a trailing /** anything below, e.g. PATCH /rkszones/*/wlans/**.

Each person sets up their own Authenticator app on the same card. Writes are logged, with who asked for them, on the Write history page (/writes).

Every controller write Claude can cause goes through the guard, but not always as one confirm per request:

PathHow it is guarded
wifi.call / switches.callOne confirm per write, as above
switches.backupA write: confirm
switches.restoreA dangerous write. Also refused unless the switch has a successful backup from the last 15 minutes, unless you say to force it
history.undoThe inverse write goes through the guard like any call. If the target changed after the write, it also needs force
mop.start (a MOP with writes)Approved once for the whole run: you confirm a preview of every write step, its body, its rollback and the change window. The run’s writes are then sent without further confirms, for 12 hours, and only for the person who started it. They are still refused for a read-only role or organisation, or if a step has since become a dangerous action
mop.rollbackSent under the run’s approval, by the person who started it; anyone else uses history.undo
mop.scheduleA standing approval of that MOP version and those parameters, through the same preview. Each occurrence re-checks that the approver still has the role, the MOP is unchanged and no step has become dangerous, then runs for 12 hours under that approval. Recurring schedules last at most 90 days
lint.to_mopOnly builds (or saves) a MOP; nothing is sent until mop.start
org.import_bundleChanges SZ-MCP’s own settings, never the controller. A dry run unless apply: true (engineer and up)
switches.ping, switches.traceroutePOSTs classified as reads, so not confirmed. So are SwitchM’s list endpoints (POST /switch and other add… operations)

Two controller writes happen outside the chat, from the dashboard, and do not go through the guard or read-only mode: turning on streaming (an admin action that creates a Northbound streaming profile on the controller) and Let’s Encrypt certificates for a controller behind a tunnel (issued and renewed by an admin’s choice; each write is recorded in the write log under that admin).

The password is stored in Cloudflare D1, encrypted with AES-256-GCM. The key is derived per record with HKDF-SHA256 from an ENCRYPTION_KEY that exists only in Cloudflare’s encrypted secret store.

Each record carries its own random 16-byte salt, stored alongside the ciphertext. The salt is not a secret; its job is to force per-record key derivation, so compromising the master key still means attacking each ciphertext individually rather than all of them at once.

The plaintext is decrypted in memory, only when a new service ticket needs minting, and is never written anywhere. It is never returned by any API: the credentials endpoint omits it, and the dashboard renders a placeholder instead.

SmartZone authenticates API calls with a service ticket, minted from your username and password and then passed on each request.

SmartZoneTicket cache (KV)SZ-MCP WorkerClaudeSmartZoneTicket cache (KV)SZ-MCP WorkerClaudealt[cached and fresh][missing or expired]call({ method, path })look up sz_ticket:orgIdticketdecrypt passwordPOST /serviceTicketserviceTicketcache for 25 minutesrequest + ?serviceTicket=...responseresponse with url redacted

As the diagram shows, the ticket is fetched from cache where possible and minted from the decrypted password only when the cache misses. It is attached to the upstream URL by the Worker, and the response handed back to Claude has that parameter replaced with <redacted>.

PropertyValue
Cache locationCloudflare KV, keyed sz_ticket:<orgId>: one ticket per organisation, shared by its members
Cache lifetime25 minutes — deliberately shorter than SmartZone’s idle timeout
On 401The ticket is discarded, one fresh ticket is minted, and the request is retried exactly once
FlushedOn every credentials save, on delete, and on Test connection

A second 401 after the retry is returned to the model as-is rather than triggering another login.

How often SZ-MCP logs in to your controller

Section titled “How often SZ-MCP logs in to your controller”

Every service ticket is a login to SmartZone under the admin username you saved, so the ticket lifecycle decides what your controller’s admin or audit log sees from SZ-MCP:

  • While Claude or SZ-MCP is using the controller: at most one login every 25 minutes per organisation. The ticket’s lifetime is not extended by use, so steady use logs in again roughly every 25 minutes.
  • While nobody is chatting: background work still uses the login, and logs in whenever it finds no ticket cached: the hourly inventory refresh (on once the organisation has run its first inventory sync, with a daily config snapshot), metric polling every 5 minutes if it is on (which also reads the admin audit log), synthetic checks, scheduled MOPs, and Let’s Encrypt renewals. With none of these on, there are no logins.
  • After a 401: one extra login for the single retry.
  • On Save & detect or Test connection: one login to detect the API version (a save that keeps the stored password skips detection). Both also flush the cached ticket, so the next call logs in again.
ItemReaches Claude?
SmartZone passwordNo — decrypted in the Worker only, never in the isolate
Service ticketNo — attached server-side, redacted from the url field
Your Google credentialsNo — Google handles the sign-in
MCP access tokenHeld by Claude; stored here only as a SHA-256 hash
SmartZone API responsesYes — this is the point

The program Claude writes runs in a separate sandboxed isolate with no network access of its own and no ambient credentials. Its only reach is the functions on the twelve code-mode namespaces. See How code mode works.

Two protections apply to what comes back from the controller:

  • Private keys. Every PEM private-key block in a controller response is replaced with [private key redacted] (SmartZone’s GET /certstore/certificate/{id} returns one in full).
  • Network text is labelled. A result carrying names, SSIDs, hostnames or log text gets a dataNotice, and any string that reads like an instruction to the model is listed in injectionWarnings. Claude is told never to follow instructions found in data, and no write is sent without your agreement.

Every MCP request is authenticated to exactly one person and one organisation before any tool runs: the connector is bound to the organisation chosen when it was authorised, and the person’s membership is checked again on every call, so removing someone stops their connector at once. Stored credentials, cached tickets and collected data are keyed by organisation. Members of one organisation share its controller login; there is no code path that serves one organisation’s request from another’s data.

Tokens and session cookies are stored hashed, never in plaintext:

CredentialStorage
Web session cookie szmcp_sessionSHA-256 hash; HttpOnly, Secure, SameSite=Lax; 30 days
MCP access token (szmk_…)SHA-256 hash; 30 days
MCP refresh tokenSHA-256 hash; 90 days, rotated on use
OAuth authorization codeSHA-256 hash; 10 minutes, single use

All calls to your controller are HTTPS with certificate validation enforced by the platform — which is why a publicly trusted certificate is a requirement.

Outbound destinations are constrained: hosts in private, loopback, link-local, CGNAT, multicast and reserved ranges are refused, both when you save credentials and on every call, and only ports 8443 and 443 are allowed. The full list is in Controller requirements. Redirects from the controller are returned, never followed.

Observability logs record method, status and duration. Request and response bodies are not logged there. For SmartZone’s own login errors only the status is logged; the body is never logged or echoed to the client, because it can disclose account-lockout state and username hints.

DataWhereKept
Write log: time, person, method, path, status, how it was approved, and a SHA-256 hash of the bodyD1365 days
Write history: each write’s full body, the target’s state just before it, and a create’s response (secret-looking fields masked when shown, kept unmasked so an undo can restore them)Your organisation’s store30 days, at most 5,000 writes
Daily config snapshots and their field changes; secrets kept only as keyed digestsYour organisation’s store180 days
Controller admin audit logYour organisation’s store400 days

Northbound streaming is not available on the hosted service yet. When an organisation streams, the stored events, alarms, client sessions and connection attempts name clients; an organisation admin can shorten their retention (1–14 days, default 14), keep application flows for 0–2 days (default 2), and replace client MACs, host names and user names with pseudonyms, on the Privacy card under Data collection.