Nov 19, 2025
10 min read

Should you store secrets as code? What works and what doesn’t

Should you store secrets as code? What works and what doesn’t

TL;DR

Secrets as code can refer to storing encrypted secrets in Git, defining them in Infrastructure as Code (IaC), or pulling them dynamically at runtime. Each approach has strengths, but also serious risks if misapplied. The most reliable pattern is to centralize secrets in a manager, keep them out of Git when possible, fetch them only at runtime, and tightly scope and rotate access. With the right workflow, secrets as code become secure, scalable, and far easier for platform teams to manage.

In this article, we’ll explore the three main approaches to secrets as code. We’ll look at where each one works, where it falls short, the anti-patterns that slow teams down, and the practices that make secrets as code work at scale.

Ask ten engineers what “secrets as code” means, and you’ll get ten different answers. Some will point to encrypting and versioning secrets alongside source code. Others will describe managing secrets through Infrastructure as Code tools that connect to external secret managers. Still, others argue that secrets should never live in code or configuration at all and should only be injected dynamically at runtime.

All of these approaches are often labeled as secrets as code, but in practice, they have very different implementations, each with different trade-offs. Pick the wrong model, and instead of securing secrets, you risk leaking credentials or stalling deployments.

The three approaches to secrets as code

When people say secrets as code, they usually mean one of three things:

  • Storing encrypted or sealed secrets directly in Git repositories using Mozilla SOPS or Kubernetes Sealed Secrets
  • Managing secrets through Infrastructure as Code
  • Injecting secrets dynamically at runtime

Each approach has strengths and weaknesses. Let's compare them side by side before unpacking each in detail.

ApproachHow it worksProsConsWhen it makes sense

Encrypted secrets in Git

Commit encrypted secrets to version control using tools like Mozilla SOPS(Secrets operations) or Sealed Secrets.

  • Full GitOps compatibility
  • Versioning & peer review
  • Declarative workflows
  • Works well with Kubernetes controllers
  • Key management overhead
  • Permanent Git history(encrypted blobs linger)
  • Repo and encryption key access can expose secrets
  • Requires strict discipline
  • Teams fully committed to GitOps
  • Kubernetes clusters using Sealed Secrets or SOPS
  • Small teams that value simplicity but enforce strong encryption practices

Secrets in IaC

Declare secrets in Terraform/Pulumi and reference them in Doppler, Vault, AWS Secrets Manager.

  • Centralized lifecycle management
  • Compliance-friendly
  • Automated rotation & audit trails
  • Aligns with IaC workflows
  • State files can leak
  • Steep learning curve
  • Dependency on secrets manager uptime
  • Enterprises with compliance/audit needs
  • Teams heavily invested in Terraform
  • Workloads requiring automated rotation

Secrets at runtime

Fetch secrets only when needed at deploy/run time via CI/CD or app startup. Tools: Doppler CLI, Vault, AWS Secrets Manager.

  • Rotation is painless
  • Short-lived credentials
  • Minimizes exposure
  • Debugging friction
  • Dependency on runtime availability
  • Integration complexity
  • Security-focused orgs
  • Fast-moving CI/CD teams
  • Apps requiring least-privilege access

Encrypted secrets in Git repositories

This is the most literal interpretation of secrets as code, which involves committing encrypted secrets to version control using tools like SOPS and Kubernetes Sealed Secrets. Instead of leaving values in plaintext, you commit an encrypted blob and rely on a key management system to decrypt it when needed. With SOPS, for instance, you can commit an encrypted YAML or JSON file, and only those with the encryption key pair can decrypt it.

Example workflow for GitOps-based secrets management
Example workflow for GitOps-based secrets management

Many teams like this workflow because it keeps everything in Git. Secrets can be versioned and peer-reviewed in environments alongside the rest of your infrastructure, which is very attractive for teams that live and breathe GitOps.

Secrets in Infrastructure as Code

Another common interpretation of secrets as code is to manage secrets through Infrastructure as Code (IaC) tooling like Terraform or CloudFormation. You can store secrets directly in your IaC(for example, embedding them in Terraform and marking them as sensitive) or use encrypted secrets inline. Alternatively, you can also declare secrets resources as references that point to a centralized secrets management solution like Doppler, AWS Secrets Manager, or Vault.
This model fits teams already invested in IaC, since it integrates secrets into the same workflows and compliance processes as other resources. Terraform makes it easy to reference secrets stored in your cloud provider. Every resource is codified, peer-reviewed, and subject to the same compliance and audit trails. Rotation policies, access control, and audit logs come from the central manager, but the lifecycle is still driven through code.

Secrets at runtime (Dynamic injection)

The third and most security-focused interpretation of secrets as code is to never store secrets in Git or IaC at all. Instead, secrets are fetched dynamically at runtime. In this model, CI/CD pipelines, container orchestrators, or applications themselves pull secrets from external secret stores like Doppler only when they’re needed.

Example workflow for runtime secret injection
Example workflow for runtime secret injection

With this approach, teams can rotate production and development secrets without changing anything in their repos. You can also keep environments consistent by pulling values from one central source, and move faster because developers don’t need to keep local copies of sensitive files. If a new secret is created in your secrets manager, your apps automatically pull it at runtime without changing code or redeploying.

Regardless of which interpretation of secrets as code you follow, mistakes are common. Some come from shortcuts that feel convenient, others from misusing tools you don’t fully understand. Either way, these mistakes can lead to security risks and slow your team down as you scale.

