Right, let’s talk about keeping your secrets out of your Git history, because right now, if you’re just committing your values.yaml files, you’re basically handing out your database passwords and API keys to anyone who can clone the repo. We’re better than that. Helm doesn’t handle encryption natively—it’s a package manager, not a vault—so we bring in a helper. The most common and robust tool for this job is helm-secrets, which is a Helm plugin that’s really just a slick wrapper around sops (Secrets OPerationS) or sometimes vals. We’re going to focus on the sops workflow because it’s brilliant and widely adopted.

Why sops is the right tool for the job

You could use something like git-crypt or blackbox, but sops has a killer feature: it’s multi-platform by design. It doesn’t just encrypt the entire file into a binary blob. It encrypts the values within your YAML (or JSON) file, leaving the structure intact. This means your file remains vaguely readable, you can see what keys are there (e.g., password: ENC[AES256_GCM,data:...]), and, crucially, you can still use git diff meaningfully to see which secret changed without revealing the secret itself. It achieves this by using a mix of asymmetric encryption (PGP or age) for your developers and symmetric encryption (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault) for your CI/CD pipelines. This hybrid approach is the secret sauce that makes it work for both humans and machines.

Installing the necessary tools

First, you need sops itself. On a Mac, it’s a brew install sops away. On Linux, grab the latest binary from the GitHub releases. Next, install the helm-secrets plugin. It’s not a standard helm plugin install; you use their installer script because life is never simple.

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/jkroepke/helm-secrets/main/scripts/install-helm-secrets.sh
chmod 700 get_helm.sh
./get_helm.sh

If you cringed at piping a script from the internet straight into bash, good. You should. I did too. You can always inspect it first. This is the mildly absurd part, but it’s the way they’ve chosen to distribute it.

Creating your encryption key and your first encrypted values file

Let’s use age (pronounced “ahj”), a modern, simple alternative to PGP. First, generate a keypair.

age-keygen -o age-key.txt

This gives you a public key (for encryption) and a private key (for decryption). Guard that age-key.txt file with your life, but do not commit it. Now, create a .sops.yaml file in your chart’s root directory to tell sops which key to use by default for files in this tree. This is a massive quality-of-life improvement.

# .sops.yaml
creation_rules:
  - path_regex: secrets\.yaml$
    age: "age1qlylz5f6f5pwjq0a454q5q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q" # Your public key

Now, let’s create a secrets.values.yaml file and let sops encrypt it.

sops --encrypt secrets.values.yaml > secrets.enc.yaml

Or, better yet, use the Helm plugin to do it all in one go, which ensures you’re using the correct config:

helm secrets create secrets.values.yaml

This will open your $EDITOR, you’ll paste in your sensitive data, save, and exit. The plugin then automatically encrypts it and saves it as secrets.enc.yaml. Your plaintext file is never saved to disk. Neat.

Integrating encrypted secrets into your Helm workflow

This is where the magic happens. You don’t run helm install or helm upgrade directly. You prefix them with helm secrets.

helm secrets upgrade --install my-app ./my-chart \
  -f values.yaml \
  -f secrets.enc.yaml

The plugin temporarily decrypts the file, feeds it to Helm, and then cleans up the decrypted temp file. From Helm’s perspective, it’s just another values file. You never see the decrypted content on disk during a normal operation.

The crucial CI/CD pipeline setup

Here’s the pitfall that gets everyone: your CI server needs to decrypt the file, but it doesn’t have your private key. Of course it doesn’t! So you need a different decryption method. This is where the cloud KMS systems shine. You can add another encryption key to your sops file. Your .sops.yaml can have multiple keys.

# .sops.yaml
creation_rules:
  - path_regex: secrets\.yaml$
    age: "age1qlylz5f6f5pwjq0a454q5q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q2q" # Your public key
    aws_kms: arn:aws:kms:us-east-1:123456789012:key/abcd1234-12ab-34cd-56ef-123456789012 # Your CI's KMS key

Now, when you create the file, it’s encrypted for both your age key and the AWS KMS key. You can git commit the file. In your CI pipeline, you just need to have the appropriate AWS IAM permissions (or GCP/Azure equivalent) configured. The helm-secrets plugin will automatically use that environment to decrypt the file without you having to manage a private key file on the runner. The designers got this part very, very right.

Best practices and final warnings

  1. .gitignore is your friend: Add *.dec.yaml and your private key file (age-key.txt, *.asc) to your .gitignore. The only files you should commit are secrets.enc.yaml and .sops.yaml.
  2. Rotate Keys: If a key is compromised, you can’t just remove it from .sops.yaml. You have to re-encrypt the entire file with a new rule that only contains the new keys. sops doesn’t “remove” a key; it just adds new ones. Use sops updatekeys secrets.enc.yaml to manage this.
  3. The Editor Quirk: Sometimes, when using helm secrets edit, if your editor crashes or you don’t save the file correctly, you can be left with a decrypted temp file. The plugin tries to clean up, but be aware of your surroundings. Always run git status afterwards to make sure nothing sensitive is staged.
  4. It’s a Plugin, Not Magic: Remember, everyone on your team and every CI system needs the helm-secrets plugin and sops installed. Document this ruthlessly. It’s the number one “it works on my machine” issue you’ll face.

It’s an extra step, sure. But it’s the difference between being the person who accidentally committed AWS credentials to a public repo and the person who knows better. Be the latter.