Skip to content

Variables and secrets

Versioned workspace Terraform and environment variables with write-only sensitive values.

Updated View as Markdown

The first release supports variables directly on a workspace. Organization, project, priority, and attached variable sets are not available in private beta.

Categories

Category Runner behavior
terraform Supplied as an OpenTofu input variable
environment Supplied as a process environment entry

Terraform variables also carry an hcl flag. Environment values are strings. Keys are bounded to 256 characters, descriptions to 1,024 characters, and values to 65,536 bytes. The native, dashboard, and OpenTofu-compatible variable mutation envelopes are bounded to 131,072 bytes so the maximum value fits with its JSON metadata. Credential-profile mutation documents use a separate 100,000-byte envelope limit for their supported 64 KiB canonical generic-secret map and bounded profile metadata.

Environment variables cannot use the runner-owned STACKSHIP_* or TFC_* namespaces, TF_CLI_ARGS, any TF_CLI_ARGS_* subcommand override, TF_LOG, any TF_LOG_* subsystem setting, TF_FORCE_LOCAL_BACKEND, or TF_TOKEN_stackship_run. StackShip also reserves OpenTofu controls that can redirect the managed workspace, data directory, provider process, plugin cache or transport, encryption, or persistence behavior: TF_CLI_CONFIG_FILE, TF_CLOUD_HOSTNAME, TF_CLOUD_ORGANIZATION, TF_CLOUD_PROJECT, TF_DATA_DIR, TF_DISABLE_PLUGIN_TLS, TF_ENCRYPTION, TF_IN_AUTOMATION, TF_PLUGIN_CACHE_DIR, TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE, TF_REATTACH_PROVIDERS, TF_STATE_PERSIST_INTERVAL, and TF_WORKSPACE. OpenTofu injects TF_CLI_ARGS* into its command line and its raw logging controls can write state or credentials to process output or disk, so StackShip reserves both complete prefix families instead of maintaining a subcommand or subsystem allowlist. The dashboard and both native and OpenTofu-compatible APIs reject these names before persisting a variable, with guidance to choose a provider-specific name. OpenTofu input variables remain available because StackShip projects them into the separate TF_VAR_* namespace at execution time.

Lifecycle

The native v1 API and dashboard support:

  • list current workspace variable metadata;
  • create a key and its first value version;
  • replace the value with a new immutable version, or edit the description and HCL flag while keeping the stored value; and
  • delete the variable.

Create, replace, and delete require an idempotency key. An exact replay returns the durable result; a changed request under the same key is rejected.

The current list response includes variable id, current version id, category, HCL flag, key, description, sensitivity, value digest, and timestamps.

In the dashboard, each row shows the key, a category badge using the same labels as the create form (“OpenTofu input” and “Environment”), an HCL badge when the flag is set, a Write-only badge for sensitive values, and the value digest truncated with the full SHA-256 as a tooltip. The edit form sits behind a per-row Edit action, and at most one edit form is open at a time. Its value field is optional and labeled “New value (leave blank to keep the current value)”: submitting without a value updates the description and the OpenTofu-input HCL flag while the stored value — even a sensitive one — stays untouched. Sensitive values remain write-only and are never prefilled into the form. Changing the sensitive flag requires re-entering the value; the dashboard asks for it inline before any request is sent, and the API enforces the same rule with 400 WORKSPACE_VARIABLE_VALUE_REQUIRED.

After a successful dashboard save, the variable row announces either that a new immutable version was created or that the metadata was updated with the stored value unchanged, and keeps that confirmation visible while the updated metadata is rendered. Any submitted value is cleared before the confirmation is shown; sensitive plaintext never appears in the message.

The create card also offers Add several at once: paste KEY=value lines to create non-sensitive variables of the selected category, one idempotent create per line. Blank lines and # comments are skipped; malformed lines are reported by line number before any request is sent, and a partial failure reports how many variables were created. Members without workspace:admin see a quiet read-only notice above the list.

Sensitive values

When sensitive is true:

  • the API encrypts the value before database persistence;
  • a random per-version AES-256-GCM data key is wrapped by the internal credential broker;
  • authenticated additional data binds the organization, workspace, variable, version, and category;
  • the database stores ciphertext, nonce, wrapped key, key version, and digest;
  • list and mutation responses return value: null; and
  • replacing the secret creates a new immutable version rather than changing old ciphertext.

The dashboard clears the submitted sensitive value from component state after the request settles. There is no product endpoint for reading sensitive plaintext.

Non-sensitive variables may return their current value. Their digest is still recorded so an immutable run input can bind the value it used.

Run snapshots

A managed run resolves the current variable versions before execution and stores version ids and digests in its immutable input snapshot. Non-sensitive values may be carried in that snapshot. Sensitive values appear only as secret version references for the credential broker.

Speculative runs omit sensitive workspace-variable references. Manifest publication fails closed if a speculative snapshot contains one.

Generic secret credential profiles

Use a generic_secret credential profile when a plan or apply must receive a customer-managed token, password, or other environment value for which no federation flow exists. Profile versions accept between 1 and 32 unique environment entries. Their canonical UTF-8 JSON representation may be at most 64 KiB. Values cannot contain a NUL code point because operating-system process environments cannot represent it. The API rejects such a value before sealing, and the credential broker and runner enforce the same boundary again before decryption output can reach process creation.