Anti-patterns that hurt security and scalability

Some of the practices that make secrets as code fail include:

Mistaking encoding for encryption

This mistake often happens with the Git-based model of secrets management. Kubernetes Secrets are a common example, where values are base64-encoded and committed inside manifests or configuration files, giving teams a false sense of protection. However, base64 doesn’t actually hide anything; anyone with access to the repo or cluster can turn those values back into plain text in seconds.

Baking secrets into build artifacts

Some teams take a shortcut by putting secrets directly into Docker images, AMIs, or even compiled binaries. It seems convenient because the image runs everywhere without extra setup. The real problem is that once a secret is baked into an image, it's permanent. Those images now permanently contain confidential data, scattered across registries and caches. Rotating the secret later in your secrets management tool doesn't erase the old value from those builds.

Over-privileged and non-rotated tokens

This issue appears in every secrets as code model, whether Git, IaC, or runtime. Instead of scoping tightly and rotating often, teams rely on broad, long-lived tokens because they are convenient. Examples include an AWS admin key or a GitHub personal token with full repo access. These keep systems running, but once the secret leaks, the damage is more instant and far-reaching than if the secret had been properly scoped.

No ownership or guardrails

Even with runtime injection or centralized managers, problems arise when no one is accountable for secrets, and access rules are undefined. If every CI job, developer, or contractor can pull the same production secrets, there is no clear ownership or separation of responsibility. Without clear ownership, you end up with poorly maintained secrets that can lead to security breaches.
We've covered what doesn't work. Now, let's look at how to manage secrets as code the right way.

How to do secrets as code right

To make secrets as code work, you need a workflow that protects secrets from exposure, scales with your team, and doesn't slow development.

Keep secrets out of source control unless necessary

Storing encrypted secrets in Git can work for GitOps-first teams, but the risks grow quickly. Rotating keys becomes cumbersome because every change has to go through Git. The repository itself can bloat as old versions of secrets accumulate in history. And as more people gain access to the repo, the chances of secrets spreading beyond intended boundaries increase. At that point, a centralized manager and runtime injection give you stronger guarantees with less operational overhead.

Centralize storage, access control, and lifecycle management

Secrets belong in a secrets manager, not scattered across repos, scripts, and CI configs. Use a centralized manager like Doppler for secret creation, rotation, and revocation. These tools also provide audit logs and role-based access controls, so you always know who has access and when. For example, with Terraform and Doppler, you can declare a database password secret as a managed resource, as shown below:

In this example, Terraform provisions secret resources in Doppler without committing raw values in Git. The code ensures a secret named DB_PASSWORD exists in the backend project’s prd config, and Doppler manages core secrets operations. You define and track the lifecycle in code, while Doppler handles secure storage, access controls, and rotation.

Fetch secrets at runtime, not build time

Once a secret lands in an artifact, it can spread to registries, caches, and backups and become hard to remove. Therefore, secrets should only exist when they are needed. Pull them at runtime, not inside Docker images, AMIs, or binaries. That way, secrets remain tied only to the process that actually needs them.

For example, you can use Doppler in GitHub Actions to fetch secrets at job runtime and expose them as step outputs, then pass only the ones your app needs into the next step's environment:

In the code example above, the action pulls secrets at runtime and exposes them under steps.doppler.outputs.*. You choose which ones to map into the env of the step that needs them. Secrets never touch Git or the filesystem and exist only for the lifetime of that step.

Scope access with least privilege and rotate automatically

Beyond fetching secrets only at runtime, they should also be scoped as tightly as possible. A database credential should only work for the service that needs it, and a CI job should only have access to the variables required for that specific run. Furthermore, leverage secrets managers to refresh values on a schedule and set your pipelines to always pull the latest version at deploy time. This limits the usefulness of any leaked secret to a short window and removes the risk of old tokens sitting around for years.

When you combine strict scoping with automatic rotation, secrets stay short-lived, narrowly targeted, and far less dangerous if exposed, which is precisely how secrets as code should work.

Final thoughts

Secrets as code can be approached in several ways. Some teams commit encrypted blobs to Git, others define secrets in Terraform, and some inject them at runtime through CI/CD. Each has its strengths and trade-offs.

The best practice is to use a secrets manager as the source of truth, but the right model depends on your stage and priorities. Small GitOps-first teams may find Git-encrypted secrets practical, as long as key management is disciplined. Mid-size organizations with compliance needs often lean on IaC plus a manager, since it integrates secrets into audit trails and rotation policies. Larger or fast-moving teams usually adopt runtime injection because it scales best, minimizes exposure, and enables short-lived credentials.

If you’re unsure where to start, here’s a quick checklist to guide you:

  • Centralize storage in a secrets management system.
  • Avoid storing secrets in Git. If your workflow requires it, use encryption tools like SOPS or Sealed Secrets.
  • IaC should reference managers, but the actual secrets-based values must live in secure storage, never in Git.
  • Fetch secrets dynamically at runtime for cloud services instead of baking them into builds.
  • Use dynamic secrets and automate secrets rotation.
  • Scope access with least privilege and use short-lived tokens.
  • Enforce role-based access control and isolate environments(dev, staging, prod)

Want to see secrets as code done right? Try a quick Doppler demo and see how centralized storage, IaC, and runtime delivery all fit together.

Enjoying this content? Stay up to date and get our latest blogs, guides, and tutorials.

Related Content

Explore More