Skip to main content
Every recipe below answers the same question for a different piece of infrastructure: how does a real, validated API key reach blobhub or blobhub-worker when nobody is sitting at a terminal to type it in. Authentication and Authentication and Profiles document the mechanism — a literal flag, its own -stdin sibling, or a named -env <VAR> sibling, resolved before either binary even considers prompting. This page is worked examples: the same sources, wired into the infrastructure that actually runs BlobHub commands. Two things apply to everything below, so they’re stated once instead of five times:
  • Provision a service account’s key, not a person’s. Every recipe here hands a credential to infrastructure nobody is watching in real time. A service account key means the Blobs, revisions and thread items that infrastructure produces are authored by, say, ci-deploy — not by whoever happened to set the pipeline up — and revoking it later never touches a person’s own access.
  • Lead with --api-key-stdin (or --api-key-env), never the literal --api-key. The literal flag sits in ps output for as long as the process runs and in shell history for as long as that file exists — exactly the two places a provisioning script tends to run. Every example below pipes or points at the key instead. The same three-source shape covers --api-url too, for anything not pointed at the default https://api.blobhub.io/v1 — it’s left out of the examples below to keep them focused on the key.

When not to use login at all

An environment key resolves on its own, with no credentials file, no profile, and nothing to clean up:
That’s the whole story for a process that starts, does one thing, and exits — see Configuration for the CLI’s side of it. login earns its place over that one line when you need a stored profile, for three reasons that a bare environment variable can’t give you:
  • A name. --profile ci is something a teammate can read in a service definition; a key sitting in one process’s environment isn’t addressable at all once that process is gone.
  • A key validated before anything depends on it. login calls GET /v1/users/me and fails loudly, before any deploy or any turn runs, if the key is wrong. An unvalidated environment variable fails at the first real command instead — later, and with a less specific error.
  • For the worker specifically, a state tree that survives rotation. A state tree is named after where its key came from: a stored profile’s tree is named after the profile, so rotating the key under --profile prod keeps the same tree — same cursors, same lock. A key supplied directly by --api-key/BLOBHUB_API_KEY gets a tree named after a digest of that key, so rotating it is, from the worker’s point of view, moving to a brand new tree with no memory of the old one. Nothing breaks, but every cursor starts over.
That third point is why every recipe below that provisions a worker uses login, and why the CI recipe — the one genuinely short-lived case — is the one place a bare environment variable is usually the right answer for either binary.

CI

A CI job’s secret store hands the runner an environment variable, already masked in logs and gone the moment the job’s container or VM is torn down. For a blobhub command that runs once and exits, that’s the entire setup, on any CI platform:
If the same job needs a stored, validated profile — because it’s about to hand the key to something longer-lived, like the four recipes below — script login against the variable the platform already injected, rather than re-typing the key anywhere:
This is also usually where the key in the next four recipes originates: a CI job’s own secret store, pushed into a Kubernetes Secret, a Docker secret, or an Ansible vault as one step of the same pipeline.

One shape, three paths

Docker secrets, a Kubernetes mounted Secret, and systemd’s LoadCredential= all exist for the same reason: hand a process a file only it can read, instead of an environment variable that shows up in docker inspect, /proc/<pid>/environ, or a child process’s own environment. Whichever platform delivers it, the command on the receiving end is identical — pipe the file into --api-key-stdin:
The next three sections are that one line with three different paths, and the platform-specific reason each one chose a file over a variable in the first place.
A mount that isn’t actually there fails at the shell, not inside login< /path: No such file or directory — before the binary ever runs. A mount that is there but empty is what produces login’s own INPUT_REQUIRED, since an empty pipe is treated as no source at all. The two look similar in a log and mean opposite things: one is a missing mount, the other is a secret that resolved to nothing.

Docker and Compose secrets

