# Secrets Configuration

import File from '@site/src/components/File'
import Intro from '@site/src/components/Intro'

<Intro>
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/commands/secret/usage)
CLI and resolved at runtime with the [`!secret`](/functions/yaml/secret) YAML function.
</Intro>

## Stores vs Secrets

Atmos exposes two user-facing concepts that share a single backend layer (the store registry):

- **[Stores](/cli/configuration/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`, `!store.get`, and `atmos.Store`
against a `secret: true` store are errors ("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`/`azure-kv`/`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`).

<File title="atmos.yaml">
```yaml
stores:
  # Regular store (machine outputs) — accessed with !store
  terraform-outputs:
    kind: aws/ssm
    options:
      region: us-east-1

  # AWS SSM Parameter Store secret store (SecureString) — accessed with !secret
  ssm-secrets:
    kind: aws/ssm
    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:
    kind: aws/asm
    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:
    kind: azure/keyvault
    secret: true
    identity: azure/prod-subscription
    options:
      vault_url: https://myvault.vault.azure.net/

  # GCP Secret Manager
  gcp-secrets:
    kind: gcp/secretmanager
    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
```
</File>

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

<dl>
  <dt>`secret`</dt>
  <dd>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.</dd>

  <dt>`kind`</dt>
  <dd>Optional `cloud/thing` backend selector (e.g. `aws/ssm`, `aws/asm`, `hashicorp/vault`). Equivalent to the legacy `type`; when both are set, `kind` wins.</dd>

  <dt>`identity`</dt>
  <dd>Optional auth identity (resolved via [`auth`](/cli/configuration/auth/identities)) used to authenticate to the backend. When omitted and the secret is resolved within a component scope, the component's effective identity is inherited. A store pinned to an `identity` is not affected by a different `--identity`, but `--identity=false` disables Atmos authentication entirely: the pinned identity is bypassed, the store uses ambient credentials from the environment, and Atmos prints a one-time warning naming the store and the bypassed identity.</dd>
</dl>

For a HashiCorp Vault or OpenBao store pinned to an `identity`, that native identity must produce `environment/v1` credentials containing `VAULT_TOKEN`. The identity token takes precedence over both `options.token` and ambient `VAULT_TOKEN`; if the identity cannot resolve or omits `VAULT_TOKEN`, Atmos fails before contacting the server and does not fall back. Configure the server address with `options.address`/`options.url` or ambient `VAULT_ADDR`. With no selected identity, the existing static and ambient token behavior remains available.

For an identity that owns a temporary Vault/OpenBao token, set
`options.identity_lifetime: operation` to release it after each backend
operation, before a read result reaches its consumer:

```yaml
stores:
  openbao:
    type: hashicorp-vault
    secret: true
    identity: openbao-reader
    options:
      address: https://openbao.example.com
      mount: platform
      identity_lifetime: operation
```

This mode requires an explicit store identity and an auth manager that supports
operation ownership. Each operation gives the selected target a unique credential
realm for its keyring, files and native private session, and a separate process
cache entry. The target authenticates afresh and is logged out before returning.
Root provider and ancestor identity sessions, existing target sessions, and the
component's auth context are preserved. The identity's `Logout` implementation must revoke its
server token: clearing an environment map alone does not revoke credentials.

Cleanup also runs after failed reads. A cleanup failure suppresses a successful
read result, so credentials cannot proceed to a child process on an unconfirmed
logout. Native secret-provider reads pass cancellation to context-aware Vault
operations; cleanup receives a separate bounded context. Credential acquisition
retains bounded ownership after cancellation so a newly issued token can be
released. Forced process termination and an ambiguous server/network outcome
cannot guarantee remote revocation; configure a short server-enforced token TTL
as well. The default `identity_lifetime: session` retains the existing behavior.

#### 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](https://developer.1password.com/docs/cli/secret-reference-syntax/) via the [`reference`](#declaring-secrets) field. References support Go templating (`{{ .atmos_stack }}`, `{{ .atmos_component }}`) and [sprig](https://masterminds.github.io/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:

