Skip to main content

Credentials

A credential is a target — driver = "credential" — that declares an identity a build needs: an ordered list of ways to obtain it, and the shape it's handed to consumers in. A consuming target names it with credentials = [...] and never learns which of the ways supplied it.

The contract

A credential grants access; it is not an input. A target's outputs must be identical whichever identity satisfied its credential requirement. A target whose output depends on who ran it is not cacheable, and says so with cache = False.

Nothing about a credential — the material, which source supplied it, the declaration, even the names of the variables it presents — enters the input hash. A cache hit acquires nothing: build the same target twice with two different identities and the second run is still a cache hit, never a re-acquire.

Declaring one

BUILD
aws = target(
name = "aws",
driver = "credential",
sources = [
# CI: exchange the runner's own OIDC token for the role, no secret
# stored anywhere.
heph.auth.oidc(
"github_actions",
audience = "sts.amazonaws.com",
present = heph.auth.aws_web_identity(
role = "arn:aws:iam::123456789012:role/deployer",
),
),
# Laptop: whatever's already logged in via the AWS CLI.
heph.auth.exec(
["aws", "configure", "export-credentials", "--profile", "acme", "--format", "process"],
fields = {
"access_key_id": "AccessKeyId",
"secret_access_key": "SecretAccessKey",
"session_token": "SessionToken",
},
expires = "Expiration",
login = [["aws", "sso", "login", "--profile", "acme"]],
present = heph.auth.aws_process(),
),
],
)

target(
name = "deploy",
driver = "bash",
credentials = [aws],
tools = ["//tools:terraform"],
env = {"AWS_REGION": "eu-west-1"}, # selects bytes — stays an ordinary hashed input
run = "terraform apply -auto-approve",
cache = False, # this target's output depends on which account it ran against
)

//svc:deploy says one word — credentials = [aws] — with no branching on CI and no environment names. The declaration is environment-independent; the chain inside it isn't. AWS_REGION stays on the consumer because it selects which bytes come back, not because it's a secret.

The chain

sources is ordered, and the environment picks the winner, not the author. A source's probe answers "is this applicable here?", not "will it succeed?" — the first applicable source is the one used; if its acquire step fails, that's a hard failure, not a fallthrough to the next source. heph auth explain <addr> prints the walk: every source, why each was skipped or chosen.

SourceConstructorProbe
Environment variablesheph.auth.env(names)Every named variable is set
A fileheph.auth.file(path, fields=None, expires=None)The path exists
A commandheph.auth.exec(run, fields=None, expires=None, login=None, runner=None)run's program resolves
Host paths, in placeheph.auth.passthrough(paths, env=None, login=None)Every named path exists
A CI provider's own tokenheph.auth.oidc(provider="github_actions", audience=None)That CI provider is detected
Another targeta bare target address (no wrapper)none — selected by when instead

heph.auth.exec's command must print JSON to stdout; fields maps a material field name to a key in that JSON, and expires names the key holding an absolute timestamp or a duration in seconds. heph.auth.file uses the same fields/expires shape against a file's contents instead of a command's stdout, or presents the whole file as ${value} when fields is omitted. heph.auth.passthrough exposes existing host paths without copying them, each reachable in a presentation as ${file:<name>}.

Every source also accepts:

OptionMeaning
whenRestrict this source to a condition instead of probing for one — see below.
credentialsCredentials this source's own acquire step needs (a command that must itself authenticate to a secret manager, say).
presentOverride the credential's presentation for material from this source only.
hintText shown by heph auth explain for this source.

when

A closed vocabulary, not an expression language:

ValueMatches
"ci"Any detected CI provider
"ci:<provider>"One named provider — "ci:github_actions", "ci:gitlab_ci", "ci:buildkite", "ci:circleci", "ci:jenkins"
"interactive"A terminal is attached
"env:NAME"A named host environment variable is set and non-empty
"os:linux" / "os:darwin"The build host's OS

A source that is a target address has no cheap probe — you can't ask "is this applicable?" of a target without running it — so it's always selected by when rather than probed.

A source that's a target

When acquiring needs tools, dependencies, or more than one command, make it an ordinary target instead of an inline heph.auth.exec:

BUILD
mint = target(
name = "mint",
driver = "bash",
cache = False, # required of every credential source
out = {"credential": "cred.json"}, # the reserved group, read as JSON
run = 'vault kv get -format=json -field=data secret/cloudflare > $OUT_CREDENTIAL',
)

