Security¶
Artek-MCP gives whoever holds a valid credential a view into your gateway and, if you allow it, control over parts of it. You decide what to expose, to which keys, and on which network. This page explains what the controls do and where they stop.
Write-enabled means gateway-admin authority¶
Treat every key on a write-enabled gateway as an administrator credential
The readOnly flag is the module's real security boundary. With readOnly: false, tools such as project_resource_write can create executable script resources that the gateway runs. A write-enabled deployment can therefore run code on the gateway: treat any valid API key, or accepted OAuth token, as full gateway authority, exactly like an admin credential.
On a write-enabled gateway:
- Keep
mutation.allowlistnarrow. Scope every mutating tool to the smallest set of targets the integration needs. See Mutation policy. - Protect the keys accordingly. Rotate a key on any suspected exposure, never embed keys in client-side code, and prefer
localhostOnlyor network-level restriction where you can.
The module's own settings resource (com.artek.mcp/settings) cannot be written through config_resource_write or config_resource_delete. Only the session-authenticated, CSRF-protected settings page can change the security policy, so a write-enabled MCP caller can never lift its own restrictions. Reads of the settings are allowed, with secrets redacted.
Mutating tools are gated twice: readOnly blocks them all, and the allowlist restricts their targets. Every mutating tool also accepts dryRun: true to validate and report without changing anything, and every call, including dry runs, writes an audit line naming its targets.
Read-only is not secret-safe¶
A read-only key can still read sensitive data
readOnly limits changes, not disclosure. Treat any valid API key or accepted OAuth token as secret-reading authority over this gateway, even in the default read-only posture.
What a read-only key sees, and how much of it is protected:
- Structured config secrets are redacted.
config_resource_readredacts secret-named JSON fields (passwords, tokens, keys, credentials, salts), including array and object values, whatever the file size. There is no unredacted mode. Non-JSON config bodies are scrubbed heuristically (key=valuepairs and URL credentials) and flaggedparsedAsJson:false. - Log and diagnostics text is scrubbed on a best-effort basis.
gateway_logsandexport_diagnostics_bundlemask URL credentials and secretkey=valuepairs in messages and stack frames, plus session cookies (Cookie,Set-Cookie,JSESSIONID) and every exact occurrence of this module's own API tokens and gateway API key. A secret logged in an unusual shape can still slip through: the scrub is a net, not a wall. - Project-resource bodies are returned as-is, not redacted.
project_resource_read,ref_searchandref_usagesreturn resource content verbatim. A script, named query or binding that contains a hardcoded password or token discloses it to any key with read access. Do not store secrets in project resources: use Ignition secret providers or named credentials, and scope read access. - Tag values are returned as-is. Check what a tag folder holds before you let a key browse it.
- Vision windows can carry live values. A Vision window stores the last evaluated value of its bindings, so the Vision read tools can return live data (a logged-in username, for example). They are not redacted.
- Database rows, when you enable database access. See Database access.
You can scope a key by path, not only by tool. A tool profile decides which tools a key holds; per-key read lists decide which tags, project resources and config resources those tools may read. A key that needs tag_read for one folder can be confined to that folder, so a credential-bearing tag folder stays out of reach even through a tool the key legitimately holds. See Per-key read scoping, including what it does not cover.
IP-based controls assume a direct connection¶
localhostOnly, Origin validation and the per-address failed-authentication backoff all use the address of the direct network peer. The module deliberately does not trust X-Forwarded-For, because any client can set it.
Behind a reverse proxy, put access control at the network layer
When the gateway sits behind a reverse proxy or load balancer:
localhostOnlytreats every client as local, so it no longer restricts anything. Do not rely on it behind a proxy; use network ACLs or a firewall on the gateway port.- The failed-authentication backoff becomes shared. All clients appear to come from the proxy's address, so one broken or abusive client can trip the lockout (20 failures per minute) for everyone. The API key itself (256 bits, infeasible to guess) is the real gate; the backoff only reduces noise.
Treat these three controls as defense in depth for a proxied deployment, not as the access control.
Per-key scoping¶
Each API key can be narrowed beyond the gateway-wide policy. Per-key settings only ever narrow; they never grant more than the gateway allows.
| Per-key field | Narrows |
|---|---|
profile |
which tools, resources and prompts the key can see and call (Tool profiles) |
mutationAllowlist |
which targets the key may change |
dbConnections |
which database connections db_query may use |
tagReadAllowlist, resourceReadAllowlist, configReadAllowlist |
which tag, project-resource and config paths the key may read |
requestsPerMinute, writesPerMinute |
the key's own rate ceilings |
Every one of these is a property of the credential, not the request: it is resolved from the authenticated key, so nothing a client puts in a header or URL can widen it. Details: Configuration.
Rate limiting¶
Every authenticated request counts against a per-key budget measured over a sliding 60-second window, so a caller cannot spend its budget at the end of one clock minute and again at the start of the next. Only admitted calls are counted, so a refused call is not charged.
- Two budgets.
http.requestsPerMinute(default 120) bounds every authenticated request.http.writesPerMinutebounds the mutating calls only; absent ornullmeans it inherits the request ceiling. The write budget is charged after every authorization check, so a call that was always going to be refused never uses up a legitimate caller's budget. - Per key, narrow only. A key may carry its own
requestsPerMinuteandwritesPerMinute. The effective limit is the lower of the gateway-wide value and the key's value. A key asking for more is clamped, and a warning at config load names the number that will apply. To give one integration a higher ceiling, raise the gateway-wide value and narrow the other keys. - A refusal says when to retry. A 429 response carries a
Retry-Afterheader and aretryAfterSecondsfield in the error data, computed from when the oldest counted call ages out, and the message names which budget ran out.mcp_statusreports the caller's effective limits undercaller.rate, and counts rate refusals separately (counters.rateLimited) from policy refusals.
{
"http": {
"requestsPerMinute": 120, // every authenticated request, per key
"writesPerMinute": null // mutating calls only; null = inherit requestsPerMinute
},
"auth": { "keys": [
{ "name": "integration", "token": "…",
"requestsPerMinute": 600, // narrows only: clamped to the gateway-wide value
"writesPerMinute": 30 }
]}
}
Audit log¶
Every tools/call is logged under the ArtekMCP.Audit logger:
cid= actor= remote= method= target= outcome= detail=
Mutating calls carry targets=, and dryRun=true in the detail field when applicable. An API-key caller is recorded by key name; an OAuth caller as oauth:<sub>. The logger_level_set tool refuses any change that would stop this logger writing at INFO.
Checklist¶
- Leave
readOnlyon unless an integration needs to change something. - Give each client its own key, with a tool profile and read lists that match its job.
- Keep secrets out of project resources and out of tag folders a key can browse.
- Enable database access only for the connections an agent needs, ideally through a read-only database user.
- Restrict the network path to the gateway port, especially behind a proxy.
- Rotate any key you suspect has been exposed.