env
Secrets and environment variables per project and environment, for apps, CI and local development.
Host: env.<d>. Binary: envd. Stage 4.
Concepts
- Secret set: variables for one project environment (
production, preview, custom), plus
team-wide shared sets that projects can reference. Variables have a name, value, secret flag (masked,
write-only in the panel), note.
- Kinds of values: plain variables, multiline values (certificates, JSON service accounts) and
files (an SSH private key, a kubeconfig,
google-services.json): stored like any secret, delivered
as a file where the consumer needs one (mounted into an app at a path, written by xvii env pull --files or in CI with mode 600), with the variable holding the path.
- SSH keys: generate an Ed25519 key pair in env (the private key never leaves it unless a consumer
asks), copy the public key to a server, a GitHub/GitLab deploy key or
authorized_keys; known_hosts
entries kept next to it. Apps and CI get the key as a file plus GIT_SSH_COMMAND; rotation makes a new
pair and shows which consumers still use the old one.
- Reference:
${{ shared.SENTRY_DSN }} or ${{ project:api.production.DATABASE_URL }}, resolved at
read time, so rotating one value updates every consumer.
- Version: every change creates a version of the set; diff and restore.
- Encryption: envelope encryption: every set has a data key (XChaCha20-Poly1305), data keys are
encrypted with the instance master key (from
/etc/xvii/env/master.key, or a KMS later). Values never
appear in logs or events (only names).
- Access: environment protection from
id (ADR 0005): protected environments need admin to read
secrets; developer reads others. Every read of a secret value is audited.
Consumers
apps resolves an app's sets at deploy time and ships them to the node with the desired state.
- CI:
xvii env run -- <cmd> or xvii env export --format dotenv with a job token exchanged at id
(GitLab id_tokens, GitHub OIDC), no stored secrets in CI settings.
- Local:
xvii env pull (writes .env, refuses to overwrite without --force), xvii env run.
- Sync out (later): push a set to GitLab CI variables or GitHub secrets.
API (highlights)
GET/POST /api/v1/sets?project=&environment= · GET/PATCH /sets/{id} · GET /sets/{id}/versions
PUT /sets/{id}/vars/{name} · DELETE /sets/{id}/vars/{name}
POST /api/v1/resolve (services and CI: resolved values for a project environment)
Events
env.changed (set, names changed, actor), env.secret_read (audit only).
As built (stage 4)
- One set per project environment (made on first use) and named shared sets per team. Variables are
var, file (delivered at a relative path, mode 600) or ssh (an Ed25519 pair made in env; the public
half shows, the private one is delivered as a file).
- Envelope encryption: XChaCha20-Poly1305 data key per set, sealed with
DATA_DIR/master.key; each value
is bound to <set>/<name>. Every change is a version holding the whole set sealed; any version
restores as a new one.
- Access by id's roles and environment protection: members see names; developers read and write
unprotected environments, admins protected ones and shared sets; tokens narrowed to a project see
only it; references can't read past the caller's rights. Reveals and resolves are logged with the
names read.
GET /api/v1/resolve?project=&environment= (team= when slugs repeat) for apps, CI and the CLI:
xvii env run|pull|export|ls|set|unset. A CI job signs in with its own token (GitLab id_tokens,
GitHub OIDC) through an id trust rule, whose team role decides what it reads.
- Services read sets with their own tokens (apps, CI resolution); of them only apps writes: the
connection strings of the databases it manages, each with a note naming the database.
- Limits
env.sets, env.vars; events env.changed (names only); MCP tools env_list_sets,
env_get_set, env_set_var, env_delete_var, env_generate_ssh_key (no secret values).