Skip to main content

Introducing Secrets Management in Atmos

· 7 min read
Erik Osterman
Founder @ Cloud Posse

Atmos now has first-class secrets management: you declare the secrets each component depends on, provision their values per environment, and reference them at runtime with a single YAML function.

The Idea

One of the best things about platforms like Vercel and Heroku is how little ceremony it takes to manage an application's settings. A recurring problem with infrastructure is the opposite: environment secrets aren't managed consistently because they're never declared anywhere. Nothing tells you which secrets an environment expects, so they get forgotten, and you find out at deploy time.

Atmos closes that gap. You declare the secrets a component depends on right alongside its configuration, and then provision the missing values for each environment. Declarations live in git; values live in the backend you choose. Secrets are declared on the component, but because they're merged through your normal stack imports and inheritance, you can put shared declarations in a catalog or a stack manifest and let every component that imports it pick them up — you write a secret down once, where it belongs, and it lands everywhere it's needed.

Declare, Provision, Resolve

Declare a secret under a component's secrets.vars, then reference it in your component vars with the !secret YAML function:

components:
terraform:
api:
secrets:
vars:
DATADOG_API_KEY:
store: app-secrets # a `secret: true` store
required: true
vars:
datadog_api_key: !secret DATADOG_API_KEY

Manage the values with a small, familiar CLI. Every operation is scoped to a stack and a component:

atmos secret set DATADOG_API_KEY --stack=prod --component=api
atmos secret list --stack=prod --component=api
atmos secret validate --stack=prod --component=api

atmos secret init walks a component's declarations and provisions the ones that are missing, so a new environment is a single command rather than a checklist. The full set ships: init, set (alias add), get, delete (alias rm), list, pull, push, import, and validate.

Pick the Backend That Fits

Different teams and environments need different kinds of secret storage, so Atmos supports a range of backends. You declare a secret once and keep its value wherever makes sense:

  • 1Password — reach for it during local development, or run 1Password Connect to serve secrets to services inside your VPCs.
  • GitHub Actions secrets — manage the secrets your CI already uses, directly from Atmos.
  • Amazon Secrets Manager and AWS SSM Parameter Store (SecureString).
  • SOPS — opaque, git-committed encrypted files, with no dependency on an external secret store.
  • Azure Key Vault and GCP Secret Manager for the other major clouds.
  • HashiCorp Vault, Redis, Artifactory, and your machine's native keychain.

Any store becomes a secret backend by setting secret: true, and the dedicated secret managers (1Password, keychain, GitHub Actions) are treated that way automatically.

A Word on SOPS

SOPS is worth calling out. It works the same on your laptop as it does in CI or automation, with no external secret store to stand up. The encrypted file is committed to git as an opaque blob, which means you can see exactly when a secret changed in your history — you get a built-in audit trail and a place that documents which secrets exist, without ever exposing their values. It's a clean answer when you want managed secrets without another running service. SOPS supports age as the simplest path to get started.

Masking Comes Along for Free

When Atmos retrieves a secret's value, that value is added to the masking dictionary, so anywhere the string shows up in output it's masked. That lowers the risk of running tasks in automation that need sensitive integration secrets.

Read-only commands don't even need access to the backend. atmos describe and the atmos list family resolve !secret to a masked placeholder without contacting the store, so you can diff or review a stack — production included — on a laptop or in CI with no cloud access:

# No credentials needed — the secret renders masked
atmos describe component api --stack=prod

# Reveal the real value (requires access to the secret backend)
atmos describe component api --stack=prod --mask=false

One thing to know: atmos secret exec and atmos secret shell inject real values into the child process so your tool can use them, and the child process's own output is not masked. Masking applies to what Atmos itself prints.

One Workflow Instead of Three Tools

Historically this took a stack of tools: one to handle identity and authentication, one to write values into the secret store, and one to pass those values into your process at runtime. Atmos brings all three together.

