StackShip uses separate credentials for separate protocol surfaces. A token for one surface is not a general-purpose API key.
| Credential | Accepted by | Purpose |
|---|---|---|
| Better Auth session | Dashboard and native /v1 API |
Human product access in an active organization |
| Service-account bearer token | OpenTofu /api/v2 compatibility surface |
Workspace- and project-bounded automation |
| OpenTofu OAuth opaque token | OpenTofu /api/v2 compatibility surface |
User access evaluated against current authorized memberships |
| Managed-run opaque credential | OpenTofu /api/v2 compatibility surface from the managed sandbox |
One active plan or apply attempt |
| MCP OAuth JWT | https://mcp.stackship.run/mcp and, for the confined MCP automation surface, native /v1 |
Organization-bound reads with mcp:tools; mutations also need mcp:write |
| Internal capability | A specific internal runner or broker route | One bounded execution action |
The separate StackShip CLI is a roadmap item and is not available in private beta.
OpenTofu service accounts
An authenticated principal with credential:write can open a workspace,
choose CLI access, create a service account, and issue one token
bounded to that workspace and its project. The dashboard offers only explicit
purpose presets:
| Purpose | Exact permissions |
|---|---|
| State migration | workspace:read, state:outputs:read, state:read, state:recover, state:write, state:lock |
| Remote plan and apply | workspace:read, state:read, run:create, run:read, run:approve, policy:read |
Expiry is selected explicitly from 1, 7, 30, 90, or 365 days. The API rejects an empty permission set, an unknown permission, a lifetime beyond 365 days, and permission escalation beyond the creating principal.
The token secret is returned only by the issuance response. Store it in the
OpenTofu credential location or an external secret manager. StackShip stores a
digest, not the bearer plaintext. An exact issuance retry by the same
authenticated principal with the same request and Idempotency-Key re-derives
the same token to recover a lost response; the metadata-only idempotency record
does not contain the token. Reusing that key with changed input conflicts. The
dashboard therefore reuses the exact request and key when a failed issuance is
retried unchanged, but creates a new bounded request and key when the operator
changes the purpose or expiry. It removes the plaintext from its rendered
state after I saved this token is acknowledged. It does not provide token
inventory because the v1 API has no list operation. Revocation
invalidates subsequent use through the API.
For non-interactive OpenTofu, provide the one-time value through the hostname-derived environment variable:
export TF_TOKEN_stackship_run='<service-account-token>'
tofu init
tofu planOpenTofu uses that credential with the native cloud protocol under /api/v2.
It is not accepted as a Better Auth dashboard session and it is not an MCP
credential.
Managed-run credentials
StackShip never forwards a customer’s OAuth or service-account credential into
the sandbox. For a managed plan or apply, the runner instead receives a separate
opaque rat_ credential through the same hostname-derived
TF_TOKEN_stackship_run environment variable. The API derives this credential
with the dedicated RUNNER_RUN_TOKEN_HMAC_KEY; it is distinct from customer
service-account tokens and from runner bootstrap, session, and artifact
capabilities.
The API accepts the credential only for its exact organization, project, workspace, run, attempt, phase, sandbox, and current workspace fence. Its state authority is phase-specific:
| Attempt | Read state | Lock state | Publish state |
|---|---|---|---|
| Managed plan | Current state | For the OpenTofu command | Never |
| Speculative plan | Pinned state version only | Never | Never |
| Managed apply | Current predecessor | For the OpenTofu command | Only with the active reservation, owned lock, expected predecessor, and current fence |
A terminal, revoked, expired, replaced, or stale attempt fails closed. The
credential never grants run:create or state:recover, is not a customer
credential, and is rejected by the dashboard, native /v1, and MCP surfaces.
OpenTofu OAuth login
Interactive tofu login stackship.run uses the fixed public tofu-cli
client, authorization code flow, and PKCE. The resulting access token is an
opaque bearer credential whose digest is stored by Better Auth. It expires
after 90 days and has only tfe:access. The client cannot request
offline_access, so no refresh token is issued.
This user token is not bound to the organization active during consent. For each cloud-protocol request, StackShip resolves the requested organization and requires the user to have a current authorized membership there. Removing a membership therefore prevents access to that organization even while the opaque token has time remaining.
The OAuth revocation endpoint deletes the opaque access-token record and makes subsequent requests fail. Removing the credential from OpenTofu’s local configuration only removes that local copy; it does not itself promise server-side revocation.
MCP OAuth
The MCP server publishes RFC 9728 protected-resource metadata. Clients obtain an access token from the API’s OAuth 2.1 authorization server using PKCE. The access token is a signed JWT bound to the organization selected during authorization and expires after one hour. Validation requires:
- a valid issuer and signature;
- an unexpired token;
mcp:tools;- the configured MCP resource as the token audience; and
- one exact organization claim.
Mutating MCP tools additionally require the mcp:write scope. Missing or
mismatched claims fail closed with a bearer challenge. Browser cookies and
OpenTofu service-account tokens are not accepted at the MCP endpoint.
The same JWT authenticates the confined MCP automation surface on native
/v1, where the API resolves it against its hashed token record and applies
live-membership tenant derivation; see the
v1 authentication rules.
Clients that request offline_access receive a hashed, opaque refresh token
with a 30-day expiry. Each successful refresh rotates that token and revokes
the previous value. Revoking a refresh token prevents future refreshes.
Revoking an already issued MCP JWT cannot invalidate the MCP Worker’s own
local check immediately because the Worker verifies JWTs without a revocation
list; tool execution still stops immediately, because the API resolves every
tool call against the hashed token record and refuses revoked or deleted
grants. Changing organizations requires a new authorization.
OAuth consent and token protocol records are not product audit events. StackShip does not currently record consent decisions, issuance, refresh, or revocation in the audit log, and no organization-settings OAuth revocation UI is available.
Handling rules
- Never put a bearer credential in a URL or query parameter.
- Never log tokens, authorization headers, session cookies, managed-run credentials, or internal capabilities.
- Give a service-account token only the permissions and lifetime it needs.
- Revoke a token when its consumer is retired or its custody is uncertain.
- Treat a downloaded state file as a credential-bearing artifact even though the download itself used authenticated streaming.