Secrets
A secret is a named value (a token, an API key, a password) stored per project and made available to the jobs that run in it. Secrets are encrypted at rest and masked in log output.
Like this, a deploy job that reads one:
printf %s "$KEY" | spool secret set STRIPE_KEYjobs: deploy: steps: - name: push run: | curl -H "Authorization: Bearer $STRIPE_KEY" https://api.example.com/chargesManaging them
Section titled “Managing them”spool secret set STRIPE_KEY # create or replace; prompts for the valuespool secret list # names + when set, never valuesspool secret get STRIPE_KEY # is it set, and since whenspool secret delete STRIPE_KEYset reads the value from stdin, or prompts for it without echo on a
terminal. No argument takes the value, so it can’t land in your shell
history.
Secrets are write-only. Once set, no CLI command, API call or console
page returns the value, and get answers only whether the name exists
and when it was last set. Jobs are the only reader, and logs mask what
they print.
To change a value, set it again. If you have lost one, rotate it at the
provider and set the new one. Listing needs viewer on the project,
setting and deleting need admin. See
roles.
In the console, Settings > Connections > Secrets has the same surface: the names for anyone who can see the project, replace and delete for admins.
How they reach a job
Section titled “How they reach a job”Every project secret is injected as an environment variable into every
pipeline job and every shell and code node in the project, whether it
needs it or not. You reference a secret the way you’d reference any env
var, as in the example at the top.
In a polyglot script the same variable is in scope, both in the script
process and in the commands a spool step runs:
#!/usr/bin/env bashspool step charge -- ./bin/charge --key "$STRIPE_KEY"A workflow-set env: value wins over a secret of the same name, so you
can shadow a secret for one job without deleting it.
Some names are refused, because every job gets the secret in its
environment: anything starting SPOOL_ (the engine’s own), and names
that change what a process runs at startup: PATH, LD_*, DYLD_*,
BASH_ENV, ENV, IFS, NODE_OPTIONS, PYTHONPATH and the like.
Store the value under a name of your own and use it from the job.
Credentials
Section titled “Credentials”For narrower access, store the value as a credential instead. A
credential reaches only the jobs and nodes that name it, and an http
or connector node can turn it into an auth header without the value ever
appearing in the node’s config. Use one for anything that would do
damage in the wrong job: a deploy key, a signing key, a token that can
push.
Credentials and connected accounts covers the kinds, where each is used, and accounts connected through a provider’s consent screen.
Runs from untrusted events
Section titled “Runs from untrusted events”A run started by an untrusted event (a fork pull request, a comment from someone outside the repo) still runs, so outside contributors get lint and test feedback, but it gets none of the project’s secrets. They are left out of its environment without an error, so a step that reads one sees an unset variable.
A credential the run declares is different: the step that declares it is
refused, with a message naming the untrusted origin. A graph can skip
its privileged nodes with if: "!run.untrusted".
Everything such a run starts is untrusted too: what it spawns or calls,
and the handler runs its events reach, its run.* lifecycle events
included. So a handler of yours that posts a commit status or a chat
message with a credential is refused for a fork’s runs. The status spool repo connect
posts is the exception: Hyperspool posts that one itself, so a fork’s pull
request still gets its check.
Pull requests from a bot account (a dependency updater, say) carry no association with the repo, so they count as untrusted too. Name the bot as trusted on the webhook that receives them:
spool webhook set github --manifest github --trust-actor 'renovate[bot]'--trust-actor applies to the author of the pull request, never waives
the fork check, and can be repeated. Setting the webhook again keeps its
signing key, so nothing changes on the forge’s side.
Masking
Section titled “Masking”Injected secret and credential values are masked wherever a job writes
them to the log (a stray echo, set -x, a tool that prints its
config): they show as ***, chunk by chunk in the live stream and
completely in the stored output.
Masking is best-effort against echoed values. Values shorter than six characters aren’t masked, since they would match ordinary output. It can’t catch a value you’ve transformed first (base64-encoded, sliced, hashed), so don’t print secrets on purpose.
Artifacts are not scanned
Section titled “Artifacts are not scanned”Masking applies to what a job writes to its log. An artifact is opaque
bytes and nothing looks inside it. If you spool artifact put a
directory that contains a .env, a kubeconfig or a service-account
JSON, anyone who can read the project’s artifacts can read that file.
So keep secrets out of what you upload, or encrypt the payload first:
#!/usr/bin/env bash# The build directory, minus anything that shouldn't travel.rm -f dist/.env dist/*.pemspool artifact put distEncryption
Section titled “Encryption”Secrets, credentials, state cells and run history are sealed with a key of your org’s own, which is in turn sealed by a service master key. One org’s key can’t open another org’s data.
Artifacts are stored as uploaded, without that sealing.