Secret Management#
In this repository I use multiple ways to encrypt secrets, utilizing sops and age.
- Kubernetes Secrets / Manifests are encrypted via KSOPS (described in this document)
- Kustomize managed Helm values are encrypted via sops and decrypted by an ArgoCD ConfigManagementPlugin
- Multi-source ArgoCD Helm values are encrypted via sops and decrypted at render time by helm-secrets
age is the recommended encryption tool for sops as it is more secure and easier to use than gpg.
Requirements#
Preparation#
For all variants we need two age keypairs. One for local use and one for ArgoCD.
Example output:
ArgoCD needs the private key of the local keypair to decrypt the secrets. So we create a kubernetes secret with the private key that ArgoCD gets mounted.
Adjusted helm values to mount the sops-age secret into the argocd-server pod:
| values.yaml | |
|---|---|
Repository configuration#
Info
The secrets need to be encrypted with both public keys. The ArgoCD key is used to decrypt the secrets in the ArgoCD cluster and the local key is used to de- and encrypt the secrets locally.
Create a .sops.yaml file in the repository root. Example:
The .sops file for this repository differs from this example.
Kubernetes Secrets / Manifests via KSOPS#
To use sops with ArgoCD, you need to mount ksops and the sops-age key into the argocd-server pod. The following helm values start the ksops container as an initContainer, copies the ksops and kustomize binaries into the custom-tools volume and mounts the binaries from the custom-tools volume into the repo server container. The sops-age key is also mounted into the repo server container.
Adjusting kustomize configuration#
Info
To tell kustomize to use ksops for decryption, we need to add a generators configuration to the kustomization.yaml file.
| kustomize-secret-generator.yaml | |
|---|---|
The backup-secrets.enc.yaml is just a normal kubernetes manifest but with sops encrypted values: Example emby backup-secrets.enc.yaml. Do not add thouse manifests to the resource list of the kustomization.yaml because this would result in a duplicate resource error.
Kustomize managed Helm values#
This repository makes heavy use of kustomize rendering helm charts. Kustomize can manage helm values either directly in the kustomization.yaml or in a separate file. The helm values can contain sensitive values and it's not possible to encrypt values in the kustomization.yaml file directly so we need to use a separate helm values file.
There are two patterns for managing Helm values with SOPS encryption, depending on whether the values file contains only secrets or a mix of secrets and non-sensitive configuration.
Pattern 1: Legacy - All values encrypted (values.enc.yaml only)#
Info
The original workflow creates an encrypted values.enc.yaml, tells kustomize to get the helm values from values.yaml and uses an ArgoCD ConfigManagementPlugin to decrypt the values.enc.yaml to values.yaml. The ConfigManagementPlugin gets executed when ArgoCD finds a values.enc.yaml before kustomize renders the kubernetes manifests.
| kustomization.yaml | |
|---|---|
Pattern 2: Split values (values.public.yaml + values.enc.yaml)#
To allow Renovate to update image tags and other non-sensitive values, the values can be split into two files:
- values.public.yaml - Non-sensitive values (image tags, resource limits, etc.) committed in plaintext. Renovate can read and update this file.
- values.enc.yaml - Only SOPS-encrypted secret values (passwords, tokens, hostnames, etc.)
The CMP detects the presence of values.public.yaml and decrypts values.enc.yaml to values.secrets.yaml instead of overwriting values.yaml. Kustomize then merges both files via additionalValuesFiles (requires kustomize 5.0+):
| kustomization.yaml | |
|---|---|
If an app has no secrets at all in its Helm values, it only needs values.public.yaml without a values.enc.yaml. The CMP will not trigger (discover rule) and ArgoCD uses the standard kustomize builder.
Note
values.yaml is listed in .gitignore to prevent accidentally committing decrypted secrets. The naming convention values.public.yaml makes the intent explicit: this file is safe to commit.
Configuration of the ConfigManagementPlugin#
If ArgoCD finds a values.enc.yaml in an application directory, it runs the CMP cmp-sops-decrypt. The CMP supports both patterns:
- If
values.public.yamlexists: decryptsvalues.enc.yamltovalues.secrets.yaml(split pattern) - If
values.public.yamldoes not exist: decryptsvalues.enc.yamltovalues.yaml(legacy pattern)
The ConfigManagementPlugin is configured as a configmap within a separate argo-cd app deployment. Additionally it needs helm and sops binaries for the argo-cd repo-server which get configured via a sidecar container. So the deployment of it needs to be extended.
ConfigManagementPlugin:
The adjusted helm values for the argo-cd repo-server. Some of the adjustments are dependent on changes from the previous section (like ksops and kustomize usage).
This basically builds the plugin container with all required tools on-demand.
Of course, the configuration could be way shorter if a container, that already includes the following binaries, would be used as extraContainer 🤷
- kustomize (from ksops)
- ksops
- sops
- helm
More information about my journey to en- and decrypt values.yaml can be found in the following ksops issue on github: Support kustomize helmCharts valuesFile.
Multi-source Helm values with helm-secrets#
The CMP-based pattern only works for single-source ArgoCD Applications where ArgoCD invokes Kustomize on a directory inside this repository. ArgoCD also supports multi-source Applications where one source is an external Helm chart repository (or a Git repository containing the chart) and a second source is this repository providing values via a $values reference. In that flow ArgoCD calls helm template directly — no Kustomize, no CMP — so values.enc.yaml would be passed to Helm without decryption.
For this case the helm-secrets Helm plugin is integrated into the argocd-repo-server. It is wrapped around the helm binary so any helm install, helm upgrade, helm template, helm lint or helm diff invocation is intercepted and routed through helm secrets, which transparently decrypts SOPS-encrypted value files before passing them to Helm.
Why a wrapper is needed#
HELM_SECRETS_WRAPPER_ENABLED=true alone is not enough. The wrapper is a small shell script (scripts/wrapper/helm.sh) that ships with helm-secrets and rewrites Helm subcommands. It must be placed in PATH before the real Helm binary so it is the first match for helm. The wrapper itself then calls the real Helm via $HELM_BIN to avoid recursion.
The argocd-repo-server has the standard PATH:
The real Helm binary lives at /usr/local/bin/helm. Mounting the wrapper as /usr/local/sbin/helm lets it shadow the real binary without touching the original.
Repo-server configuration#
In addition to the existing custom-tools volume and SOPS / age setup, add an init container that downloads helm-secrets and stages the wrapper as a standalone file. Then mount the plugin directory (so helm plugin list resolves secrets) and the wrapper file:
HELM_SECRETS_VALUES_ALLOW_ABSOLUTE_PATH=true is required so helm-secrets accepts the absolute paths that ArgoCD generates when it materializes $values/... references into the repo-server filesystem.
Application manifest pattern#
A multi-source Application points the chart source at the upstream Helm/Git repo and references the encrypted values file via $values:
No secrets:// URL prefix is used. With HELM_SECRETS_WRAPPER_ENABLED=true the wrapper detects the SOPS-encrypted file from its content and decrypts it on the fly. The prefix-style references documented by helm-secrets do not work with multi-source apps because ArgoCD requires the $values/ source reference to be at the start of the path string.
En- and decrypting helm values and manifests#
To encrypt a file inplace, use the following command:
To decrypt a file inplace, use the following command:
Tip
If you're working with VSCode I can recommend the extension @signageos/vscode-sops which automatically decrypts and encrypts secrets on save.
It can also automatically encrypt files which are not yet encrypted. To enable this feature, add the following to your settings.json:
This will automatically encrypt files which match the creation_rules in the .sops.yaml file.