Secrets Configuration
Atmos secrets provide a GitOps-friendly workflow for managing human-provisioned configuration secrets (API keys,
tokens, passwords). Secrets are declared in stack config (committed to git) and their values live in a
cloud secret backend or a SOPS-encrypted file. They are provisioned with the atmos secret
CLI and resolved at runtime with the !secret YAML function.
Stores vs Secrets
Atmos exposes two user-facing concepts that share a single backend layer (the store registry):
- Stores — the shared backend layer for external data (e.g. Terraform
outputs), accessed with
!store. - Secrets — human-managed configuration, declared explicitly and accessed only with
!secretand theatmos secretCLI.
A store becomes a secret backend by setting secret: true. !store against a secret: true store is an
error ("use !secret"), which makes the declarative registry mandatory-by-construction.
Backend Architecture (Two Tracks)
| Track 1 — store-backed | Track 2 — non-store | |
|---|---|---|
| Backends | AWS SSM, AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager, 1Password | SOPS (age/aws-kms/gcp-kms/gpg) |
| Shape | Remote key-value store | Git-committed encrypted file |
| Config | stores: entry with secret: true | secrets.providers: entry |
Track 1 — Secret stores (secret: true)
Configure secret stores in atmos.yaml alongside regular stores. A secret: true store always writes the
sensitive at-rest variant (e.g. SSM SecureString).
Each secret store accepts the same options as its non-secret counterpart, plus:
secret- Set to
trueto make the store a secret backend. Required for!secretaccess; blocks!storeaccess. Only backends that encrypt values at rest may be marked secret — plaintext backends (Redis, Artifactory) are rejected at startup with an error. kind- Optional
cloud/thingbackend selector (e.g.aws/ssm,aws/asm,hashicorp/vault). Equivalent to the legacytype; when both are set,kindwins. identity- Optional auth identity (resolved via
auth) used to authenticate to the backend. When omitted and the secret is resolved within a component scope, the component's effective identity is inherited.
1Password
The 1Password store differs from the other Track-1 backends in three ways:
secret: trueis implied. 1Password is a secret manager, so atype: onepasswordstore is always a secret backend — you never writesecret: true, and it cannot be used as a non-secret!storebackend.- Reference-based addressing. Instead of composing a key from the stack/component/key, each declared secret carries an explicit
op://vault/item/fieldsecret reference via thereferencefield. References support Go templating ({{ .atmos_stack }},{{ .atmos_component }}) and sprig functions, mirroring the SOPSspec.filepattern. - Full CRUD.
atmos secret get/set/delete/list/validateall work.setwrites the value to the field the reference points to, creating the item if it does not exist (created items use the API Credential category with a Concealed value field).deleteremoves the field, and deletes the item once its last field is removed. Service accounts cannot write to the built-in Private/Personal/Employee vault — use a shared named vault.
No op CLI is required. Authentication auto-selects between two backends so the same config works in local dev and CI:
modeauto(default),connect, orservice-account. Inauto, 1Password Connect is used when its host and token are present (the typical CI/cloud setup), otherwise a Service Account is used (typical local dev).token- Service-account token. Falls back to the
OP_SERVICE_ACCOUNT_TOKENenvironment variable. connect_host/connect_token- 1Password Connect server URL and API token. Fall back to
OP_CONNECT_HOST/OP_CONNECT_TOKEN. vault- Optional default vault. When set, vault-relative references (
Datadog/api_key) are expanded toop://<vault>/Datadog/api_key.
The native Service Account integration embeds the 1Password SDK core (~9 MB) in the Atmos binary. It is pure-Go (no op CLI, no cgo). Connect alone adds negligible size.
Track 2 — SOPS providers
SOPS is a git-committed encrypted file edited imperatively, so it is configured under secrets.providers
(not stores). atmos secret set/get/delete against a SOPS provider decrypts the file, mutates the key,
and re-encrypts in place. Atmos does this in-process with the getsops/sops Go SDK — no sops binary
is required. For sops/age you need an age key. It can be declared via spec.age_key — from a
file (the default) or a store such as the OS keychain — or supplied via the
SOPS_AGE_KEY_FILE/SOPS_AGE_KEY environment variables; the KMS-backed kinds use your cloud
credentials. atmos secret keygen generates the key and writes it to
whichever sink spec.age_key selects.
The file path is a Go template with {{ .atmos_stack }} and {{ .atmos_component }} in scope (use
{{ .atmos_component }} to keep each component's secrets in its own file). You can define a SOPS provider
globally in atmos.yaml:
The SOPS provider spec accepts:
file- Path to the encrypted file (a Go template with
{{ .atmos_stack }}/{{ .atmos_component }}in scope). Relative to the directory Atmos runs in. age_keyWhere the age private key lives (
sops/ageonly; KMS kinds ignore it). An object:storefile(default) — a local key file — or the name of a configured store such as the OSkeychain. Reading andatmos secret keygenwriting both use this backend.path- Location within the backend: the key file path (file mode; supports
~and$ENVexpansion) or the store key (store mode). Optional — defaults to the sops default keys file in file mode and the vault (provider) name in store mode. value- Optional inline key material (highest precedence). Best populated from a YAML function —
!env,!exec, or!store.get— rather than committed plaintext.
Precedence:
value→store→file→SOPS_AGE_KEY_FILE/SOPS_AGE_KEY. A bare string (age_key: <material>) is treated asvalue. Takes precedence overSOPS_AGE_KEY_FILE/SOPS_AGE_KEY.age_key_file- Back-compat shorthand for
age_key: { store: file, path: <this> }— a path to the age private key file (~/$ENVexpanded). age_recipients- Optional age public recipients used when creating a fresh file. When omitted, recipients come from the matching creation rule in the nearest
.sops.yaml. recipients_file- Where
atmos secret keygenrecords this vault's public recipient. Defaults to.sops.yaml(a creation rule) at the Atmos base path.
Configure a keychain store and point age_key.store at it to keep the private key out of files and environment variables entirely:
stores:
keychain:
type: keychain
secrets:
providers:
dev-sops:
kind: sops/age
spec:
file: secrets/dev.enc.yaml
age_key:
store: keychain
atmos secret keygen dev-sops writes the generated private key into the keychain; decryption reads it back automatically — no SOPS_AGE_KEY_FILE needed.
…or scope it to a stack by declaring a global secrets.providers block at the top of a stack
manifest (it merges into every component in that stack, like global vars):
…or scope it to a single component by declaring secrets.providers alongside secrets.vars.
A stack/component-scoped provider takes precedence over an atmos.yaml-level provider of the same name:
Declaring Secrets
Secrets are declared under a component's secrets.vars. Inheritance follows the standard Atmos stack
hierarchy. A "global" secret is simply a shared declaration imported wherever it is needed.
Each declaration accepts:
description- Human-readable description of the secret.
store- Name of a
secret: truestore (track 1) this secret resolves from. Set exactly one ofstoreorsops. sops- Name of a
secrets.providersSOPS provider (track 2) this secret resolves from. Set exactly one ofstoreorsops. reference- Optional backend-specific address for reference-based stores (1Password). For a 1Password store this is an
op://vault/item/fieldreference and may contain Go-template vars ({{ .atmos_stack }},{{ .atmos_component }}). Name-keyed backends (AWS, Azure, GCP, Vault, SOPS) ignore it. required- When
true,atmos secret validatefails if the secret is not initialized. scope- Storage scope:
instance(default),stack, orglobal. Normally derived from declaration position — a top-levelsecrets:block impliesstack, a component-level block impliesinstance— and an explicit value must match the position. The exception isglobal, which is honored at either position. See Secret scopes.
Secret scopes
A declaration's scope controls where its value is stored — and therefore who shares it. Store-backed
secrets compose their backend path from the scope:
instance(default)- One value per component instance. Path:
{prefix}/{stack}/{component}/{NAME}. stack- One value per stack, shared by every component in it. Path:
{prefix}/{stack}/{NAME}. Derived automatically for declarations in a top-levelsecrets:block. global- One value shared by every stack and component that resolves through the same store. Path:
{prefix}/{NAME}. Sharing is bounded by the store's backend (account/project/prefix), which remains the isolation boundary.
Shared (global) secrets
Declare a secret once in a catalog fragment with scope: global, import the fragment wherever the
secret is consumed, and every consumer computes the same storage path. Rotate it once with
atmos secret set and every stack sees the new value.
This composes with the store's identity for a centralized secrets
account: the store points at the central backend, its identity grants access from any environment,
and scope: global makes every consumer resolve the same value.
atmos secret set and atmos secret delete on a stack- or global-scoped secret affect every
consumer of that value, not just the (stack, component) you ran the command from.
Migrating from !store
Marking an existing store secret: true switches its access from !store to !secret — but values
written by legacy !store <store> <stack> <component> <key> usage live at hand-built paths the new
computed coordinates don't match. Adopt each value once with
atmos secret import (like terraform import, it copies — the source
value is never modified or deleted), then delete the old !store line:
# Before: imperative addressing — "atmos" and "shared" are raw path segments, not a real stack/component.
vars:
client_secret: !store app-secrets atmos shared client_secret
# One-time adoption: copy the value to the declaration's computed coordinate.
atmos secret import SHARED_CLIENT_SECRET \
--from-stack=atmos --from-component=shared --from-key=client_secret \
--stack=prod --component=api
# After: declarative — the legacy path appears nowhere in stack config.
secrets:
vars:
SHARED_CLIENT_SECRET:
store: app-secrets
scope: global
env:
CLIENT_SECRET: !secret SHARED_CLIENT_SECRET
Organizing declarations with !include
The vars map can be pulled in from a file for organization, and a shared file is how "global" declarations
are reused:
components:
terraform:
api:
secrets:
vars: !include secrets/api.yaml
Consuming secrets in commands and shells
Besides resolving secrets into stack config with !secret, you can inject a component's declared secrets straight into a subprocess environment:
atmos secret execruns a command with the declared secrets set as environment variables (named after each declaration).atmos secret shelllaunches an interactive shell with the same environment.
Both layer secrets on top of the current environment and skip uninitialized secrets with a warning. Values are passed to the child process in cleartext and are not masked in its output.
Passing secrets to components
When a component runs (Terraform, Helmfile, Packer, Ansible, or a custom component type), reference secrets with !secret in the component's env section. Resolved values are injected into the tool's subprocess environment and — for Helmfile, Packer, and Ansible — kept out of the on-disk variables file, which is written in plaintext. Avoid putting secrets in vars for those component types. Each component guide shows the secure pattern and how to read the variable in the underlying tool:
- Helmfile — read with
requiredEnvinhelmfile.yaml. - Packer — read with the
env()function in the template. - Ansible — read with the
envlookup in the playbook. - Custom components — read as
$VARin the command steps.