<dl>
  <dt>`mode`</dt>
  <dd>`auto` (default), `connect`, or `service-account`. In `auto`, [1Password Connect](https://developer.1password.com/docs/connect/) is used when its host and token are present (the typical CI/cloud setup), otherwise a [Service Account](https://developer.1password.com/docs/service-accounts/) is used (typical local dev).</dd>

  <dt>`token`</dt>
  <dd>Service-account token. Falls back to the `OP_SERVICE_ACCOUNT_TOKEN` environment variable.</dd>

  <dt>`connect_host` / `connect_token`</dt>
  <dd>1Password Connect server URL and API token. Fall back to `OP_CONNECT_HOST` / `OP_CONNECT_TOKEN`.</dd>

  <dt>`vault`</dt>
  <dd>Optional default vault. When set, vault-relative references (`Datadog/api_key`) are expanded to `op://<vault>/Datadog/api_key`.</dd>
</dl>

:::note Binary size
The native Service Account integration embeds the 1Password SDK core (~9&nbsp;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 (`sops/aws-kms`,
`sops/gcp-kms`, `sops/azure-kv`) authenticate to the cloud KMS using an Atmos
[`identity`](#sops-identity) — the same identity model as Track-1 stores and `terraform plan` — so you
do **not** need to wrap commands in `atmos auth exec` or export cloud credentials.
[`atmos secret keygen`](/cli/commands/secret/keygen) generates the age key and writes it to whichever
sink `spec.age_key` selects (KMS kinds bring externally-managed keys, so they are not generated).

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

<File title="atmos.yaml">
```yaml
secrets:
  providers:
    dev-sops:
      kind: sops/age          # or: sops/aws-kms, sops/gcp-kms, sops/azure-kv, sops/gpg
      # identity: aws/prod-admin        # KMS kinds only; auth identity for the cloud KMS (see below)
      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
```
</File>

The SOPS provider `spec` accepts:

<dl>
  <dt>`file`</dt>
  <dd>Path to the encrypted file (a Go template with `{{ .atmos_stack }}`/`{{ .atmos_component }}` in scope). Relative to the directory Atmos runs in.</dd>

  <dt>`age_key`</dt>
  <dd>
    Where the age **private key** lives (`sops/age` only; KMS kinds ignore it). An object:

    <dl>
      <dt>`store`</dt>
      <dd>`file` (default) — a local key file — or the name of a configured [store](/cli/configuration/stores) such as the OS `keychain`. Reading and [`atmos secret keygen`](/cli/commands/secret/keygen) writing both use this backend.</dd>
      <dt>`path`</dt>
      <dd>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.</dd>
      <dt>`value`</dt>
      <dd>Optional inline key material (highest precedence). Best populated from a YAML function — `!env`, `!exec`, or `!store.get` — rather than committed plaintext.</dd>
    </dl>

    Precedence: `value` → `store` → `file` → `SOPS_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`.
  </dd>

  <dt>`age_key_file`</dt>
  <dd>Back-compat shorthand for `age_key: { store: file, path: <this> }` — a path to the age private key file (`~`/`$ENV` expanded).</dd>

  <dt>`age_recipients`</dt>
  <dd>Optional age **public** recipients used when creating a fresh file. When omitted, recipients come from the matching creation rule in the nearest `.sops.yaml`.</dd>

  <dt>`recipients_file`</dt>
  <dd>Where `atmos secret keygen` records this vault's public recipient. Defaults to `.sops.yaml` (a creation rule) at the Atmos base path. Generated `path_regex` patterns are relative to the recipients file’s directory, so `secrets/.sops.yaml` works with `file: secrets/{{ .atmos_stack }}.enc.yaml`.</dd>
</dl>

#### Cloud KMS identity {#sops-identity}

The cloud-KMS kinds (`sops/aws-kms`, `sops/gcp-kms`, `sops/azure-kv`) wrap the SOPS data key with a
cloud KMS, so encrypt/decrypt require cloud credentials. Atmos obtains them from an
[auth identity](/cli/configuration/auth/identities) — the same way it does for `terraform plan` and
Track-1 stores — and injects them into the in-process KMS calls. No `atmos auth exec` wrapper or
ambient cloud credentials are needed. The cloud is inferred from the file's master keys at runtime; the
`identity` is the only knob (`sops/age` and `sops/gpg` use local keys and ignore it).

<dl>
  <dt>`identity`</dt>
  <dd>Optional auth identity (a provider-level field, sibling to `kind`) used to authenticate to the cloud KMS for KMS-backed kinds. Resolved with the following precedence (highest first):</dd>
</dl>

1. The provider's own `identity` (`secrets.providers.<name>.identity`).
2. The `--identity` flag / `ATMOS_IDENTITY` environment variable on the `atmos secret` command.
3. The stack/component **effective identity** (the same identity `terraform plan` would use for that component).

If none resolves, Atmos falls back to the ambient cloud credential chain (environment, shared config,
instance metadata), preserving prior behavior.

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

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

<File title="stacks/deploy/dev.yaml">
```yaml
secrets:
  providers:
    dev-sops:
      kind: sops/age
      spec:
        file: secrets/dev.enc.yaml
```
</File>

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

<File title="stacks/deploy/dev.yaml">
```yaml
components:
  terraform:
    api:
      secrets:
        providers:
          dev-sops:
            kind: sops/age
            spec:
              file: secrets/dev.enc.yaml
        vars:
          GITHUB_APP_KEY:
            sops: dev-sops
```
</File>

## 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.

<File title="stacks/prod/api.yaml">
```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
```
</File>

Each declaration accepts:

<dl>
  <dt>`description`</dt>
  <dd>Human-readable description of the secret.</dd>

  <dt>`store`</dt>
  <dd>Name of a `secret: true` store (track 1) this secret resolves from. Set exactly one of `store` or `sops`.</dd>

  <dt>`sops`</dt>
  <dd>Name of a `secrets.providers` SOPS provider (track 2) this secret resolves from. Set exactly one of `store` or `sops`.</dd>

  <dt>`reference`</dt>
  <dd>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.</dd>

  <dt>`required`</dt>
  <dd>When `true`, [`atmos secret validate`](/cli/commands/secret/validate) fails if the secret is not initialized.</dd>

  <dt>`scope`</dt>
  <dd>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).</dd>
</dl>

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

<dl>
  <dt>`instance` (default)</dt>
  <dd>One value per component instance. Path: `{prefix}/{stack}/{component}/{NAME}`.</dd>

  <dt>`stack`</dt>
  <dd>One value per stack, shared by every component in it. Path: `{prefix}/{stack}/{NAME}`. Derived automatically for declarations in a top-level `secrets:` block.</dd>

  <dt>`global`</dt>
  <dd>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.</dd>
</dl>

### 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`](/cli/commands/secret/set) and every stack sees the new value.

<File title="stacks/catalog/secrets/shared.yaml">
```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
```
</File>

This composes with the store's [`identity`](#track-1--secret-stores-secret-true) 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.

:::warning 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`](/cli/commands/secret/import) (like `terraform import`, it copies — the source
value is never modified or deleted), then delete the old `!store` line:

```yaml
# 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
```

```shell
# 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
```

```yaml
# 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:

```yaml
components:
  terraform:
    api:
      secrets:
        vars: !include secrets/api.yaml
```

## Consuming secrets in commands and shells

Besides resolving secrets into stack config with [`!secret`](/functions/yaml/secret), you can inject a component's declared secrets straight into a subprocess environment:

- [`atmos secret exec`](/cli/commands/secret/exec) runs a command with the declared secrets set as environment variables (named after each declaration).
- [`atmos secret shell`](/cli/commands/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](/cli/configuration/commands/component#custom-component-types)), 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](/stacks/components/helmfile#passing-secrets-securely) — read with `requiredEnv` in `helmfile.yaml`.
- [Packer](/stacks/components/packer#passing-secrets-securely) — read with the `env()` function in the template.
- [Ansible](/stacks/components/ansible#passing-secrets-securely) — read with the `env` lookup in the playbook.
- [Custom components](/cli/configuration/commands/component#passing-secrets-to-custom-components) — read as `$VAR` in the command steps.

## See Also

- [`atmos secret` command](/cli/commands/secret/usage)
- [`atmos secret exec`](/cli/commands/secret/exec) / [`atmos secret shell`](/cli/commands/secret/shell)
- [`!secret` YAML function](/functions/yaml/secret)
- [Stores configuration](/cli/configuration/stores)
- [Auth identities](/cli/configuration/auth/identities)