In the cloud, reading a secret first means authenticating as the right identity — and Atmos already does that, with SSO, OIDC, and assumed roles. So it handles both halves of the problem: it acquires the credentials and retrieves the secrets. Most tools do one or the other, leaving you to bolt a separate identity tool onto a separate secrets tool.

When a component consumes a secret, Atmos injects it automatically — terraform plan/apply just works, with no wrapper and no atmos secret exec -- atmos …. For everything else, atmos secret exec and atmos secret shell resolve a component's declared secrets and run any command — a script, a local server, a one-off CLI — with them set in the environment. That makes this just as useful inside your own developer workflow as it is for infrastructure.

Secrets Never Touch Disk

There's a subtle trap here. Atmos hands variables to Terraform through a generated varfile (*.terraform.tfvars.json) — and a naive approach would write your resolved secrets into that file in plaintext, leaving them orphaned on disk long after the run finishes. Masking the output doesn't help if the value is sitting in a file.

So Atmos doesn't do that. Any variable whose value contains a secret — whether it is the secret, or just embeds one inside a larger string like postgres://user:••••••@host/db, or buries it in a nested map — is kept out of the varfile entirely and injected at runtime as a TF_VAR_ environment variable instead. The value lives only for the lifetime of the Terraform process; nothing is left behind on disk. Detection reuses the same masking dictionary every secret is already registered in, so it works even when you run with --mask=false.

The two commands where a human might want a secret materialized take an explicit opt-in:

# Export secrets into the interactive shell as TF_VAR_* (off by default)
atmos terraform shell vpc --stack=prod --with-secrets

# Include secret values in a generated varfile (off by default)
atmos terraform generate varfile vpc --stack=prod --with-secrets

Without --with-secrets, terraform shell won't expose secrets to the subshell, and generate varfile writes a varfile with the secret variables omitted (and tells you it did).

None of this is Terraform-specific. Declaring the secrets an environment depends on, provisioning them per environment, and masking them everywhere is just good practice for any workflow that touches credentials — and now it's built in.

How to Use It

  1. Configure a secret: true store in atmos.yaml.
  2. Declare your secrets under a component's secrets.vars.
  3. Provision values with atmos secret set (or init to be prompted for missing ones).
  4. Reference them with !secret NAME in your component vars.
  5. Verify with atmos secret list and gate CI with atmos secret validate.

See the secrets configuration guide, the atmos secret command reference, and the !secret YAML function to get started.

Try It

The sops-secrets example below is fully self-contained — it manages age-encrypted secrets with no cloud credentials. Give it a spin with the bundled atmos test command to watch the whole lifecycle: set, get, list, validate, and masked-without-credentials inspection.

SOPS Secrets Example

Atmos declarative secrets management end to end with a SOPS backend (age encryption) — the full lifecycle with no cloud credentials, against a git-committed, encrypted file.

Example only. secrets/keys.txt is a throwaway age key committed so the demo is self-contained. Never commit a real age private key — distribute it out of band and reference it via SOPS_AGE_KEY_FILE.

No external tools required. Atmos encrypts and decrypts in-process via the getsops/sops Go SDK — there's no sops or age binary to install. The age key is declared right in the stack, so the example works out of the box.

Give it a spin

Run the bundled atmos test command and watch the whole lifecycle — it sets values, reads them back, lists and validates status, shows masked-without-credentials inspection, then resets the encrypted file to its clean committed state:

atmos test

Two cases worth watching:

  • Inspect with masking on!secret resolves to <MASKED> with no retrieval and no decryption, so you can review the stack with no key at all.
  • Reveal with the key removed — decryption fails, confirming the value is genuinely encrypted at rest.

Learn more

  • stacks/deploy/dev.yaml — the SOPS provider, configured globally for the stack.
  • stacks/catalog/api.yaml — the !secret declarations that consume it.
  • Secrets configuration guide — the full reference, including how to keep the age key in your OS keychain instead of a file.

Get Involved

This is one of our most-requested features, and one we deliberately took our time on — the cost of getting secrets wrong is high, and we wanted to get it right. It's marked experimental while we gather feedback. Try it out and let us know what backends and workflows you'd like to see next on GitHub.