Secrets

Give your sessions a private registry token or a test API key without putting it in the session. A secret belongs to your organization; a session holds only a placeholder for it, and the value is attached outside the session, on the way out, to requests to the hosts the secret is bound to.

Session secrets need the installation to keep a secret store. In eu1 this is not turned on yet: ciresk dev secrets set and ciresk dev secrets rm answer secrets_unavailable, and ciresk dev secrets list answers what is stored, which is nothing. Until it is on, no session gets a secret, and a private dotfiles token cannot be stored either.

How it works

  1. An admin stores a secret with its hosts, the request header it goes in, and which sessions get it.

  2. When a session starts, it is granted every secret that selects it. Each container of the session gets the variable CIRESK_SECRET_<NAME> (the name upper-cased, - as _), holding a placeholder of the form ciresk_placeholder_<name>.<nonce>. The nonce is random and new for every session, so the placeholder is useless anywhere else.

  3. You configure the client with the variable, for example in .npmrc:

    .npmrc
    //registry.acme.com/:_authToken=${CIRESK_SECRET_NPM_TOKEN}
  4. When the client sends a request over HTTPS to one of the secret's hosts, the session's egress proxy ends TLS for that connection, swaps the placeholder in the secret's header for the value, and opens its own TLS connection to the host, verified against the public roots. Every other connection stays an opaque tunnel, so the value never reaches a host the secret does not name.

The value never enters the session: not in the environment, not in a file, not in a checkpoint or a diagnostics bundle.

Create or replace a secret

Terminal
ciresk dev secrets set npm-token --host registry.acme.com --repo '*' < token.txt

The value is read from standard input only, never from an argument or an environment variable. One trailing newline is dropped, so echo and files both work. When standard input is a terminal the command refuses, so the value is never echoed or kept in your shell history. No command shows the value again.

FlagDefaultWhat it sets
<name>requiredthe secret's name: lower-case letters, digits, - and _, at most 63 characters, starting with a letter or digit, not starting with model_ or ciresk_ (names Ciresk keeps for itself)
--host <HOST>required, repeatablea host the value is attached for, exact: registry.acme.com or registry.acme.com:8443 (port 443 when you give none); 1 to 16 hosts
--header <NAME>authorizationthe request header the value goes in, for example x-api-key or private-token
--repo <*|REMOTE>required, repeatable* for every repository, or one repository as a remote URL or host/owner/name; 1 to 64 entries
--template <NAME>any templateonly sessions started from this template get the secret; repeatable, up to 64
--agent-sessionsoffsessions started by an agent, a service or an API key get the secret too
--purpose <proxy|git>proxyproxy attaches the value to the session's requests to its hosts; git makes it the credential ciresk dev push uses (see git.md)
--keep-valueoffsend no value and keep the stored one; only for a pure narrowing

Setting a name that exists replaces its settings and raises its version. The answer prints the version, the scope and the variable sessions send.

What a secret accepts:

  • Hosts are exact, lower-case names. A wildcard is refused, because it also covers names a third party hosts under your domain. An IP address is refused. A host of a model or tool vendor is refused (model_or_tool_vendor in the detail).
  • Header is a lower-case header name of up to 64 characters. Headers the proxy owns or removes are refused: connection, keep-alive, te, trailer, trailers, transfer-encoding, upgrade, host, cookie, forwarded, content-length, expect, x-original-url, x-rewrite-url, x-http-method-override, x-http-method, x-method-override, and every x-forwarded-* and proxy-* header.
  • Value is 16 to 8192 bytes of UTF-8 with no control character.

When the value is needed again

You must send the value again whenever a change could point the stored value somewhere new: a host added, a different header, a different purpose, more repositories or templates, or agent sessions turned on. Without it the change is refused with value_required. A pure narrowing (fewer hosts, repositories or templates, agent sessions turned off) may use --keep-value:

Terminal
ciresk dev secrets set npm-token --host registry.acme.com --repo github.com/acme/app --keep-value

List secrets

Terminal
ciresk dev secrets list
ciresk dev secrets list --json

One row per secret, up to 500: its name, purpose, header, hosts, repositories, templates, whether agent sessions get it, and when and by whom it was last set. Never a value. --json returns {secrets: [...]}, each entry with name, purpose, header, hosts, repos, templates, agent_sessions, version, updated_by and updated_at. Personal dotfiles tokens are not in this list (see Private dotfiles tokens).

Remove a secret

Terminal
ciresk dev secrets rm npm-token

The secret and its value are deleted. Running sessions lose it within 5 minutes. To cut a session off at once, stop it with the kill switch (see admin.md).

