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.ai or 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

BUILT WITH ❤️ FROM CANADA AND SPAIN