Plan and apply sandboxes have unrestricted public outbound network access, so every generic secret injected into a run is reachable by customer configuration, providers, data sources, modules, and processes such as local-exec. StackShip does not infer or restrict a destination from an environment name or secret value. Trust the repository and its dependencies, scope each secret to the minimum authority and lifetime, and prefer provider-native short-lived federation when it is available.

Names must match [A-Za-z_][A-Za-z0-9_]{0,127}. StackShip rejects process control names such as PATH, HOME, NODE_OPTIONS, LD_PRELOAD, OpenTofu control names such as TF_CLI_CONFIG_FILE; command-line overrides TF_CLI_ARGS and TF_CLI_ARGS_*; raw logging settings TF_LOG and TF_LOG_*; the managed-target, data, provider, plugin, encryption, and persistence controls listed above; and the reserved STACKSHIP_* and TFC_* namespaces. Publication and the runner independently enforce the same reserved-name contract, and also reject any collision with a workspace environment variable, a TF_VAR_* projection, another credential, or another runner-owned name.

The dashboard validates reserved names before submission. After a rejected generic-secret request, it clears every plaintext value while preserving the non-secret environment names and risk acknowledgements so the operator can correct the request and re-enter only the secrets. A successful request clears the complete form.

The value map is write-only:

  • the API sends it directly to the internal credential broker for sealing;
  • the broker encrypts it with a random AES-256-GCM data key;
  • the broker wraps that data key with the active, broker-only platform key;
  • authenticated context binds the envelope to the exact organization, workspace, profile-version id, and sorted environment-name set; and
  • PlanetScale stores only the ciphertext, nonce, wrapped key, key version, and a keyed digest derived from the random data key.

Equal plaintext in two versions does not produce the same digest. The API also reduces the secret-bearing part of an idempotent create request to a domain-separated HMAC before the ordinary request fingerprint reaches PlanetScale. A database reader therefore cannot use either field to test plaintext guesses without a key held outside the database.

List, create, and verification responses return configuration: null plus the safe environment-name metadata. There is no plaintext read endpoint. A version remains pending until the broker successfully decrypts and validates its stored envelope; only then can it become current. A profile with no verified current version is a detached draft: it is visible to operators but does not participate in run snapshots. Once a current version exists, run snapshotting fails closed unless that version remains verified and valid for the requested run.

New generic secret versions use plan_and_apply. They are never valid for plan, apply, or speculative_plan, including a same-repository pull request. New cloud federation versions use plan_and_apply, or speculative_plan when explicitly selected for pull-request plans. A run snapshot contains only the selected credential and profile-version ids. The execution manifest adds only the environment names, expiry, and an opaque one-time handle.

Archive and revoke

Archiving a credential profile detaches it from its workspace without deleting the profile, its immutable versions, or audit history. Revoking an exact version marks it disabled, records when it was disabled, and removes its current marker. The API can revoke a current or historical version for incident response. The dashboard presents the first revoke control only on the current verified version and requires a separate destructive confirmation for both revoke and archive.

In the dashboard, credential profiles across every workspace appear in one filterable table with spelled-out provider names, and a profile page opens from its profile ID alone — when a link omits the workspace, StackShip resolves the owning workspace server-side. The profile page leads with version status and verification; the provider trust template sits beside the version awaiting verification with a copy control; and the archive control lives in a separate confirmation-guarded section at the end of the page.

These transitions take effect at use time, not only when a run is created. StackShip re-resolves an exact pinned credential before manifest issuance and again before runner redemption. A revoked version or archived profile therefore cannot enter a new snapshot and cannot be redeemed by an already queued run. The run receives no replacement credential automatically.

Both actions require credential:write and append an organization audit event. They never erase a sealed generic-secret envelope or expose its plaintext.

Before the dashboard submits a generic secret version, the operator must separately acknowledge that plaintext enters the selected runner sandbox, infrastructure code and providers can access it, log redaction cannot make malicious infrastructure configuration safe, and federation is preferred whenever it is available. The API remains authoritative for the write-only shape and phase restrictions; dashboard acknowledgements do not make untrusted infrastructure configuration safe.

The handle is redeemable only by the authenticated runner for the exact organization, workspace, run, attempt, phase, sandbox, input digest, credential, profile version, environment-name set, and expiry used at issue time. A mismatched redemption does not consume it; one exact successful redemption deletes it atomically. The envelope is fetched from PlanetScale and presented only to the broker during redemption. It is never placed in a Cloudflare Workflow argument, Durable Object handle record, sandbox launch request, GitHub payload, policy input, audit detail, analytics event, or log.

Prefer federation

Use AWS, Azure, or Google Cloud federation profiles for cloud access whenever possible. Federation issues short-lived, phase-bound identity material and does not place a long-lived provider secret into the run environment.

The first release supports the commercial AWS partition. AWS GovCloud and AWS China profiles are rejected rather than being issued through an incompatible global STS or trust contract.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close