Secrets
Pipelines need credentials: a registry password, a deploy key, a token for an API. In a program, they are values typed Secret, given as locators rather than values. You choose where each one is kept:
- On your side, in the environment of the machine running the engine (
env:MELODIUM_SECRET_<NAME>), in a file (file:<path>), or in a Vault or OpenBao server (vault:<path>#<field>). Cadence.CI never receives these values, only the name and type of the secret. - On Cadence.CI, stored for your organization and read by your runs with
api:<name>. Values are encrypted, and nobody reads them back: members write them, runs read them.
Storing Secrets
The Secrets page of the dashboard, under Resources, lists the secrets of the organization.
| Administrator | Operator | Member | |
|---|---|---|---|
| See the names | ✓ | ✓ | ✓ |
| See descriptions, versions, rules and accesses | ✓ | ✓ | |
| See the secrets a run used, on its page | ✓ | ✓ | ✓ |
| Add a secret, give a new value | ✓ | ✓ | |
| Change the description, allow public runs, delete | ✓ | ||
| Add and delete rules | ✓ |
- Names are made of letters, digits,
_,.and-, in parts separated by/, such asprod/db-password, at most 200 characters. Names underplatform/are kept for the secrets Cadence.CI uses itself, such as the keys of a connected cloud account or of a bucket, or the token of a Clever Cloud add-on. Runs never read them, they do not count towards the plan, and each is managed where it is used. - Values are text, or a file for binary values, at most 64 KiB. A value is sent once and never shown again, to anyone. A new value becomes a new version, which runs read from then on.
- Names are not secret. A program names the secrets it reads, and programs are shown with their runs, to anyone for public runs. Never put anything confidential in a name.
- Plans allow 50 secrets on Basic and on the open-source plan, 500 on Pro, and 10 without a plan. The pricing page shows these limits as Cadence.CI applies them.
Values are encrypted with AES-256-GCM, each version with a key of its own, under keys kept outside the database. Deleting a secret erases its values.
Rules
Rules decide which runs read and write which secrets. A rule applies to the secrets named by its prefix or under it: prod covers prod and prod/db, not production, and an empty prefix covers every secret. Every matching rule applies:
- Required tags: a run reads a matching secret only if it holds all of them, and so do the runs that started it.
- Writable by runs: runs holding the tags may store matching secrets.
Without rules, every private run of the organization reads every secret, and no run stores any.
Tags are shown on the run page. A run launched with --api-report holds entrypoint=<name> and the tags listed, separated by spaces, in the MELODIUM_API_TAGS environment variable; the steps it dispatches hold the tags its program gives them. As the tags of the runs that started a step count too, a step never gets more than the run that dispatched it.
The accesses of each secret, on the Secrets page, list its reads, writes and refusals, with the run or member and the reason of a refusal. The access log below the list gathers those of every secret, newest first, and filters them by kind; accesses stay after their secret is deleted, under the name it had. Values never appear in either.
The page of a run lists the secrets it read, wrote or was refused, oldest first, by name only, with the reason of each refusal, which the run also received in its logs. Every member of the organization sees it.
The Cadence.CI team can read the names of your secrets and their accesses to help you, never their values, and changes nothing.
Reading Secrets in Programs
On a Cadence.CI worker, a Secret parameter takes an api: locator like any other:
use sql::SqlPool
use std/ops/option::|wrap
treatment database(const password: Secret<string> = "api:prod/db-password")
model pool: SqlPool(url="postgresql://app@db.example.com/app", password=|wrap<Secret<string>>(password))
{
}The engine reads the value from Cadence.CI with the token of the machine running it, which only that run holds. It keeps the value until it ends, and reveals it only to the element using it: here SqlPool, when connecting. As for any secret, the value is masked in logs, and never appears in parameters, debug events or reports. With --check-secrets, the engine reads the secrets given as parameters at startup, so that a missing secret or a refusal fails the run right away.
A refusal gives its reason, such as the tag a run is missing, never a value.
The Secrets chapter of the Mélodium Language Book covers Secret values, locators, and which elements reveal them.
From GitHub and GitLab Runners
A program launched in a GitHub or GitLab job runs outside Cadence.CI, where api: secrets are refused. The steps it dispatches run on Cadence.CI workers, which read them: give the secrets to those steps by reference, so that only the locator leaves the job.
use std/ops/option::|unwrap_or
use std/secret::|locate
treatment main()
{
// Sent by reference: the Cadence.CI worker running the step reads it.
deploy: deploy(token=|unwrap_or<Secret<string>>(
|locate<string>("api:prod/deploy-token", "deploy_token", "reference", false),
"api:prod/deploy-token"
))
}Here deploy stands for a step your program dispatches, taking a Secret<string> parameter. |locate gives the transmission explicitly, and nothing for an invalid locator, hence |unwrap_or. A secret written as a plain locator never leaves the engine resolving it: given to a step running elsewhere, it is refused with an explicit error. See Distribution in the book.
Secrets for Containers
The containers of a step start before the engine of its machine: their registry credentials (pull_secret) and the secret variables of service containers (secret_env of |service_container) travel with the request for the machine. Give them with |locate and the transmission reference: a secret stored on Cadence.CI then goes by name, and Cadence.CI hands its value to the machine when it boots.
use cicd/runners::CicdDispatchEngine
use cicd/naive::simpleStep
use process/command::|command
use std/data/map::Map
use std/data/map::|map as |secret_map
use std/data/map::|entry as |secret_entry
use std/data/string_map::StringMap
use std/data/string_map::|map
use std/data/string_map::|entry
use std/ops/option::|wrap
use std/secret::|locate
use work/resources::|service_container
treatment test[dispatcher: CicdDispatchEngine]()
input trigger: Block<void>
output success: Block<void>
{
simpleStep[dispatcher=dispatcher](
name = "test",
image = "registry.example.com/app/test:latest",
pull_secret = |locate<string>("api:registry/docker-config", "registry", "reference", false),
service_containers = [
|service_container(
"postgres", 1024, 1000, 2048, _, [], "postgres:16", _,
|wrap<StringMap>(|map([|entry("POSTGRES_USER", "ci")])),
|wrap<Map>(|secret_map([
|secret_entry<Option<Secret<string>>>("POSTGRES_PASSWORD", |locate<string>("api:ci/postgres-password", "postgres_password", "reference", false))
])),
_
)
],
commands = [|command("sh", ["-c", "make test"])]
)
Self.trigger -> simpleStep.trigger
simpleStep.success -> Self.success
}The run is refused before it starts, naming the secret, when a secret it names does not exist or the rules do not let it read it, and every read is listed among the secret’s accesses. A plain locator stays on the machine that resolves it, and is refused for containers. Secrets kept on your side go by value instead, with the transmission value.
Storing Secrets from Programs
Programs producing credentials, such as a generated database password or the key of a server they created, store them for later runs with work/secret::store, or storeBytes for binary values, rather than putting them in artifacts. A rule marked Writable by runs must match the name:
use std/engine/util::startup
use std/engine/log::logError
use crypto/random::token
use work/secret::store
// Generates a password and stores it on Cadence.CI, for later runs.
treatment main()
{
startup()
generate: token(name="db_password")
storePassword: store(name="generated/db-password")
logGenerateError: logError(label="generate")
logStoreError: logError(label="store")
startup.trigger -> generate.trigger,token -> storePassword.secret
generate.error -> logGenerateError.message
storePassword.error -> logStoreError.message
}The value is revealed only to be sent to Cadence.CI. Each store adds a version that records the run.
Public Runs
Runs of an organization holding the open-source plan are public: anyone can read their logs and programs.
- A public run reads a secret only if an Administrator marked it Readable by public runs. Allow it only for values meant for public runs, such as a token limited to publishing.
- A public run never stores a secret.
- The names of the secrets a public run reads appear in its program.
- Values stay masked in logs, as in every run.
- Identity tokens of public runs have a subject of their own, which trusts set up for the organization do not accept.
Going Further
- Identity of Runs: let your cloud accounts and Vault trust your runs, without storing keys.
- Keys: their format, and what happens when one leaks.
- What Goes Through Cadence.CI.