helm-secrets is a Helm plugin that allows you to store encrypted secrets directly within your Helm charts.

Here’s a breakdown of how it works and how to use it:

The Surprising Truth: Your Secrets Aren’t Really In Your Helm Charts

Helm charts are typically version-controlled and shared. If you commit your secrets directly into your chart, they’re exposed to anyone with access to your repository. helm-secrets solves this by encrypting your secrets before they’re committed, and then decrypting them only when Helm needs to deploy them.

Seeing helm-secrets in Action

Let’s imagine you have a simple Helm chart that needs a database password.

1. Install the Plugin

First, you need to install the helm-secrets plugin:

helm plugin install https://github.com/helm/helm-secrets

2. Set up Encryption

helm-secrets uses Mozilla SOPS for encryption. You’ll need to configure SOPS to manage your encryption keys. The simplest way is to use a local KMS (Key Management Service) or a cloud provider’s KMS. For local development, you can use the age backend, which is often the easiest to get started with.

First, install age:

brew install age # On macOS
# Or follow instructions for your OS at https://age-encryption.org/

Then, generate an age keypair:

age-keygen -o ~/.config/sops/age/key.txt

This will create ~/.config/sops/age/key.txt containing your public and private keys. SOPS will automatically pick this up.

3. Create an Encrypted Secret

Let’s create a secrets.yaml file in your Helm chart’s templates/ directory.

# templates/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-db-secrets
type: Opaque
data:

  DB_PASSWORD: {{ .Values.dbPassword | b64enc }}

Now, create a values.yaml file to hold your actual secret value (which will be encrypted).

# values.yaml
dbPassword: "supersecretpassword123"

To encrypt this value using helm-secrets, you’ll run:

helm secrets encrypt values.yaml

This command will modify values.yaml to encrypt the dbPassword field. It will look something like this:

# values.yaml (encrypted)
dbPassword: ENC(AgEDQZgQ5h9B5N4L...<long_encrypted_string>...)

4. Deploy with Helm

Now, when you deploy your chart, helm-secrets intercepts the process.

helm install my-release ./my-chart

helm-secrets will:

  1. Detect the ENC(...) markers in values.yaml.
  2. Use SOPS to decrypt the dbPassword value using your configured key.
  3. Pass the decrypted value to Helm.
  4. Helm will then create the Kubernetes Secret with the decrypted DB_PASSWORD.

You can verify the secret in Kubernetes:

kubectl get secret my-db-secrets -o yaml

You’ll see the DB_PASSWORD field is base64 encoded, but the original plaintext password was used during deployment.

The Mental Model: Decryption at the Helm Level

helm-secrets acts as a Helm plugin. When you run helm install, helm upgrade, or helm template with helm-secrets enabled (which it is by default if installed), it hooks into Helm’s lifecycle.

  1. Pre-processing: Before Helm even looks at your values.yaml or templates, helm-secrets scans them for encrypted values (marked with ENC(...)).
  2. Decryption: It uses SOPS to decrypt these values based on your configured KMS.
  3. Substitution: The decrypted values are then substituted back into the Helm context.
  4. Helm Execution: Helm proceeds as normal, using the now-decrypted values to render your templates and create Kubernetes resources.

This means your secrets are never stored in plaintext in your Git repository, but they are available in plaintext during the Helm execution phase for Helm to use.

The core levers you control are:

  • Encryption Backend: Which KMS (age, AWS KMS, GCP KMS, Azure Key Vault, etc.) SOPS uses. This determines how your encryption keys are managed and accessed.
  • Secret Values: What data you choose to encrypt. Typically, sensitive credentials, API keys, and passwords.
  • Access Control: Who has access to the decryption keys. This is your primary security control.

The Counterintuitive Bit: How Helm Actually Gets the Secrets

It’s not that helm-secrets magically bypasses Helm’s value parsing. Instead, it hijacks the value loading process. When Helm expects a plain string for a value, helm-secrets intercepts that expectation. If it sees an ENC(...) marker, it performs the decryption and then returns the decrypted plaintext string to Helm. Helm then processes this plaintext string as if it had been there all along, including base64 encoding it for the Kubernetes Secret’s data field if your template requires it. The key is that Helm itself never sees the encrypted form; it only ever deals with the plaintext during its rendering phase.

The Next Step: Managing Multiple Secrets and Environments

Once you’re comfortable encrypting individual values, the next logical step is to manage multiple secrets for different environments (dev, staging, prod) using SOPS’s ability to encrypt different values with different keys or by using different files for each environment.

Want structured learning?

Take the full Helm course →