A docker run -e or a Compose environment: entry is visible to docker inspect and to anything that can docker exec into the container — indefinitely, for as long as the container runs. Docker secrets exist specifically to avoid that: they’re mounted as files under /run/secrets/<name>, readable only inside the container that declared them, never inspectable from outside it. Creating the secret (Swarm) or declaring it (Compose) both land the key at the same path:
exec in the entrypoint matters: without it, start would run as a child of the shell instead of as the container’s own process, and a docker stop would have to wait out its default grace period instead of reaching the worker’s own signal handling directly.

Kubernetes

The same reasoning as Docker applies, with one more wrinkle: an environment variable sourced from a secretKeyRef is set for the life of the container and readable via that container’s own /proc/1/environ by anything sharing its process namespace. A volume-mounted Secret can be permissioned tighter and doesn’t widen that surface. Project the key to one file and mount it read-only:
A mounted Secret updates in place when the underlying object changes, but that only matters to a process that re-reads it; a one-shot login at container start won’t notice a live rotation. Roll the pod (or run login again) after rotating the key, the same as you would for any other credential baked in at startup.

systemd’s LoadCredential=

An environment variable set in a unit file is visible to systemctl show and to /proc/<pid>/environ for any local user who can read that process, and it’s inherited by every child process the unit spawns. LoadCredential= is systemd’s own answer, built for exactly this: it stages the credential in a per-unit, mode-0400 directory owned by the service’s own user, exposed only to that unit via $CREDENTIALS_DIRECTORY — nowhere else, and not to unrelated processes on the same host.
$CREDENTIALS_DIRECTORY is set for every Exec*= line in the unit, so ExecStartPre can read it and ExecStart never needs to. ExecStart=/ExecStartPre= are not run through a shell by default — that’s why the redirect is wrapped in sh -c '...' rather than written directly on the ExecStartPre= line, where systemd would pass the literal < to the binary as an argument instead of interpreting it.

Ansible

A playbook is often the thing populating the file the three recipes above expect, but it doesn’t have to write the key to disk on the target at all: ansible.builtin.command’s stdin parameter feeds a string straight to the child process’s standard input, so the raw key never touches the target’s filesystem.
no_log: true is not optional here. Once the key is a module parameter rather than a file path, Ansible’s own verbose and failure output is the next place it could leak — no_log is what keeps it out of both. Keep blobhub_api_key itself in an ansible-vault-encrypted variable file; nothing about login cares where the value came from before it reached stdin.
Keep --profile a literal or a variable you know is always set. --profile "{{ some_var }}" rendering to an empty string is refused outright on the worker (PROFILE_NOT_FOUND) rather than falling through to whatever the stored default happens to be — see the warning in Authentication and Profiles for why that’s deliberate. blobhub login on the CLI side does not share this guard and falls through instead, which is exactly the asymmetry to design around rather than discover in production.

Confirming what got stored

Both binaries can report the same thing a script would otherwise have to scrape from human-readable lines — but spelled differently, and shaped differently, which is worth seeing side by side rather than assuming one mirrors the other. --json is a root option on the CLI and a subcommand option on the worker:
The CLI wraps its payload in an envelope carrying schema_version; the worker prints the bare object with no envelope at all — the same divergence blobhub-worker login has under --json. Neither ever includes the key itself, here or on any other command. running on the worker’s payload is the one field worth watching after a deploy: it tells you whether start actually took, not just whether the credential resolved. The shapes above are one instance of a wider rule worth carrying into every script built from this page: key off the error code and the exit status, never the message text. The two binaries word the same condition differently on purpose — see Error codes — so a check written against a sentence is one release, on either side, away from breaking.

See also

  • Authentication and Authentication and Profiles — the full flag reference these recipes assume: every source, the prompting rules, and what each binary writes.
  • Service Accounts — the identity every recipe on this page should be provisioning, and its own end-to-end recipe for minting one.
  • Configuration — the credentials file, the precedence ladder, and the --json envelope in full.
  • Error codes and ReferenceINPUT_REQUIRED, ENV_VAR_NOT_SET, and every other code a scripted login can raise.