Security and auth
Your box holds the data, and Stackbone does not sit in front of it. Studio and the CLI open connections straight to the box address you registered, so no run, payload or credential passes through Stackbone's servers. Every caller proves itself at the box: your browser and the CLI with a five-minute identity token, the control plane with a signature. The credentials your code needs (workspace secrets, connector tokens, the model key) are encrypted in the box's own database and never leave it.
Your traffic does not pass through Stackbone
The control plane stores the box address you registered and hands it to Studio and to the CLI. Both then call your box themselves.
| What you do | Where the request goes |
|---|---|
| Open a run, read a trace, edit a prompt | Your browser to your box |
stackbone runs list, stackbone secrets set |
Your terminal to your box |
| Chat with your agent from your own service | Your service to your box |
| Invite a member, register a box, install | Your browser to the control plane |
| Push installation config down to a box | The control plane to your box |
Your box does not have to be reachable from the internet, only from the machines that use it: a browser inside your VPN and a CI runner are enough. A Stackbone outage also leaves your agent running, because the box serves its own traffic.
A self-hosted control plane forwards one call, the one that reveals a secret.
The hosted control plane at app.stackbone.ai forwards nothing.
How Studio and the CLI prove themselves to the box
Studio and the CLI present a short-lived identity token, and the box checks it against the control plane's public keys.
The control plane mints that token. When you open a box in Studio, or run a box
command such as stackbone runs list, it signs a token for your session with
its private key (EdDSA). It is valid for five minutes and names you, your
organization, the installation the box was registered under, and your role.
Your browser or the CLI sends it as Authorization: Bearer.
The box verifies it on the other end. It fetches the control plane's public
keys from /api/auth/jwks and caches them. Those keys rotate every seven days, and a
retired one stays published for thirty days after, so a rotation never cuts a
box off mid-request. With a valid signature in hand, the box checks that the
token names the organization and installation it was registered under, then
applies your role.
| The box answers | When |
|---|---|
401 bearer missing or malformed |
No Authorization header on a gated route |
invalid_signature |
The token was not signed by the control plane the box trusts |
invalid_token |
Expired, or missing the organization or installation claim |
invalid_token_tenant |
A valid token for a different organization or installation |
box_not_registered |
The box has never been linked, so it trusts nobody yet |
403 with a capability name |
Your role does not carry that action |
Those two tenant codes differ on purpose, so you can tell a token aimed at the wrong organization from a box nobody has linked yet.
By default a box trusts the hosted control plane at api.stackbone.ai. A box
that belongs to a self-hosted control plane learns which one to trust from
STACKBONE_CONTROL_PLANE_URL on the container. The notes under
stackbone link cover the warning link prints
when a box cannot verify tokens.
How the control plane proves itself to the box
The control plane holds no session with your box. It signs each call with a shared signing secret, and the box checks the signature.
The box mints that secret itself. On its first boot it writes a 32-byte secret
into its own database (the runtime_identity table) and prints it once in its
log at warning level. That secret is what
stackbone link or the
browser wizard asks you for. The
registration signs a challenge with it and the box confirms it can reproduce the
signature. It then tells the box, over the same proven secret, which
organization and which installation it answers for. Only after both steps does
the control plane write the deployment record. A box that cannot be told is not
registered at all, so a failure here leaves no record and no orphan box.
The signature travels in one header. stackbone-signature holds a timestamp
and one or more HMAC-SHA256 digests:
stackbone-signature: t=1755523200,v1=9f2c…The digest covers the timestamp, the HTTP method, the path with its query, and the body. The box recomputes it and refuses anything it cannot reproduce, plus anything more than five minutes old. A missing, malformed or empty signature fails closed.
Rotating the secret does not cut the box off. Register a new one and the control plane keeps the previous secret in a rotation slot, sending both digests for a while. The box accepts the request if either one matches, so a box that still holds the old secret keeps answering.
Set HMAC_SECRET on the container when your platform injects secrets from a
vault and you want to decide the value before the box starts. It wins over
whatever is stored. Mind who can read your container logs and your box's
database: the secret is in the first boot log, and it sits in the clear in the
box's own database so that an operator debugging a failed link can read it back.
The control plane keeps its own copy encrypted at rest.
The chat wires take any bearer today
The OpenAI, Anthropic and AG-UI endpoints ask for a bearer credential
(Authorization: Bearer <key> or x-api-key: <key>), so the standard SDKs work
unchanged. The box only checks that a credential is present. It never looks at
the value, because per-workspace API keys are not there yet. Until keyed access
ships, keep a deployed box off the public internet or put its chat routes behind
your own gateway. Treat the box address itself as something to protect. The
protocol reference lists the error
shapes.
Approval hooks and inbound triggers
A workflow that requestApproval() paused resumes
when someone posts a decision to its hook,
POST /api/workflows/hooks/:token/resume. That route needs no token from the
control plane: the resume token is random and unguessable, and holding it is the
permission. Only the /hooks/* subtree is exempt, so starting a workflow still
goes through the gate. If you pass your own
token to requestApproval(), make it as hard to guess as the generated one.
Deciding from the HITL Inbox or with stackbone hitl approve goes through the
gated path and needs the hitl:decide capability.
Nothing calls into your box to start a workflow from outside. Inbound triggers poll the provider from inside the box, and a connector's OAuth callback comes back to the box's own address carrying a signed state, never a credential.
A local box has no gate
stackbone dev runs the same server on your machine and arms neither check:
nothing on your laptop holds a signing secret or verifies identity tokens. The
exemption is deliberate, so the local loop needs no login round trip.
The tunnel stackbone dev opens so that Studio can reach it is a
*.tun.stackbone.ai address, granted by the control plane to your signed-in
CLI and gone when stackbone dev exits. Part of that address is stable for the
workspace, so a Studio link survives a restart. Treat the address as something
to keep to yourself while it lives.
Credentials your code never holds
Three kinds of credential, kept apart on purpose: the secrets your code reads,
the accounts your connectors act as, and the key that reaches your model
provider. All three live in the box, encrypted under STACKBONE_SECRET_KEY,
the one key you supply to the container.
You write a secret once. The list never shows you a value.
Workspace secrets
Create them in Studio under Settings › Secrets or with
stackbone secrets set NAME, which reads the value from stdin so it never lands
in your shell history. The box encrypts every value with libsodium's
crypto_secretbox (XSalsa20-Poly1305, a fresh nonce per write) and stores the
envelope in its own Postgres.
The list shows a mask. Reveal is a separate action, and what it gives you
depends on which control plane you use. On app.stackbone.ai it stops at a
re-authentication step that is not live yet, so rotate a secret you have lost
rather than looking for it. A local stackbone dev box, and a box behind a
self-hosted control plane, return the plaintext to the dialog. The CLI has no
reveal on any of them: list, set and remove. Studio's DB Explorer runs as
a read-only database role that cannot select from
the secrets table. Names beginning STACKBONE_, and the platform's own
(DATABASE_URL, MODEL_PROVIDER_API_KEY, AWS_*, S3_* and a few more), are
reserved so a workspace secret cannot shadow them.
Your code reads a secret with
stackbone.secrets.get(name): a query on the
box's own database and a local decrypt. That query makes no round trip to
Stackbone and caches nothing, so a rotation lands on the next read. Pass the
value to the client that needs it and never log it.
Connector tokens, which Stackbone never receives
Stackbone Connect runs a broker inside your box, and every provider credential stays there.
You type an integration's client secret once, when you register it. The broker
encrypts that value and never reads it back to the screen. Connecting an account
runs the OAuth exchange between your box and the provider: the callback returns
to your box's own address (STACKBONE_PUBLIC_URL) carrying a signed state, and
the control plane sees neither the code nor the token.
Your box encrypts the access and refresh tokens before they reach its Postgres,
under a key derived from STACKBONE_SECRET_KEY, and writes no plaintext to a
column. It renews them itself too: on demand when a call finds a token close to
expiry, and on a background sweep every fifteen minutes, so a long-idle
connection is still live when you need it.
Your code never touches the token.
callConnector() hands the broker the
operation and gets back only that operation's output. The lower-level
connect(connectorId) adapter exists for connection runtimes that must attach a
credential themselves, and it hands back a bearer for the current principal
only.
You enter the model key once. Studio never shows it again.
The model provider key
Set it under Settings › Model provider, or with MODEL_PROVIDER_API_KEY and
MODEL_PROVIDER_BASE_URL on the container, which win over the stored value and
turn the screen read-only. The box stores it as a system secret in the same
encrypted table, tests the connection server-side so the key never returns to the
browser, and reads it back at boot before your bundle loads. It goes to your
provider and nowhere else: Stackbone does not hold your model key and does not
see your model bill.
The two keys the container runs on are STACKBONE_SECRET_KEY, which encrypts
everything above and cannot live in the database it protects, and the signing
secret from How the control plane proves itself to the
box. The
secrets FAQ covers what changing
each one does.
What Stackbone holds, and what stays in your box
| Where | What lives there |
|---|---|
The control plane (app.stackbone.ai) |
Your account (email, name), sessions, organizations, members and roles, invitations, agent templates, installations with the configuration you typed when installing, the deployment record (box address, image tag, signing secret encrypted at rest), tunnel grants for stackbone dev, and the key pair that signs identity tokens. |
| Your box | Runs, sessions, traces, approvals and their decisions, prompts and their versions, dynamic config, guardrails, eval cases, suites and results, RAG collections, recurring job state, workspace secrets, connector credentials, the model provider key, and the files in your bucket. |
| Your browser and terminal | The session cookie, and ~/.stackbone/credentials.json for the CLI. |
No run, payload, secret or connector token crosses the control plane. Studio reads them from the box, in your browser, with the identity token. Your browser reaches object storage through signed URLs that expire after an hour, and the Studio storage browser cannot issue one that lives longer. See Where does my data live? for the databases and the bucket.
What you set on a deployed box
The container reference lists every variable. These are the ones that decide who gets in.
| Setting | Why it matters |
|---|---|
An https:// address |
Your browser calls the box directly, and the registration form refuses a plain http:// address. The box does not have to be reachable from the internet, only from the machines that use it. |
STACKBONE_SECRET_KEY |
Encrypts every secret, connector credential and the model key. openssl rand -base64 32. Keep it stable: changing it makes everything already stored unreadable, with no recovery. |
HMAC_SECRET (optional) |
The signing secret. The box mints one on first boot if you leave it unset. Set it when a vault should decide it. Anyone holding it can sign requests to the box as the control plane. |
STACKBONE_CONTROL_PLANE_URL |
Only for a box that belongs to a self-hosted control plane. It names whose identity tokens the box trusts. Left unset, the box trusts api.stackbone.ai. |
STACKBONE_PUBLIC_URL |
The address a connector's OAuth callback returns to. Needed when you connect accounts to integrations. |
| Network exposure | The chat wires accept any non-empty bearer today. Keep the box inside your network or behind your own gateway until per-workspace keys ship. |
| Who reads the logs and the database | The box prints the signing secret once, on the boot that mints it, and keeps it in the clear in its own runtime_identity table. Workspace secrets and connector tokens stay encrypted, and the DB Explorer role cannot read them. |
Not there yet
Things a security questionnaire may ask about that a box does not do today:
- Per-workspace API keys for the chat wires. The box checks presence, not value.
- Reading a stored secret back from
app.stackbone.aior from the CLI. Rotate instead. - Automatic log scrubbing. Guardrails can mask personal data and credentials
in a turn when you attach a rule; nothing rewrites your
console.*lines. Do not print secret values. The durable-logs design adds masking at the pipe. - Key rotation for
STACKBONE_SECRET_KEY. Envelopes carry a version, but only one is decoded today. - An audit trail of member and role changes on the control plane. A box does log each approval decision with who decided and when.
Read more
- Connect your box: registering a deployed container from the browser, and where the signing secret comes from.
stackbone linkand what you set on the deployed container.stackbone.secrets: the read surface and its error codes.- Integrations and Connections: the broker, and calling a connector from your code.
- How are secrets handled? and Where does my data live?.
- Governance: what Studio shows about a running box, and who can decide what.
- Install and sign in: how a person signs in to the control plane, and where the CLI keeps its session.