Security model
Copy page
This page explains how Sluice decides who can do what, and how it keeps secrets out of places where they do not belong. For the steps to secure a deployment, see Harden a deployment.
Credentials
Section titled “Credentials”Sluice has its own user accounts. It has no OIDC, SSO or SCIM. A request authenticates in one of three ways:
| Credential | Where it works | Used by |
|---|---|---|
Session cookie sluice_session |
The UI and /api/v1 |
Browsers |
API token, Authorization: Bearer slu_… |
/api/v1 and /mcp |
Scripts, CI jobs, the CLI, MCP clients |
Run token, Authorization: Bearer … |
/api/runner/v1 only |
The runner inside a task |
Webhooks under /hooks/ use their own keys or signatures. /mcp accepts only API tokens. A session cookie there gets 401.
Passwords
Section titled “Passwords”Sluice stores each password as an argon2id hash with 19 MiB of memory, 2 iterations and 1 thread. A new password needs at least 10 characters. A change of your own password needs the current password, and it ends your other sessions.
An admin can set a temporary password for a user. Until the user sets a new one, every operation except the own profile and password operations answers 403 password_change_required.
The login has a rate limit. More than 10 failures for one email, or 50 failures from one IP address, in 15 minutes give 429 rate_limited with Retry-After. The counts are in Postgres, so the limit applies over all instances. Sluice reads the IP address from the TCP connection, not from X-Forwarded-For. Behind a reverse proxy, all clients thus share the address of the proxy.
Sessions
Section titled “Sessions”| Property | Value |
|---|---|
| Cookie | sluice_session, HttpOnly, SameSite=Lax, Path=/. |
Secure flag |
On when SLUICE_PUBLIC_URL starts with https://. |
| Lifetime | SLUICE_SESSION_TTL, default 168h. Each use extends it, at most once per minute. |
| Storage | Postgres holds only the SHA-256 hash of the session ID. |
| End | Logout deletes the session. A disable or a password reset of a user deletes all sessions of the user. |
API tokens
Section titled “API tokens”An API token is slu_ and 43 base62 characters, 256 random bits. Sluice shows the token once, at creation. The list shows only the first 10 characters, for example slu_w6Ryj9. Postgres holds only the SHA-256 hash.
A token has a role that is at most the role of its owner. Its effective role is the lower of the token role and the current role of the owner. A demoted owner thus does not keep a higher token. A token can expire after 1 to 365 days, or never. A revoked or expired token, or a token of a disabled user, gets 401.
Each request reads the session or the token from Postgres. A disable, a role change or a revoke thus applies on all instances at once.
Sluice has four fixed roles: viewer, operator, editor and admin. Each role has all permissions of the roles before it.
| Capability | viewer | operator | editor | admin |
|---|---|---|---|---|
| Read dashboards, flows, files, executions, logs, metrics and variables | ✓ | ✓ | ✓ | ✓ |
| List secret keys and their metadata, never values | ✓ | ✓ | ✓ | ✓ |
| Use the assistant with the read tools | ✓ | ✓ | ✓ | ✓ |
| Trigger, cancel, rerun and restart executions, run a file | ✓ | ✓ | ✓ | |
| Request a triage, git Sync now | ✓ | ✓ | ✓ | |
| Edit managed files, push a branch, enable or disable flows, rotate webhook keys | ✓ | ✓ | ||
| Write namespace secrets and variables, check a secret | ✓ | ✓ | ||
| Create managed namespaces | ✓ | ✓ | ||
| Users, all tokens, global secrets and variables, secret providers, git sources | ✓ | |||
| Storage, instances, the AI provider, the audit log, delete namespaces | ✓ |
Every user manages their own profile, password and tokens.
Every API operation declares its access in code. The server does not start when an operation has none. The server checks the role before it reads the request body, so a caller without permission never sees validation details. The UI hides actions that your role does not allow, but the server enforces each rule.
The same-origin rule
Section titled “The same-origin rule”A browser sends the session cookie with every request to Sluice, also from another site. Sluice therefore checks the origin of each change that uses the cookie. A POST, PUT, PATCH or DELETE with the cookie passes only in one of these cases:
Originis the origin ofSLUICE_PUBLIC_URL.Originhas the same host as theHostheader of the request.- The request has no
Origin, andSec-Fetch-Siteissame-origin.
Every other cookie request gets 403 csrf_failed, also Origin: null. Requests with an API token do not need Origin, because a browser does not add a token by itself.
Security headers
Section titled “Security headers”Every response of the UI and of the API has these headers:
| Header | Value |
|---|---|
Content-Security-Policy |
default-src 'self'; frame-ancestors 'none' |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
The UI loads scripts, styles, fonts and images only from its own origin. The fonts are in the binary. No other site can show Sluice in a frame. A reverse proxy must not remove or change these headers.
Secrets
Section titled “Secrets”The rule is that no secret value leaves the task process in plain text.
| Place | Protection |
|---|---|
| Postgres | Builtin secret values are AES-256-GCM ciphertext. External secrets store only the reference. |
| API responses | No operation returns a secret value. A secret check returns only a status. |
| Logs, outputs, metric tags, errors | The runner masks secret values before it sends them. The server masks them again before it stores them. A secret shows as ***. |
| Object storage | Log archives and artifacts hold only masked text. |
| Docker containers and Kubernetes Jobs | They hold no secret values. The runner reads them from the runner API at run time. |
| AI requests and tool results | Sluice masks execution data with the secret values of every task run of the execution. |
| Audit events | Events hold keys and provider names, never values. |
The task process itself gets the resolved values in its environment. Sluice cannot stop a script that sends a value somewhere else.
Master keys
Section titled “Master keys”SLUICE_MASTER_KEYS holds the keys that encrypt builtin secrets, as kid:base64key entries. Each key has 32 bytes. The first key is the active key for new writes. The other keys can still decrypt older values. Each stored value records the ID of its key.
Without master keys, a write of a builtin secret answers 409 builtin_provider_disabled. The other providers still work. When a stored value uses a key ID that is not in the list, /readyz fails with master_key_missing. sluice secrets rekey encrypts all builtin values again with the active key. See Rotate the master key.
Stored hashes
Section titled “Stored hashes”Postgres holds no credential in plain text. Passwords are argon2id hashes. Session IDs, API tokens, run tokens and webhook keys are SHA-256 hashes.
Run tokens
Section titled “Run tokens”The dispatcher creates a run token when it claims a task run. The token reaches the runner in SLUICE_RUN_TOKEN.
| Property | Value |
|---|---|
| Scope | One task run. A token of task A on task B gets 403. |
| Validity | Only while the task run is RUNNING. |
| Expiry | The task timeout plus 10 minutes. |
| End | Sluice deletes the hash when the task run ends. Later calls get 401. |
| Routes | /api/runner/v1 only. A run token cannot call /api/v1. |
A Kubernetes Job holds the run token in its environment, because the runner needs it. The token has a narrow scope and expires, and the Job holds no secret values.
Webhook keys
Section titled “Webhook keys”A webhook trigger has no key until an editor rotates it. The rotation returns the key and the URL once. A key has 256 random bits. Sluice stores only its hash and compares it in constant time. A wrong key gets 404, so a caller cannot find out which flows exist. Sluice does not store the Authorization, Cookie and Proxy-Authorization headers of a webhook call.
Git webhooks use an HMAC signature or a token from a global secret.
MCP clients
Section titled “MCP clients”An MCP client uses an API token, and each tool runs with the role of the token. A mutating tool runs at once, with no confirmation, and Sluice records ai.tool.call in the audit log. Give each client its own token with the lowest role that it needs and an expiry. See The assistant and MCP.
Audit log
Section titled “Audit log”Sluice records each change with the time, the actor type (user, token, system or ai), the actor, the action, the target, details and the client IP address. A call with an API token records the token ID. An admin reads the log on Settings → Audit log or with GET /api/v1/audit, with filters for the actor, the action, the target and the time.
| Area | Actions |
|---|---|
| Sign-in | auth.login, auth.login_failed, auth.logout, auth.revoke_other_sessions |
| Users and tokens | user.create, user.update, user.change_password, user.reset_password, token.create, token.revoke |
| Secrets and variables | secret.create, secret.update, secret.delete, secret.rekey, secret_provider.create, secret_provider.update, secret_provider.delete, variable.create, variable.update, variable.delete |
| Git, namespaces and files | git_source.create, git_source.update, git_source.delete, git_source.sync, git.push, namespace.create, namespace.delete, file.save, file.sync |
| Flows and triggers | flow.enable, flow.disable, trigger.webhook_key_rotate, trigger.failed, trigger.chain_depth_exceeded |
| Executions | execution.trigger, execution.run_file, execution.cancel, execution.rerun, execution.restart |
| AI | ai.provider.update, ai.provider.delete, ai.triage.request, ai.action.confirmed, ai.action.rejected, ai.tool.call |
The maintenance leader deletes events older than 365 days.
TLS and the public URL
Section titled “TLS and the public URL”Sluice serves plain HTTP. It has no TLS settings. Put an Ingress, a load balancer or a reverse proxy with TLS in front of it.
SLUICE_PUBLIC_URL must be the URL that browsers use. Sluice uses it for the Secure flag of the cookie, the same-origin check and the webhook URLs. Keep SLUICE_INTERNAL_URL on the internal network. Runners call it with run tokens.
Containers
Section titled “Containers”Both Sluice images run as the non-root user 65532. The Helm chart runs the server with a read-only root file system, no privilege escalation, all capabilities dropped and the seccomp profile RuntimeDefault. The only writable path is an emptyDir at /tmp. See Deploy on Kubernetes with Helm.