Skip to main content

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 !secret and the atmos secret CLI.

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-backedTrack 2 — non-store
BackendsAWS SSM, AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager, 1PasswordSOPS (age/aws-kms/gcp-kms/gpg)
ShapeRemote key-value storeGit-committed encrypted file
Configstores: entry with secret: truesecrets.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).

atmos.yaml
stores:
# Regular store (machine outputs) — accessed with !store
terraform-outputs:
type: aws-ssm-parameter-store
options:
region: us-east-1

# AWS SSM Parameter Store secret store (SecureString) — accessed with !secret
ssm-secrets:
type: aws-ssm-parameter-store
secret: true
identity: aws/prod-admin
options:
region: us-east-1
prefix: /atmos/secrets

# AWS Secrets Manager — structured/JSON secrets, rotation, larger values
asm-secrets:
type: aws-secrets-manager
secret: true
identity: aws/prod-secrets
options:
region: us-east-1
prefix: atmos/secrets

# HashiCorp Vault (KV v2)
vault-secrets:
type: hashicorp-vault
secret: true
options:
url: https://vault.example.com
mount: secret

# Azure Key Vault
azure-secrets:
type: azure-key-vault
secret: true
identity: azure/prod-subscription
options:
vault_url: https://myvault.vault.azure.net/

# GCP Secret Manager
gcp-secrets:
type: google-secret-manager
secret: true
options:
project_id: my-project

# 1Password (`secret: true` is implied) — reference-based, full CRUD
op:
type: onepassword
options:
mode: auto # auto | connect | service-account

Each secret store accepts the same options as its non-secret counterpart, plus:

secret
Set to true to make the store a secret backend. Required for !secret access; blocks !store access. 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/thing backend selector (e.g. aws/ssm, aws/asm, hashicorp/vault). Equivalent to the legacy type; when both are set, kind wins.
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: true is implied. 1Password is a secret manager, so a type: onepassword store is always a secret backend — you never write secret: true, and it cannot be used as a non-secret !store backend.
  • Reference-based addressing. Instead of composing a key from the stack/component/key, each declared secret carries an explicit op://vault/item/field secret reference via the reference field. References support Go templating ({{ .atmos_stack }}, {{ .atmos_component }}) and sprig functions, mirroring the SOPS spec.file pattern.
  • Full CRUD. atmos secret get/set/delete/list/validate all work. set writes 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). delete removes 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:

mode
auto (default), connect, or service-account. In auto, 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_TOKEN environment 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 to op://<vault>/Datadog/api_key.
Binary size

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:

atmos.yaml
secrets:
providers:
dev-sops:
kind: sops/age # or: sops/aws-kms, sops/gcp-kms, sops/gpg
spec:
file: secrets/{{ .atmos_stack }}.enc.yaml
# age_key: # optional; where the age PRIVATE key lives (read + keygen sink)
# store: keychain # "file" (default) | a configured store name (e.g. keychain)
# path: dev-sops # optional; file path (file mode) or store key (default: vault name)
# age_recipients: age1... # optional; otherwise read from the matching .sops.yaml creation rule

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_key

Where the age private key lives (sops/age only; KMS kinds ignore it). An object:

store
file (default) — a local key file — or the name of a configured store such as the OS keychain. Reading and atmos secret keygen writing both use this backend.
path
Location within the backend: the key file path (file mode; supports ~ and $ENV expansion) 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: valuestorefileSOPS_AGE_KEY_FILE/SOPS_AGE_KEY. A bare string (age_key: <material>) is treated as value. Takes precedence over SOPS_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 (~/$ENV expanded).
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 keygen records this vault's public recipient. Defaults to .sops.yaml (a creation rule) at the Atmos base path.
Keep the key in the OS keychain

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):

stacks/deploy/dev.yaml
secrets:
providers:
dev-sops:
kind: sops/age
spec:
file: secrets/dev.enc.yaml

…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:

stacks/deploy/dev.yaml
components:
terraform:
api:
secrets:
providers:
dev-sops:
kind: sops/age
spec:
file: secrets/dev.enc.yaml
vars:
GITHUB_APP_KEY:
sops: dev-sops

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.

stacks/prod/api.yaml
components:
terraform:
api:
secrets:
vars:
DATADOG_API_KEY:
description: "Datadog API key for monitoring"
store: ssm-secrets # references a `secret: true` store (track 1)
required: true
GITHUB_APP_KEY:
description: "GitHub App private key for CI"
sops: dev-sops # references a SOPS provider (track 2)
required: true
DB_PASSWORD:
description: "Database password from 1Password"
store: op # a 1Password store (track 1)
reference: "op://{{ .atmos_stack }}/postgres/password"
required: true
vars:
datadog_api_key: !secret DATADOG_API_KEY
db_password: !secret DB_PASSWORD

Each declaration accepts:

description
Human-readable description of the secret.
store
Name of a secret: true store (track 1) this secret resolves from. Set exactly one of store or sops.
sops
Name of a secrets.providers SOPS provider (track 2) this secret resolves from. Set exactly one of store or sops.
reference
Optional backend-specific address for reference-based stores (1Password). For a 1Password store this is an op://vault/item/field reference 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 validate fails if the secret is not initialized.
scope
Storage scope: instance (default), stack, or global. Normally derived from declaration position — a top-level secrets: block implies stack, a component-level block implies instance — and an explicit value must match the position. The exception is global, 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-level secrets: 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.

stacks/catalog/secrets/shared.yaml
components:
terraform:
api:
secrets:
vars:
SHARED_CLIENT_SECRET:
description: "OAuth client secret shared by all environments"
store: gcp-secrets
scope: global # path: {prefix}/SHARED_CLIENT_SECRET — same from everywhere
required: true

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.

One value, many consumers

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 exec runs a command with the declared secrets set as environment variables (named after each declaration).
  • atmos secret shell launches 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 requiredEnv in helmfile.yaml.
  • Packer — read with the env() function in the template.
  • Ansible — read with the env lookup in the playbook.
  • Custom components — read as $VAR in the command steps.

See Also