Kumoss keeps every secret in environment variables. config.yaml holds
only non-secret settings and, where a secret is needed, the name of
the variable that carries it (fields ending in _env). This page
documents every variable in core/env.sample and in the four sidecar
env.sample files, plus the variables that the IaC engine and the LLM
providers read on their own.
Each env.sample is a template: copy it to a .env file next to it
(core/.env, services/<name>/.env). Docker Compose loads those .env
files per container. core/.env is required for the stack to start;
the sidecar files are optional and the containers fall back to their
built-in defaults when a file is missing.
All example values in this page are fictitious. Never commit a .env
file; they are gitignored.
Which files you need depends on the deployment model:
Quickstart needs core/.env and
services/iac/.env (plus the notifications file when enabled);
Deploy to production
replaces the files with a secret store but keeps the same variable
names.
Related guides: Configuration, LLM providers and models, Enable authentication.
Core (core/env.sample)
The core reads variables through the names configured in
config.yaml. The table lists the default names; if you rename a
*_env field, rename the variable accordingly.
| Variable | Requirement | Default | Meaning | Checked |
|---|---|---|---|---|
LLM provider credentials ( |
Mandatory for the provider selected by |
none |
Credentials that LiteLLM reads for the configured provider. Names
follow LiteLLM conventions; see
LLM providers and models.
|
At boot for providers LiteLLM can validate; otherwise on the first LLM call |
|
Mandatory (the IaC sidecar is always called; it has no |
sample value |
Bearer token the core sends to the IaC sidecar. Must equal
|
At boot: an empty token aborts startup |
|
Conditional |
sample value |
Bearer token for the mapping sidecar, sent when
|
At boot when enabled |
|
Conditional |
sample value |
Bearer token for the notifications sidecar, sent when
|
At boot when enabled |
|
Conditional |
sample value |
Bearer token for the authorization sidecar, sent when
|
At boot when enabled |
|
Optional but needed for pushes |
empty |
Account username at the Git provider selected by |
At boot the core logs a warning when empty; pushes fail later unless credentials come from elsewhere |
|
Optional but needed for pushes |
empty |
Personal access token for that account. Written with |
Same as |
|
Conditional |
|
Access key for the S3-compatible store, read when |
At boot: the core creates or checks every configured bucket and aborts if the store is unusable |
|
Conditional |
|
Secret key for the artifact store, matching the secret key of
|
At boot |
|
Conditional |
empty |
Shared key of the Azure storage account, read when
|
At boot: configuration validation fails if empty |
|
Mandatory |
sample value points at the bundled |
Connection URL of Kumoss’s PostgreSQL database. Must carry the
credentials of the |
At boot: configuration validation fails if empty, and database initialisation fails if unreachable |
|
Optional; not present in |
|
Redis URL for the session cache. Add it only to point at an external Redis — for example when the URL embeds a password or sits outside the compose network. Cache operations fail open at runtime, but the boot-time ping must succeed. |
At boot: Redis initialisation fails if unreachable |
|
Optional; not present in |
|
Path, inside the container, of the configuration file to load. The file must exist at that path (for example through a mounted volume); a missing file silently falls back to built-in defaults. |
At boot |
|
Optional; not present in |
The release version of the source the core was built from |
Version string reported by the API’s OpenAPI document. Leave it unset to report the built release; set it only to override that, for example on a patched build. |
never |
The rustfsadmin fallback applies only when the variable is absent
from the environment. A variable that is present but blank
(RUSTFS_ACCESS_KEY= in core/.env) resolves to the empty string, no
fallback is applied, and under storage.provider: RUSTFS the core
embeds no static credentials at all: boto3 falls back to the AWS
default credential chain, which has nothing to find in the container,
and boot fails at the bucket check. Blank the two keys only for
storage.provider: S3 with an instance profile or IRSA, where an empty
value is what tells the core to omit them and let the AWS default chain
resolve.
What the boot check actually covers. The core validates the LLM
credentials with litellm.validate_environment for every entry in the
effective model list. Coverage depends on the provider. For the shipped
anthropic/… models a missing key fails the boot. For azure_ai/…
models the check verifies nothing: LiteLLM has no validation mapping
for that provider, so a missing AZURE_AI_API_KEY or AZURE_AI_API_BASE
boots cleanly and fails on the first LLM call. For
vertex_ai/… models the check requires VERTEXAI_PROJECT and
VERTEXAI_LOCATION only; VERTEXAI_CREDENTIALS is not verified at
boot, and when it is unset LiteLLM falls back to Application Default
Credentials. Coverage is per provider (for anthropic/*, either
ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN satisfies it), and no
provider’s check validates that a credential actually works.
litellm loads core/.env by itself. The library calls
load_dotenv() at import time unless LITELLM_MODE is PRODUCTION,
so running the core or its test suite outside Docker silently picks
up core/.env even when you meant to run with a clean environment.
Running from another directory does not avoid it: python-dotenv
searches upwards from the calling package’s directory, not from the
working directory, so a virtual environment at core/.venv finds
core/.env from anywhere. Variables already set in the environment
are left alone. To reproduce a missing-credential failure, set
LITELLM_MODE=PRODUCTION, move core/.env aside, or use a virtual
environment outside core/.
Two related facts about the sample file:
-
The pre-filled
dev-*-tokenplaceholders are accepted because the bundled sidecars ship with an empty expected token, which disables their bearer check. That is acceptable only on an isolated workstation. -
Nothing OIDC-related goes in
core/.env. The single-page application is a public client using PKCE (Proof Key for Code Exchange), and the core validates tokens with the issuer’s public keys. All OIDC settings live inconfig.yaml.
Read by libraries, not by Kumoss
Two more variables can matter to the core even though no Kumoss code
reads them. Neither is in core/env.sample, and neither is needed for
the bundled Phoenix.
| Variable | Read by | Meaning |
|---|---|---|
|
the OpenTelemetry SDK |
Comma-separated |
|
the |
API key for an authenticated Phoenix instance, used when fetching and
seeding prompt templates. The core constructs |
Because the core passes the endpoint and base URL programmatically, whether each library still picks these variables up from the environment in that configuration is not verified in this repository — consult the OpenTelemetry Python and Phoenix client documentation for the version you run before relying on them.
IaC sidecar (services/iac/env.sample)
| Variable | Requirement | Default | Meaning | Checked |
|---|---|---|---|---|
|
Recommended; mandatory outside an isolated workstation |
empty (accepts any bearer) |
Token the sidecar requires on every |
Per request: a mismatch returns |
|
Optional |
|
Name or absolute path of the IaC engine CLI. |
At boot: the service refuses to start if the binary cannot be found |
|
Optional |
empty (unset) |
Path to a |
No: unlike |
The job retention period (how long a finished job stays pollable at
GET /v1/jobs/{job_id}, one hour) is a constant in
the sidecar’s own configuration, not an environment variable. Jobs live in
memory, so a restart also forgets them.
Cloud credentials for the IaC engine
The IaC sidecar does not interpret cloud credentials. It launches the
engine with its entire container environment inherited and no
allowlist, so every variable in services/iac/.env (including
KUMOSS_IAC_TOKEN) is visible to the Terraform and OpenTofu providers
and to any external or local-exec code in the repositories you run.
The providers read their credentials directly during init, plan,
and apply. Missing or invalid credentials never stop the container;
the command runs and the engine’s own authentication error appears in
the job’s stderr, which the session shows to the user. Keep only the
variables the engine needs in that file, and treat the sidecar’s
environment as exposed to the IaC code it executes.
The sidecar’s /v1/import/scope-resource-ids endpoint is the one
exception to "does not interpret": it launches no engine and instead
reads the same ARM_*, GOOGLE_*, and AWS_* credential variables
itself through the clouds' auth libraries to query Azure Resource
Graph, GCP Cloud Asset Inventory, or AWS Resource Explorer. Two extra
requirements apply to it only: on AWS AWS_REGION or
AWS_DEFAULT_REGION must be set and Resource Explorer must be enabled
for the account; on GCP the
cloudasset.googleapis.com and cloudresourcemanager.googleapis.com
APIs must be enabled. The core does not call this endpoint today. See
Import discovery in
the IaC sidecar reference.
The sample file lists no cloud variables; add the ones your modules need. Terraform providers gives the minimum set per cloud and authentication method (service principal, OIDC federation, managed identity, named profile, and so on) plus the state-backend minimums, and its exhaustive section lists every variable each provider and backend reads. The table below is a short orientation; the sidecar imposes nothing beyond what the provider supports.
| Cloud | Typical variables | Provider chain (examples, not a complete list) |
|---|---|---|
Azure ( |
|
Service principal with client secret. Certificate, OIDC federation,
and managed identity are other documented provider options.
|
Google Cloud ( |
|
|
AWS ( |
|
Static keys, or a profile from a mounted credentials file. Instance roles and web-identity federation are other documented provider options. |
Oracle Cloud ( |
|
The provider’s documented mechanisms, for example an OCI configuration file mounted into the container, or instance principals. |
Kubernetes ( |
|
A mounted kubeconfig ( |
These are the provider credentials, the ones plan and apply use
on the resources themselves. init needs a second, separate grant on
the state store, which may be a different account or even a different
cloud, and which some variables serve exclusively — ARM_ACCESS_KEY,
ARM_SAS_TOKEN, ARM_USE_AZUREAD, GOOGLE_BACKEND_CREDENTIALS,
GOOGLE_IMPERSONATE_SERVICE_ACCOUNT. Both grants live in the same
services/iac/.env; which variables you need for which is in
Two
sets of credentials on the sidecar.
Credential files you mount must be readable by the unprivileged user
the image runs as (kumoss, uid and gid 10001 by default).
Credentials for the state backend
The sidecar is the process that reads and writes Terraform state, so state-backend credentials are always a sidecar concern.
-
By default (
storage.terraform_state_bucketnon-blank — that is the shipped value), Kumoss manages state. When the core’s rendered backend block contains static keys (RUSTFS, orS3/STORAGE_ACCOUNTwith keys set incore/.env), the sidecar needs nothing extra for state. -
When Kumoss-managed state is on and the block omits those keys (
provider: S3withRUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEYblank), the engine resolves credentials from the sidecar’s own environment:AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY,AWS_PROFILE, or an attached instance role or IRSA token. Granting that role to the core container only is the common mistake — the core creates the bucket at boot. -
With Kumoss-managed state on, the sidecar must also reach
storage.endpoint_url. In the Compose stackcoreandiacsharebridge-network, sohttp://object-storage:9000resolves; elsewhere, allow that egress. -
If a deployment sets
storage.terraform_state_bucketto"", the backend comes from the target repository instead, and the sidecar must hold whatever that backend needs — S3 keys or a role, an Azure identity, a GCS service account. Nothing on the core side helps here.
Where state lives, and how each provider is configured, is covered in Configure state backends.
Mapping sidecar (services/mapping/env.sample)
| Variable | Requirement | Default | Meaning | Checked |
|---|---|---|---|---|
|
Recommended when the sidecar is enabled |
empty (accepts any bearer) |
Token required on |
Per request |
The bundled mapping service is an identity passthrough: it returns the
identifier it receives as both repo_url and identifier, echoes back
any terraform_provider it was sent, and always answers null for
scope_id — it knows nothing it was not told, and null means "ask the
user". While services.mapping.enabled is false (the default) the
core performs that same mapping itself and never calls the service.
Once enabled, a sidecar that times out or is unreachable makes the
wizard’s resolve step fail with 504 or 502, and so does a response
the core cannot parse or one naming a provider outside the contract’s
enum; there is no silent fallback.
Notifications sidecar (services/notifications/env.sample)
| Variable | Requirement | Default | Meaning | Checked |
|---|---|---|---|---|
|
Recommended when the sidecar is enabled |
empty (accepts any bearer) |
Token required on |
Per request |
|
Mandatory for the bundled container to start |
empty |
Slack incoming-webhook URL the service posts to. Treat it as a secret: anyone holding it can post to the channel. The service never logs it. |
At boot: the container exits with |
|
Optional |
|
Root log level ( |
At boot: an unknown level name aborts startup |
The core sends four kinds of notification on its own:
iac.compliance.failed and iac.impact.high at the end of a generate
round, iac.apply.failure when an apply fails, and
system.exception.failure when a background run raises. Users can
also send free-form support requests through POST
/api/v1/notifications (see Admin
portal). The audience of every notification is the session owner (or
the caller) plus every user with a panel role of editor or higher.
Delivery is best-effort: the core logs Failed to send '<kind>'
notification and continues. The bundled sidecar delivers synchronously
to Slack with a 10-second budget and answers 502 when Slack rejects
the message.
The bundled notifications container refuses to start without
SLACK_WEBHOOK_URL, even when the core integration is disabled.
Because Compose starts every sidecar regardless of config.yaml, a
notifications container that exits immediately is expected and harmless
while services.notifications.enabled is false. No restart policy is
set for it (only proxy carries restart: on-failure), so it exits once and then shows as
Exited in docker compose ps — it is not restarted in a loop. Remove
the service from your Compose file or provide a webhook URL if you
would rather not see it.
Authorization sidecar (services/authz/env.sample)
| Variable | Requirement | Default | Meaning | Checked |
|---|---|---|---|---|
|
Recommended when the sidecar is enabled |
empty (accepts any bearer) |
Token required on |
Per request |
|
Optional |
empty |
When set, the sidecar grants its own |
At boot |
|
Optional |
|
|
At boot |
|
Optional |
|
Path of the JSON file holding the sidecar’s user-to-roles map. The path is container-local in the shipped Compose file (no volume is mounted), so assignments are lost when the container is recreated; point it at a mounted path if you need them to survive. |
On first write |
Two things to keep apart:
-
The bundled authorization sidecar is a permissive reference implementation. It exists so that the contract in
contracts/openapi/authz.v1.yamlhas a runnable example. The core consults it only fromPOST /v1/auth/authorize(a cloud-project access check) and only whenservices.authz.enabledistrue. Enabling it without replacing the logic changes nothing about who is allowed to do what. While disabled, the core answers "authorized" without any call; once enabled, an unreachable or slow sidecar makes the preflight fail with502or504rather than allowing the request. -
The sidecar’s
adminanduserroles, its root-admin bootstrap, and its/v1/usersendpoints belong to the sidecar alone. Kumoss’s own operation roles (developer,devops) and admin-panel roles (viewer,editor,admin) live in the core database and are documented in the Admin portal guide.
Cloud-provider credentials at a glance
| Consumer | Where | Purpose |
|---|---|---|
Terraform or OpenTofu providers |
|
|
AWS SDK (boto3) in the core |
|
Artifact storage when |
Azure Storage shared key in the core |
|
Artifact storage when |
LiteLLM in the core |
|
Cloud-hosted LLM providers (Vertex AI, Bedrock, Azure OpenAI, and so on) |
Cloud credentials for the IaC engine and for the LLM provider are
independent. A stack that generates AWS infrastructure with an Azure
OpenAI model needs AWS credentials in services/iac/.env and Azure
OpenAI credentials in core/.env.
LLM-provider credentials
Variable names follow LiteLLM’s provider conventions and are selected
by the llm.model and llm.small_model strings. The complete
per-provider table, the os.environ/VARIABLE_NAME syntax for custom
names, and the limits of boot-time validation are documented in
LLM providers and models.
Credential handling and production recommendations
-
Generate bearer tokens with a cryptographic generator. For example:
openssl rand -hex 32Paste the output into both
core/.envand the matching sidecar.env. Generate a distinct value per sidecar. Do not reuse thedev-*-tokenplaceholders or an empty sidecar token outside an isolated workstation: an empty sidecar token disables its bearer check entirely. -
Never put secrets in
config.yaml. It is baked into the core image and is meant to be shareable. Use the*_envfields to rename variables if your platform imposes naming conventions. -
Keep sidecars private. The bundled sidecars compare bearer tokens in constant time, but they are still simple services: keep them on a private network that only the core can reach, and rotate tokens on a schedule.
-
All five Kumoss-built images run unprivileged. The core, IaC, mapping, notifications, and authorization Dockerfiles each create and switch to the
kumossuser (uid/gid10001by default, from theKUMOSS_UID/KUMOSS_GIDbuild args). Theproxyservice is the upstream nginx image, which starts as root and drops privileges for its worker processes. Apply your platform’s pod or container security defaults on top regardless — the Compose file adds nocap_drop,read_only, or resource limits. -
Prefer a secret manager or orchestrator secrets (Docker secrets, Kubernetes Secrets, a vault) over plaintext
.envfiles in shared and production deployments. Mount files with permissions readable only by uid10001. -
Scope Git tokens narrowly.
GIT_TOKENis single-tenant: one token pushes every session’s branch and opens every pull request. Give it the minimum repository permissions your provider offers. -
Rotate the RustFS keys and database password before exposing the stack beyond a workstation.
docker-compose.ymlsets no RustFS keys, so RustFS uses its built-inrustfsadmin/rustfsadmin. SetRUSTFS_ACCESS_KEY/RUSTFS_SECRET_KEYonobject-storageand the same values incore/.env. The compose file also hard-codes thepostgresdatabase password. The same file hard-codesPHOENIX_SQL_DATABASE_URLwith thephoenix-dbpassword in it — change it outside a workstation. The RustFS web console is disabled (RUSTFS_CONSOLE_ENABLE=false); enabling it is up to you, see RustFS console. -
Restrict who can reach the stack while OIDC is disabled. With a blank
oidc.issuer_url, every request is treated as a fully privileged local developer. See Enable authentication.
Example .env sets
All values below are fictitious. Replace every token and URL.
Core plus IaC sidecar only (Anthropic models)
Only the IaC sidecar is enabled, and the model strings are the shipped
ones — anthropic/* is both what the checked-in config.yaml selects
and what the core falls back to when no config.yaml is loaded at all.
The Vertex AI set is shown in the next example.
config.yaml excerpt for this example:
llm:
model: "anthropic/claude-sonnet-5"
small_model: "anthropic/claude-haiku-4-5"
core/.env:
# LLM provider
ANTHROPIC_API_KEY=sk-example-not-a-real-key
# Sidecar bearer tokens (only iac is enabled in the shipped config.yaml)
KUMOSS_IAC_TOKEN=0000000000000000000000000000000000000000000000000000000000000001
KUMOSS_MAPPING_TOKEN=unused-while-disabled
KUMOSS_NOTIFICATIONS_TOKEN=unused-while-disabled
KUMOSS_AUTHZ_TOKEN=unused-while-disabled
# Git push credentials
GIT_USER=kumoss-bot
GIT_TOKEN=ghp_example_not_a_real_token
# Bundled RustFS and PostgreSQL defaults
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
KUMOSS_SQL_DATABASE_URL=postgresql://postgres:postgres@core-db:5432/kumoss
services/iac/.env (AWS example):
KUMOSS_IAC_TOKEN=0000000000000000000000000000000000000000000000000000000000000001
IAC_BINARY=tofu
# Unset: the backend comes from the workspace — the target repository's
# own block, or the override the core writes when Kumoss-managed state
# is on (the shipped default). If set, this file's values win over that
# override, key by key (see Terraform/OpenTofu state backends), so set
# it only with storage.terraform_state_bucket explicitly blanked to "",
# and mount the file in this container.
# IAC_BACKEND_CONFIG=/etc/kumoss/backend.hcl
# These credentials serve both the providers and, when the rendered
# backend block omits static keys, the S3 state backend.
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=example-not-a-real-secret-access-key
AWS_REGION=eu-west-1
All sidecars enabled (Vertex AI models, Azure infrastructure)
config.yaml excerpt:
llm:
model: "vertex_ai/claude-sonnet-4-5"
small_model: "vertex_ai/gemini-3.7-flash"
services:
notifications:
enabled: true
mapping:
enabled: true
authz:
enabled: true
iac:
# No enabled flag: the IaC sidecar is mandatory and always called.
endpoint: "http://iac:8082"
core/.env:
# LLM provider (Google Vertex AI). The service-account key JSON, on one line.
VERTEXAI_PROJECT=demo-platform-project
VERTEXAI_LOCATION=europe-west1
VERTEXAI_CREDENTIALS={"type":"service_account","project_id":"demo-platform-project","private_key":"<service-account-private-key-pem>","client_email":"kumoss@demo-platform-project.iam.gserviceaccount.example.invalid"}
# One distinct token per sidecar
KUMOSS_IAC_TOKEN=0000000000000000000000000000000000000000000000000000000000000001
KUMOSS_MAPPING_TOKEN=0000000000000000000000000000000000000000000000000000000000000002
KUMOSS_NOTIFICATIONS_TOKEN=0000000000000000000000000000000000000000000000000000000000000003
KUMOSS_AUTHZ_TOKEN=0000000000000000000000000000000000000000000000000000000000000004
GIT_USER=kumoss-bot
GIT_TOKEN=example-not-a-real-gitlab-token
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
KUMOSS_SQL_DATABASE_URL=postgresql://postgres:postgres@core-db:5432/kumoss
Paste the downloaded key file’s content as the value, collapsed onto one
line and unquoted. Keep the \n escapes inside private_key as they
are: turning them into real line breaks makes the JSON invalid.
VERTEXAI_CREDENTIALS also accepts a path to the key file instead
of its content. Kumoss does not provide that file: create a secret or
file mount into the core container yourself (the Compose file mounts
nothing there), readable by uid 10001. A path that does not exist
inside the container is not caught at boot. The first LLM call then
fails with a JSON-parsing error rather than a missing-file error.
|
services/iac/.env (Azure example):
KUMOSS_IAC_TOKEN=0000000000000000000000000000000000000000000000000000000000000001
IAC_BINARY=tofu
# Provider grant: the resources plan and apply create.
ARM_CLIENT_ID=00000000-0000-0000-0000-000000000000
ARM_CLIENT_SECRET=example-not-a-real-client-secret
ARM_TENANT_ID=00000000-0000-0000-0000-000000000001
ARM_SUBSCRIPTION_ID=00000000-0000-0000-0000-000000000002
# State-backend grant: the storage account the target repository's own
# azurerm backend block names. Backend-only, and enough on its own.
# Drop it and add ARM_USE_AZUREAD=true to reuse the principal above.
ARM_ACCESS_KEY=<storage-account-access-key>
services/mapping/.env:
KUMOSS_MAPPING_TOKEN=0000000000000000000000000000000000000000000000000000000000000002
services/notifications/.env:
KUMOSS_NOTIFICATIONS_TOKEN=0000000000000000000000000000000000000000000000000000000000000003
SLACK_WEBHOOK_URL=https://hooks.slack.example.invalid/services/T000/B000/example
LOG_LEVEL=INFO
services/authz/.env:
KUMOSS_AUTHZ_TOKEN=0000000000000000000000000000000000000000000000000000000000000004
KUMOSS_AUTHZ_ROOT_ADMIN_EMAIL=platform-admin@example.invalid
KUMOSS_AUTHZ_PERMISSIVE=true
KUMOSS_AUTHZ_ROLE_STORE=/data/roles.json
Other LLM providers (drop-in replacements for the LLM block)
AZURE_API_KEY=sk-example-not-a-real-key
AZURE_API_BASE=https://demo-platform.openai.azure.example.invalid
AZURE_API_VERSION=2025-01-01-preview
Model strings openai/<model>.
OPENAI_API_KEY=none
OPENAI_API_BASE=https://llm.example.invalid/v1
Other cloud providers (drop-in replacements for the IaC block)
# Provider grant. Also serves the gcs backend, unless you override it below.
GOOGLE_CREDENTIALS={"type":"service_account","project_id":"demo-platform-project","private_key":"<service-account-private-key-pem>","client_email":"kumoss@demo-platform-project.iam.gserviceaccount.example.invalid"}
# State-backend grant: backend-only, and needed only when the state bucket
# lives outside the project above. Omit it and `init` reuses GOOGLE_CREDENTIALS.
# GOOGLE_BACKEND_CREDENTIALS={"type":"service_account", ...}
Kubeconfig mounted into the container.
KUBE_CONFIG_PATH=/home/kumoss/.kube/config