Which sessions get a secret

A session gets a secret when all of these hold when it starts:

  • the secret's repositories include * or the session's repository;
  • the secret names no template, or names the session's template;
  • the session was started by a person, or the secret allows agent sessions. A session started by an agent identity, a service or an API key is not a person's. An agent working under a person's own sign-in, on their laptop, is that person.

Repositories, templates and agent sessions choose which sessions get a secret. They are not an access boundary: any member who may start a session in your organization can start one that the secret selects, and use the secret through the proxy against its hosts. The boundary is your organization.

When changes reach a running session

The worker holding a session fetches its secrets' values when the session starts and again every 5 minutes. A session's placeholders and the certificate authority it trusts are fixed when it starts:

ChangeA running session
Secret removed, or narrowed so it no longer selects the sessionloses it within 5 minutes
New valueuses it within 5 minutes
Host removedstops attaching for it within 5 minutes
Host addednot attached until a new session; run ciresk dev up --recreate
New secret, or one widened to select the sessionnot in the session until a new session; run ciresk dev up --recreate

A session that is granted secrets never runs without them. If the worker cannot fetch the values when the session starts, or cannot set up the session's certificate authority, the session does not start: ciresk dev up answers session_failed with the reason secrets_not_delivered. Run ciresk dev up --recreate to try again. A session whose granted secrets were all removed or narrowed away before it started starts without them.

Values are handed out only while the session's worker may keep running it: a stopping, killed or paused session gets none, and the proxy drops the ones it holds. After a worker restart, a session that keeps running gets its values again at the next fetch.

What a session sees

In every container of your template in a session with at least one secret, and in every ciresk dev exec:

Variable or fileWhat it holds
CIRESK_SECRET_<NAME>the placeholder, one per secret of purpose proxy (CIRESK_SECRET_CIRESK_DOTFILES for your dotfiles token)
/etc/ciresk/extra-ca.pem, NODE_EXTRA_CA_CERTSthe session's own certificate authority (and, when the session has models, the installation's)
/etc/ssl/certs/ca-certificates.crt, SSL_CERT_FILE, REQUESTS_CA_BUNDLEthe system roots followed by those authorities

The session's certificate authority is minted for that session only and is name-constrained to the secrets' hosts, so it vouches for nothing else. Its key stays on the worker. A session without secrets gets no authority, no variables and no interception.

CIRESK_SECRET_* and REQUESTS_CA_BUNDLE are reserved: a template that sets one is refused with reserved_env, and ciresk dev exec refuses them with env_not_allowed.

A secret of purpose git gets no placeholder: nothing in the session stands for it.

Where the value is attached

The proxy attaches the value only in the secret's header, and only when that header holds exactly the placeholder, <scheme> <placeholder> (Bearer ...), or, for authorization, Basic credentials whose user or password is exactly the placeholder (git, pip and docker send Basic). The request line and the body are never touched.

A request that is refused is answered 403, nothing is sent to the host, and the decision log records the secret rule (see egress.md):

  • a placeholder anywhere else: another header, a header name, another form;
  • a placeholder that does not resolve: not granted to this session, bound to another host or header, or a wrong nonce;
  • a placeholder in a plain http:// request: a value never crosses the network in clear.

A secret's hosts never widen egress. The destination must still pass the session's egress policy first: the organization's ceilings, the template's egress rows, and the installation's egress.yaml rows for the session product dev-session. Rows the installation keeps for Ciresk's own control plane (dev) or for the worker's push (dev-push) never let a session out, so a bound host needs a dev-session row too.

On a connection to a bound host:

  • The client must send the host as its TLS server name, and speak HTTP/1.1. Each connection carries one request and is answered with Connection: close.
  • The Host header must name the connection's host, or the answer is 421 and nothing is attached.
  • TRACE, TRACK and CONNECT are refused with 405, an upgrade (WebSocket) with 501. Ambiguous framing (both Content-Length and Transfer-Encoding, two Content-Length headers, a bare LF) is refused with 400.
  • Forwarding headers (Forwarded, X-Forwarded-*, X-Original-URL, X-Rewrite-URL) are removed.
  • In response headers, each of the session's values is replaced by its placeholder. Trailers are dropped.

What does not work today:

  • A host behind a private certificate authority: the proxy verifies hosts against the public roots only.
  • A host on a private address, which the egress policy refuses.
  • A client that pins certificates, or ignores both the system bundle and the three variables: its TLS to a bound host fails. Other hosts are not affected.