cf = target(
name = "cf",
driver = "credential",
sources = [mint],
present = {"env": {"CLOUDFLARE_API_TOKEN": "${token}"}},
)

An output group named credential is read as JSON and its top-level keys become material fields; every other output group becomes a named file, reachable in a presentation as ${file:<group>}. Expiry comes from an expires_at or expires_in key in that same JSON, or from the credential's own ttl.

The target must be cache = False — its output is material, and heph refuses to let it become a cached, shareable artifact.

Presentation

present decides how material reaches a consumer's sandbox — set on the credential target itself, or per source to override it for that source's material:

present = {
"env": {"NAME": "${field}"}, # environment variables
"files": {"name": "${field}"}, # files, written mode 0600
"helper": "dialect", # a callback the consuming tool invokes
}

At least one of the three is required. Only material and the handles needed to use it belong here — a region, an account id, a profile name all select which bytes a tool talks to, so they stay ordinary hashed config on the consumer, not the credential.

ShapeSurvives the material expiring mid-run?
helperYes — the consuming tool calls back for a fresh credential whenever it needs one.
filesSometimes — depends on whether the tool re-reads the file on refresh.
envNo — handed over once; the target fails if it expires while still running.

Template vocabulary

Values inside present are templates:

WrittenResolves to
${<field>}A material field, by name.
${file:<name>}The absolute path of a presented file.
${helper:command} / ${helper:args}How to invoke the callback (for a hand-written helper config).
$$A literal $.

A present value is also a deferred-value field, so role = "${read://infra/aws:role-arn}" works the same way it does anywhere else that accepts one — read the owner of the value from another target instead of copying it into the BUILD file.

Presentation presets

Hand-rolling a third-party wire format is how you get a subtly wrong one, so common ones ship as presets. Each returns a present dict — use one directly in a source's or credential's present =:

PresetParametersProduces
heph.auth.aws_process()An AWS credential-process callback.
heph.auth.aws_web_identity(role, session_name="heph")role, session_nameA web-identity token file plus AWS_WEB_IDENTITY_TOKEN_FILE/AWS_ROLE_ARN/AWS_ROLE_SESSION_NAME.
heph.auth.gcp(audience, impersonate=None)audience, impersonateA generated external_account file plus GOOGLE_APPLICATION_CREDENTIALS.
heph.auth.azure_workload(client_id, tenant_id)client_id, tenant_idA federated token file plus AZURE_FEDERATED_TOKEN_FILE/AZURE_CLIENT_ID/AZURE_TENANT_ID.
heph.auth.github(hosts="github.com")hostsGH_TOKEN plus a git credential callback for the named host(s).
heph.auth.docker(registries)registriesA Docker credential-helper callback for the named registries.
heph.auth.git(hosts)hostsA git credential callback for the named hosts.
heph.auth.netrc(machines, login="${username}", password="${token}")machines, login, passwordA generated netrc file plus NETRC.

Using a credential

Reference it by address on a consuming target:

BUILD
target(
name = "fetch",
driver = "bash",
credentials = ["//auth:github"],
run = 'curl -sf -H "Authorization: token $GH_TOKEN" https://api.github.com/user > $OUT',
out = "response.json",
)

Whether the target should still cache depends on the contract above, not on the fact that it uses a credential: if the output would be identical regardless of which identity ran it, leave caching on; if the output embeds the identity — a plan naming an account, a presigned URL — set cache = False.

A target cannot reference the same credential twice, and two credentials can't both present the same variable name to one target — both are rejected at parse time rather than silently picked between.

The CLI

CommandDoes
heph auth statusOne row per declared credential, its state, source, and expiry. --json for machine-readable output.
heph auth explain <addr>Walks the whole chain: every source, why each was skipped, which won.
heph auth login [addr]Runs whichever login commands their probes found stale.
heph auth logoutClears cached material from disk.

A build never signs you in on its own — it fails, naming the exact heph auth login command to run.

Redaction

Declared credential material is scrubbed from a target's captured output before it's written anywhere, so a build step that echoes its own token prints [redacted] instead. It's best-effort: material shorter than 8 bytes isn't scrubbed, a secret the target transformed (base64, URL-encoded) isn't recognized, and only material fields are scrubbed — the contents of a presented file are not, so cat-ing a credential file still leaks it.