Security model
Two facts shape everything below. Your SmartZone password never reaches Claude, and your SmartZone account is the only thing limiting what Claude can do. The first is enforced by the architecture; the second is a deliberate design decision that puts the control in your hands.
Access control is delegated to SmartZone
Section titled “Access control is delegated to SmartZone”SZ-MCP exposes every WSG and SwitchM endpoint your saved admin account is permitted to call — reads, writes and deletes alike. It does not gate by HTTP method, does not maintain an allowlist, and does not layer its own role model on top of SmartZone’s.
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.
What to do about it
Section titled “What to do about it”- 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.
Where the password lives
Section titled “Where the password lives”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.
The service ticket lifecycle
Section titled “The service ticket lifecycle”SmartZone authenticates API calls with a service ticket, minted from your username and password and then passed on each request.
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>.
| Property | Value |
|---|---|
| Cache location | Cloudflare KV, keyed sz_ticket:<userId> |
| Cache lifetime | 25 minutes — deliberately shorter than SmartZone’s idle timeout |
On 401 | The ticket is discarded, one fresh ticket is minted, and the request is retried exactly once |
| Flushed | On 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 is using the connector: at most one login every 25 minutes per SZ-MCP account. The ticket is not refreshed in the background and its lifetime is not extended by use, so a long session logs in again roughly every 25 minutes.
- While nobody is using it: no logins at all. A ticket is minted only when
a
callfinds none cached. - 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
calllogs in again.
What Claude never sees
Section titled “What Claude never sees”| Item | Reaches Claude? |
|---|---|
| SmartZone password | No — decrypted in the Worker only, never in the isolate |
| Service ticket | No — attached server-side, redacted from the url field |
| Your Google credentials | No — Google handles the sign-in |
| MCP access token | Held by Claude; stored here only as a SHA-256 hash |
| SmartZone API responses | Yes — 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 five primitives. See How code mode works.
Per-user isolation
Section titled “Per-user isolation”Every MCP request is authenticated to exactly one user before any tool runs. Stored credentials, cached tickets and decrypted passwords are all keyed by user id, and there is no code path that serves one user’s request from another’s data.
Tokens and session cookies are stored hashed, never in plaintext:
| Credential | Storage |
|---|---|
Web session cookie szmcp_session | SHA-256 hash; HttpOnly, Secure, SameSite=Lax; 30 days |
MCP access token (szmk_…) | SHA-256 hash; 30 days |
| MCP refresh token | SHA-256 hash; 90 days, rotated on use |
| OAuth authorization code | SHA-256 hash; 10 minutes, single use |
Transport and egress
Section titled “Transport and egress”All calls to your controller are HTTPS with certificate validation enforced by the platform — which is why a publicly trusted certificate is a hard requirement rather than a recommendation.
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. The full list is in Controller requirements.
Observability logs record method, status and duration. Request and response bodies are not logged; SmartZone’s own login error bodies are logged server-side but never echoed to the client, because they can disclose account-lockout state and username hints.