What a bound host returns enters the session. A host that echoes a request header back in a response body, or that mints a new credential from the value (a registry's token endpoint, a token API), hands the session what it returns. Bind only hosts and tokens scoped so that does not matter, such as a read-only registry token.

Do not bind a proxy secret to your git forge's host: the session's own requests to it would carry the value, so ciresk dev push refuses with push_credential_proxied while one does. Store the push credential with --purpose git instead (see git.md).

Private dotfiles tokens

A private dotfiles repository's token is a secret of its own, stored by you for your sessions, not by an admin for the organization:

Terminal
ciresk dev dotfiles set --repo https://github.com/me/dotfiles --token-stdin < token.txt

It travels the same way as an organization secret of purpose proxy: kept in the installation's secret store, never shown again, delivered only to the worker running the session, and attached by the session's egress proxy while the shell clones the repository. The session holds the placeholder CIRESK_SECRET_CIRESK_DOTFILES, never the token. It differs in these ways:

Organization secretDotfiles token
Who stores itan owner or admin, ciresk dev secrets set (dev:secrets.write)you, for yourself, ciresk dev dotfiles set --token-stdin (dev:sessions.write); never an agent
Where it is attachedthe secret's hosts and headerthe dotfiles repository's host on port 443, in Authorization
Which sessions get itevery session it selects, by repository, template and agent sessionsonly sessions you start yourself; a session an agent identity, a service or a key starts skips your dotfiles with the reason agent_session
Listed byciresk dev secrets listciresk dev dotfiles show (credential stored), never the token
Removed byciresk dev secrets rmciresk dev dotfiles rm, which removes the repository and its token; setting a repository on another host also drops the token

The token is 16 to 8192 bytes with no control character; a token off that rule is refused with dotfiles_invalid, and a token without a dotfiles repository with dotfiles_unset. Any process of yours in the session can use the token against the repository's host, so use a read-only token scoped to that one repository. The host must pass the session's egress like any other destination. See sessions.md for dotfiles themselves.

Images from your own registry

A template's service can pull a pinned image from a private registry with an organization secret: pull: {secret: <name>, user: <registry user>} (see templates.md). The worker uses the value for the pull only. ciresk dev up refuses a template whose pull secret the session is not granted with pull_secret_not_granted, naming the secret.

Who may do what

OperationCommandScopeWho may call it
List secretsciresk dev secrets listdev:secrets.readpeople, agents, services
Create, replace or remove a secretciresk dev secrets set, ciresk dev secrets rmdev:secrets.writepeople only
Store your dotfiles tokenciresk dev dotfiles set --token-stdindev:sessions.writepeople only, for themselves

Owners and admins hold both secrets scopes; members and viewers hold dev:secrets.read. No one can read a value back: no command, API call or tool returns one, and it is never logged, never in an error and never in an audit record.

The audit log records dev.secrets.set (the name, the fields that changed, and whether the value changed), dev.secrets.delete, dev.dotfiles.credential.set (the host, never the token), and dev.session.secrets each time a worker is given a session's secrets, naming them.

Values are delivered to the worker running the session. When your organization runs sessions on its own hosts, those hosts hold your secrets' values while they run your sessions.

Agents

An agent can see which secrets exist, never a value, and can never set or remove one, nor store a dotfiles token: those calls are refused with principal_not_allowed.

  • With the MCP server, dev_secrets_list answers what ciresk dev secrets list --json does.
  • An agent uses a secret by putting ${CIRESK_SECRET_<NAME>} where the token belongs in the client's configuration. When a secret is missing, it asks a person to run ciresk dev secrets set.
  • A session an agent identity starts gets only the secrets set with --agent-sessions, and never its owner's dotfiles token.

Errors

CodeWhenWhat to do
secret_invalida name, host, header, repository, template or value breaks its ruleciresk dev secrets set --help
value_requireda new secret, or a widening change, without a valuesend the value on standard input, without --keep-value
secret_not_foundrm names no secretciresk dev secrets list
secrets_unavailablethe installation keeps no secret storeask the installation's operator
pull_secret_not_granteda template's pull secret does not select the sessionciresk dev secrets set <name> --host <registry> --repo <this repository>
session_failed with secrets_not_deliveredthe worker could not deliver the session's secrets at startciresk dev up --recreate
principal_not_allowedan agent or service called set or rmask a person

The API is in the API reference: GET /v1/dev/secrets, PUT /v1/dev/secrets/{name} and DELETE /v1/dev/secrets/{name}; the dotfiles token is PUT /v1/dev/dotfiles/credential.