Get a Password Out of Your Helm Chart
Encrypt Helm chart passwords with SOPS before deploying to a local kind cluster.
Introduction
30 Second Summary
A password can look scrambled while still being one quick step away from readable text. Once that file is shared, anyone who can read it can recover the password.
In this project, you will prove that a password stored in a Kubernetes Secret can be recovered as plain text on your laptop. You will protect the password with SOPS plus age before deploying it through Helm with the helm-secrets plugin.
What You'll Build
You will decode the exposed password on screen, replace the leaked file with ciphertext, and watch the same app receive the real password from a successful local deployment.
By the end of this project, you'll have:
- A local app deployed from a Helm chart. You can show its running pod as proof.
- A visible security proof that reveals the database password stored behind a Kubernetes Secret.
- An encrypted deployment flow that keeps ciphertext in Git while delivering the real password to the app.
- Secret Mission: Rotate the database password, re-encrypt the values file, and confirm the pod receives the new value without exposing it in Git.
Are there any prerequisites?
You will need Docker, kind, kubectl, Helm, SOPS, and age installed before the hands-on steps. No cloud account is required because everything runs on your laptop.
Before We Start
Your chart needs several local tools before it can expose the password problem safely. A working Docker engine provides the containers that become your local Kubernetes cluster.
You will prepare the command-line tools for deploying charts with Helm. You will also install SOPS 3.13.3, age, Git, and Gitleaks for the encryption workflow.
In this step, get ready to:
- Start Docker Desktop for the local cluster.
- Install the Kubernetes, encryption, version-control, and scanning tools.
- Create an empty helm-password-demo working directory.
Start Docker and check your toolchain
Each command-line tool has one job in the workflow. Checking them together reveals which parts of your local toolchain are already ready.
Before you run the checks, how many tools do you expect your terminal to find?
macOS
- Check the Docker engine and every required command by running:
docker version
kind version
kubectl version --client
helm version
sops --version
age --version
git --version
gitleaks version
What do these checks prove?
- The docker check prints both client information and server information when Docker Desktop is running.
- The kind, kubectl, and helm checks confirm that you can create a cluster and deploy a chart.
- The sops and age checks confirm that the encryption tools are available.
- The git and gitleaks checks confirm that you can commit the chart and scan its history.
Windows
- Check the Docker engine and every required command in PowerShell by running:
docker version
kind version
kubectl version --client
helm version
sops --version
age --version
git --version
gitleaks version
What do these checks prove?
- The docker check prints both client information and server information when Docker Desktop is running.
- The kind, kubectl, and helm checks confirm that you can create a cluster and deploy a chart.
- The sops and age checks confirm that the encryption tools are available.
- The git and gitleaks checks confirm that you can commit the chart and scan its history.
Your results determine the setup path below. Choose the tab that matches what you saw.
✔️ All checks pass
That is the heavy setup finished. Docker is accepting requests while the cluster, chart, encryption, version-control, and scanning commands are available.
- Confirm that the SOPS output includes version 3.13.3.
ⓧ SOPS has a different version
This project uses SOPS 3.13.3 so the encryption steps stay consistent. Replace the other version with the pinned release.
macOS
- Install SOPS 3.13.3 from the official release page.
- Close the current Terminal window after the installation finishes.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter.
- Confirm the pinned SOPS version by running:
sops --version
What should you see?
You should see SOPS version 3.13.3. The new Terminal window reloads your command search path.
Windows
- Install SOPS 3.13.3 from the official release page.
- Close the current PowerShell window after the installation finishes.
- Press the Windows key to open search.
- Type PowerShell and press Enter.
- Confirm the pinned SOPS version by running:
sops --version
What should you see?
You should see SOPS version 3.13.3. The new PowerShell window reloads your command search path.
ⓧ A command is missing
The guide list below covers every dependency. Tools that already printed a version can stay as they are.
macOS
- Install Docker Desktop from the official macOS guide if the Docker check failed.
- Install kind from its official quick start if the kind check failed.
- Install kubectl from the official macOS guide if the kubectl check failed.
- Install Helm from its official installation guide if the Helm check failed.
- Install SOPS 3.13.3 from the official release page if the SOPS check failed.
- Install age from its official project page if the age check failed.
- Install Git from the official macOS guide if the Git check failed.
- Install Gitleaks from its official project page if the Gitleaks check failed.
Reload newly installed commands
A running terminal can keep an older command search path. Starting a fresh Terminal window lets it find newly installed binaries.
- Close the current Terminal window after the installations finish.
- Press Cmd+Space to open Spotlight.
- Type Docker and press Enter to start Docker Desktop.
Docker Desktop can take a minute to start its engine. Wait until its interface reports that the engine is running.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter.
- Repeat the complete tool check by running:
docker version
kind version
kubectl version --client
helm version
sops --version
age --version
git --version
gitleaks version
What should you see?
Every command should print version information. The Docker output should contain client details and server details.
The SOPS output should include 3.13.3.
Windows
- Install Docker Desktop from the official Windows guide if the Docker check failed.
- Install kind from its official quick start if the kind check failed.
- Install kubectl from the official Kubernetes tools page if the kubectl check failed.
- Install Helm from its official installation guide if the Helm check failed.
- Install SOPS 3.13.3 from the official release page if the SOPS check failed.
- Install age from its official project page if the age check failed.
- Install Git from the official Windows guide if the Git check failed.
- Install Gitleaks from its official releases page if the Gitleaks check failed.
Reload newly installed commands
A running PowerShell window can keep an older command search path. Starting a fresh PowerShell window lets it find newly installed binaries.
- Close the current PowerShell window after the installations finish.
- Press the Windows key to open search.
- Type Docker Desktop and press Enter to start it.
Docker Desktop can take a minute to start its engine. Wait until its interface reports that the engine is running.
- Press the Windows key to open search.
- Type PowerShell and press Enter.
- Repeat the complete tool check by running:
docker version
kind version
kubectl version --client
helm version
sops --version
age --version
git --version
gitleaks version
What should you see?
Every command should print version information. The Docker output should contain client details and server details.
The SOPS output should include 3.13.3.
Still missing a command?
- Open a new terminal after every installer has finished so your command search path reloads.
- Check the relevant official guide for its instructions on adding the downloaded binary to your command search path.
- Start Docker Desktop before repeating docker version so the command can reach the engine.
Get help with a missing tool or Docker connection.
Keep the project in one folder
A dedicated folder keeps the future chart, encrypted values, and scanner output together. It also gives the later Git repository a clear boundary.
macOS
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The command changes the terminal's current location to your Desktop. Your new project folder will be easy to find there.
- Create the helm-password-demo folder by running these commands:
mkdir helm-password-demo
cd helm-password-demo
What do these commands do?
The first command creates the empty project directory. The second command makes that directory your terminal's current location.
Before the final check, what path do you expect the terminal to print?
- Confirm your location and inspect the empty directory by running:
pwd
ls -la
What should you see?
The printed path should end in Desktop/helm-password-demo. The directory listing should contain no project files.
Windows
- Move to your Desktop in PowerShell by running this command:
Set-Location -Path "$HOME\Desktop"
What does this command do?
The command changes PowerShell's current location to your Desktop. Your new project folder will be easy to find there.
- Create the helm-password-demo folder by running these commands:
New-Item -Path 'helm-password-demo' -ItemType Directory
Set-Location -Path 'helm-password-demo'
What do these commands do?
The first command creates the empty project directory. The second command makes that directory your PowerShell location.
Before the final check, what path do you expect PowerShell to print?
- Confirm your location and inspect the empty directory by running:
Get-Location
Get-ChildItem
What should you see?
The printed path should end in Desktop\helm-password-demo. The directory listing should contain no project files.
Your local environment is ready. Docker is running while every required command is available from an empty project directory.
Your toolchain now has everything the project needs. Next up, you will start the local cluster and deploy a chart with a plain-text password.
Deploy a Chart with a Plain Password
A Kubernetes Secret can deliver a password to an application. A plain password inside a chart remains exposed to anyone who can read the chart files.
In this step, you will establish that vulnerable baseline with a Helm chart on a local kind cluster. You will install the chart with super-secret-password visible in values.yaml before confirming that its Pod is running.
In this step, get ready to:
- Start a local kind cluster named helm-password-demo.
- Build a Helm chart that renders a plain password into a Kubernetes Secret.
- Install the chart as release password-demo.
Start the local cluster
The chart needs a Kubernetes API where Helm can create its resources. Kind provides that API inside a local container.
Cluster creation can take a few minutes while kind prepares its node image. Keep the terminal open while the progress messages appear.
- Create the helm-password-demo cluster by running this command:
kind create cluster --name helm-password-demo
What does this command do?
The --name flag gives the cluster a stable name. Kind also configures kubectl to use the new cluster context.
- Confirm the active Kubernetes context by running this command:
kubectl config current-context
What does this check prove?
The current context controls which cluster receives each later kubectl request. This check protects other clusters from accidental changes.
You should see kind-helm-password-demo. Your first milestone is in place because kubectl now points to the local cluster.
Cluster creation did not finish?
- Confirm that Docker is still running before retrying the cluster command.
- Free some memory if kind stops while preparing its control plane.
Use help me diagnose why my local kind cluster did not start if the progress output stops or reports a Docker problem.
Build the plain-text chart
A Helm chart combines metadata with default values. Its templates turn those values into YAML resources for Kubernetes.
This password is intentionally disposable. Never replace it with a real credential from another account or system.
- Create password-demo/Chart.yaml by running the code below:
mkdir -p password-demo/templates
cat > password-demo/Chart.yaml <<'EOF'
# Declare this as an installable Helm chart.
apiVersion: v2
name: password-demo
type: application
# Set the chart package version.
version: 0.1.0
EOF
What does this chart metadata do?
- The apiVersion field identifies the chart format.
- The name field identifies the chart as password-demo.
- The version field assigns package version 0.1.0.
- Confirm that Chart.yaml exists by running this command:
ls password-demo
What does this check show?
The directory listing confirms that the chart metadata was written inside password-demo.
You should see Chart.yaml beside the templates directory.
Chart.yaml missing?
Check that your terminal is still inside the existing helm-password-demo directory. Retry the file creation command from that location.
Use help me find why Chart.yaml was not created if the file remains missing.
- Create password-demo/values.yaml with the disposable password by running the code below:
cat > password-demo/values.yaml <<'EOF'
# Keep the demonstration password visible in the chart values.
database:
password: super-secret-password
EOF
Why use a nested value?
The database.password path groups the credential under its purpose. Helm templates can read it through .Values.database.password.
- Print the saved values file by running this command:
cat password-demo/values.yaml
What does this check reveal?
The output shows the password in readable text. Anyone who can read this file can copy the working credential.
You should see database.password set to super-secret-password.
Values file looks different?
Make sure password is indented by two spaces beneath database. YAML uses indentation to define nested values.
Use help me fix the indentation in values.yaml if the nesting does not match.
- Create password-demo/templates/secret.yaml by running the code below:
cat > password-demo/templates/secret.yaml <<'EOF'
# Render the plain chart value into a Kubernetes Secret.
apiVersion: v1
kind: Secret
metadata:
name: password-demo-secret
type: Opaque
stringData:
password: {{ .Values.database.password | quote }}
EOF
What does the Secret template do?
- The stringData field accepts the password as readable text.
- Kubernetes converts that value into the Secret's data field when the resource is created.
- The quote function renders the value as a YAML string.
- Confirm that the Secret template exists by running this command:
ls password-demo/templates
What does this check show?
The template listing confirms that Helm can find secret.yaml during rendering.
You should see secret.yaml in the output.
Secret template missing?
Check that the path includes the templates directory. Helm only renders resource templates stored there.
Use help me find my missing Secret template if the listing stays empty.
- Create password-demo/templates/deployment.yaml by running the code below:
cat > password-demo/templates/deployment.yaml <<'EOF'
# Keep one pod running for this local demonstration.
apiVersion: apps/v1
kind: Deployment
metadata:
name: password-demo
spec:
replicas: 1
selector:
matchLabels:
app: password-demo
template:
metadata:
labels:
app: password-demo
spec:
containers:
- name: password-demo
image: busybox:1.36
command:
- sh
- -c
- sleep 3600
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: password-demo-secret
key: password
EOF
What does the Deployment template do?
- The Deployment keeps one BusyBox container running for the demonstration.
- The secretKeyRef block reads the password key from password-demo-secret.
- The container receives that value through DATABASE_PASSWORD.
- Render the complete chart locally by running this command:
helm template password-demo ./password-demo
What does this render prove?
The helm template command combines values.yaml with both templates. It prints the resources without installing them.
You should see a Secret named password-demo-secret followed by a Deployment named password-demo.
Chart does not render?
- Check the indentation in both template files.
- Confirm that the password expression contains matching template braces.
- Confirm that database.password exists in values.yaml.
Use help me debug this Helm rendering failure if Helm reports a template or YAML problem.
Use these tabs to compare the finished chart before you install it.
✔️ Awesome, I've got everything!
Your chart metadata is ready. Your plain values and both templates are also ready.
ⓧ I'd like to double check the full code
# Declare this as an installable Helm chart.
apiVersion: v2
name: password-demo
type: application
# Set the chart package version.
version: 0.1.0
# Keep the demonstration password visible in the chart values.
database:
password: super-secret-password
# Render the plain chart value into a Kubernetes Secret.
apiVersion: v1
kind: Secret
metadata:
name: password-demo-secret
type: Opaque
stringData:
password: {{ .Values.database.password | quote }}
# Keep one pod running for this local demonstration.
apiVersion: apps/v1
kind: Deployment
metadata:
name: password-demo
spec:
replicas: 1
selector:
matchLabels:
app: password-demo
template:
metadata:
labels:
app: password-demo
spec:
containers:
- name: password-demo
image: busybox:1.36
command:
- sh
- -c
- sleep 3600
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: password-demo-secret
key: password
Install the chart and verify the pod
Rendering proves that Helm understands the chart. Installing it sends the Secret and Deployment to your local cluster as a named release.
The first install can take a few minutes while the cluster pulls the container image. The --wait flag keeps Helm running until the Deployment becomes ready.
- Install the local chart as release password-demo by running this command:
helm install password-demo ./password-demo --wait
What does this command create?
Helm installs the chart into the default namespace. The release contains password-demo-secret plus the password-demo Deployment.
You should see release details showing that the release is deployed. The application Pod now receives the Secret key through DATABASE_PASSWORD.
Release did not become ready?
- Confirm that the current context remains kind-helm-password-demo.
- Check your network connection if the cluster cannot pull the container image.
- Compare both templates with the full-code tab if Helm reports invalid YAML.
Use help me diagnose why my Helm release is not ready if the install stops or times out.
Before you run the final check, which signals do you expect from the cluster context, Helm release, Pod, and Secret?
- Verify the complete deployment state by running these commands:
kubectl config current-context
helm status password-demo
kubectl get pods
kubectl get secret password-demo-secret
What does the final check cover?
- The context command confirms that your requests still target kind-helm-password-demo.
- The Helm status confirms that release password-demo is deployed in the default namespace.
- The Pod listing confirms that the password-demo workload is ready.
- The Secret listing confirms that password-demo-secret contains one data key.
You should see kind-helm-password-demo as the context. The Helm output should identify password-demo as a deployed release in the default namespace.
You should also see one ready password-demo Pod with a running status. The Secret row should identify password-demo-secret as an opaque Secret with one data entry.
Your deliberately exposed baseline is running. Next, you will retrieve the stored Secret value and prove how quickly its encoding can be reversed.
Decode the Exposed Secret
Your Helm release is running with the password loaded into a Kubernetes Secret. Now you will test what someone with read access can recover.
That Secret stores the password as Base64-encoded data. Anyone who can read the field can reverse the encoding. The original credential comes back in plain text.
In this step, get ready to:
- Retrieve the encoded password from password-demo-secret.
- Decode the password with base64.
- Identify why the plain value in values.yaml is exposed.
Read the stored password value
The data.password field contains the encoded bytes that Kubernetes stores for the password. Reading that field exposes the encoded value without changing the Secret.
- Retrieve the encoded password value from password-demo-secret by running this command:
kubectl get secret password-demo-secret -o jsonpath='{.data.password}'
What does this command do?
- The kubectl command reads password-demo-secret from the current cluster.
- The jsonpath output format selects the data.password field.
- The command only reads the resource. Your Secret remains unchanged.
You should see c3VwZXItc2VjcmV0LXBhc3N3b3Jk in your terminal. That string is the encoded form of the password.
Can't read the Secret?
- Check that the Secret name is spelled exactly as password-demo-secret.
- Confirm that the kind cluster from the previous step is still running.
- Make sure your terminal still uses the cluster context from the previous step.
Help me diagnose why kubectl cannot read password-demo-secret from my local kind cluster.
Decode the exposed password
Base64 is a reversible encoding scheme. A decoder can recover the original bytes without any decryption key.
Before you run the next command, do you expect to see another encoded string or the original password?
- Read the field through a Base64 decoder by running this command:
kubectl get secret password-demo-secret -o jsonpath='{.data.password}' | base64 --decode
How does the pipeline expose the password?
- The kubectl command retrieves the encoded value.
- The pipe sends that value directly to base64.
- The --decode flag converts the encoded bytes back into the original password.
- The pipeline only reads data from the cluster. Your running pod remains unchanged.
You will see super-secret-password in the terminal. You have recovered the working credential with one read-only command.
Don't see the decoded password?
- Check that the data.password field matches the key stored in the Secret.
- Check that the quotes around the JSONPath expression are intact.
- Copy the pipeline again if your shell changed any braces or quote characters.
Help me find why the password decoding pipeline is not showing the expected value.
You have proved that encoding leaves the credential recoverable. Next, you will replace the plain-text path with an encrypted values file.
Encrypt the Password Values
You proved that base64 encoding leaves a Kubernetes Secret readable to anyone with access. The same password still sits in your chart where a Git commit could expose it.
In this step, you will use SOPS with an age key pair to protect the password. Your Helm chart will keep its structure while the sensitive value becomes ciphertext.
Why SOPS with age?
SOPS encrypts values inside structured files such as YAML. The readable keys preserve the shape that Helm expects.
Age provides a local key pair without requiring a cloud account. This keeps the entire exercise on your laptop.
In this step, get ready to:
- Generate an age key pair outside the project directory.
- Separate the password from the chart's non-secret values.
- Encrypt the secret values with SOPS.
Generate an age key pair
An age key pair has a public recipient for encryption. Its private identity unlocks the encrypted data.
This next command creates a private credential. Keeping it under ~/.config/sops/age/keys.txt prevents it from landing inside your project directory.
- Generate your age identity outside the project by running these commands:
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt
What do these commands do?
- The mkdir command prepares the local directory that stores your private identity.
- The -o option writes the new identity to ~/.config/sops/age/keys.txt.
- The -y option derives the matching public recipient from that identity.
- Record the final recipient line here: your age public recipient.
You should see one public recipient beginning with the age recipient prefix. Your private identity remains in the local key file.
Did the key generation fail?
- Check that the terminal still has permission to write inside your home directory.
- Remove an empty key file only if the command created one before failing.
Get help with the key-generation output.
Separate secret values from chart values
SOPS uses creation rules to choose which files receive encryption. A filename rule limits this key to the chart's dedicated secret values file.
- Create password-demo/.sops.yaml with your public recipient by running this command:
cat > password-demo/.sops.yaml <<'EOF'
# Encrypt the dedicated secret values file with this age recipient.
creation_rules:
- path_regex: secrets\.enc\.yaml$
age: [[AGE_PUBLIC_RECIPIENT="your age public recipient"]]
EOF
What does this configuration do?
- The creation_rules list tells SOPS how to encrypt new files.
- The path_regex rule matches only secrets.enc.yaml.
- The age field identifies the public recipient that can encrypt the file.
- Display the saved creation rule by running this command:
cat password-demo/.sops.yaml
What should I see?
You should see one creation rule for secrets.enc.yaml. The age field should contain the public recipient you recorded.
Does the recipient look wrong?
- Compare the recipient in password-demo/.sops.yaml with the final line printed by age-keygen.
- Confirm that the filename is exactly .sops.yaml.
Get help checking the SOPS creation rule.
The password now needs its own values file. The regular values.yaml file will keep only non-secret chart values.
- Separate the secret value from the non-secret values by running these commands:
cat > password-demo/secrets.enc.yaml <<'EOF'
# SOPS encrypts this value before the file reaches Git.
database:
password: super-secret-password
EOF
cat > password-demo/values.yaml <<'EOF'
# Keep only non-secret chart values in this file.
database: {}
EOF
What changed in the chart?
The password has moved to secrets.enc.yaml. The empty database map keeps the non-secret values file ready for Helm value merging.
The installed release remains unchanged because this step does not run a Helm deployment.
- Display the two values files by running these commands:
cat password-demo/values.yaml
cat password-demo/secrets.enc.yaml
What should I see?
You should see no password under values.yaml. The password should appear only in secrets.enc.yaml at this point.
Is the password still in values.yaml?
- Check that the second file-writing command targets password-demo/values.yaml.
- Confirm that password-demo/secrets.enc.yaml contains the database.password value.
Get help checking the values split.
Encrypt and inspect the secret values
The secret values file is still readable until SOPS encrypts it. In-place encryption replaces the plaintext value while preserving the YAML structure.
Before you encrypt the file, do you expect the password text to remain visible afterward?
- Encrypt password-demo/secrets.enc.yaml in place by running this command:
sops --config password-demo/.sops.yaml encrypt -i password-demo/secrets.enc.yaml
What does this command do?
The --config option selects the chart's SOPS configuration. The -i option replaces the source file with its encrypted form.
SOPS leaves the YAML keys readable while encrypting the password value. It also adds metadata needed for decryption and integrity checks.
Before you inspect both files, which one do you expect to contain ciphertext?
- Display the final values files by running these commands:
cat password-demo/values.yaml
cat password-demo/secrets.enc.yaml
What should I see?
You should see no password in values.yaml. In secrets.enc.yaml, the password value should be a long encrypted value instead of readable text.
You have closed the repository leak. The chart now stores ciphertext where the plain password used to sit.
Did SOPS refuse to encrypt the file?
- Check that password-demo/.sops.yaml contains your complete public recipient.
- Confirm that the filename is exactly password-demo/secrets.enc.yaml so it matches the creation rule.
- Confirm that the original YAML indentation places password beneath database.
Get help diagnosing the SOPS encryption failure.
✔️ Awesome, I've got everything!
Great. Your public encryption rule is saved. Your normal values file contains no password.
ⓧ I'd like to double check the full code
Compare the deterministic files below with your chart. The terminal check above covers the generated ciphertext and SOPS metadata in secrets.enc.yaml.
# Encrypt the dedicated secret values file with this age recipient.
creation_rules:
- path_regex: secrets\.enc\.yaml$
age: [[AGE_PUBLIC_RECIPIENT="your age public recipient"]]
# Keep only non-secret chart values in this file.
database: {}
Your password is encrypted for storage in the project. Next up, you will connect that encrypted file to Helm and deploy it without exposing the plaintext to Git.
Deploy with Encrypted Values
The encrypted file is ready. Your running Kubernetes workload still comes from the earlier plain-text deployment.
This step adds the helm-secrets plugin to Helm. It lets SOPS use your local age key during the upgrade without restoring plaintext to a project file.
In this step, get ready to:
- Install helm-secrets version v4.7.8.
- Point SOPS to the age private key outside the project.
- Upgrade the password-demo release with encrypted values.
Why helm-secrets with SOPS?
Sealed Secrets adds a controller to the cluster. Helm-secrets fits this local chart because it decrypts the encrypted values file during the Helm command.
Check and install helm-secrets
A Helm plugin runs with your user permissions. Pinning the plugin to v4.7.8 gives you a known version for this deployment.
- Check the installed helm-secrets version by running this command:
helm secrets -v
What does this check do?
The -v option asks helm-secrets to print its installed version. A missing plugin prevents this command from returning a version.
- Select the tab that matches your terminal output.
✔️ I see version 4.7.8
Your helm-secrets installation already matches the pinned version. You can continue with the existing plugin.
ⓧ I see another version
Removing the old plugin only replaces its local plugin files. Your chart and kind cluster stay untouched.
- Remove the existing helm-secrets plugin by running this command:
helm plugin uninstall secrets
What does this command do?
Helm removes the plugin registered under the name secrets. This clears the old version before you install the pinned release.
- Check your Helm major version by running this command:
helm version --short
Why check the Helm version?
Helm 3 installs the plugin from its tagged source. Helm 4 installs the packaged core plugin from the release assets.
- Choose the tab matching the first number in the version output.
Helm 3
- Install helm-secrets v4.7.8 for Helm 3 by running this command:
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.7.8
What does this command do?
Helm downloads the helm-secrets source at the v4.7.8 tag. It registers the plugin under the secrets command.
Helm 4
- Install the helm-secrets v4.7.8 core package for Helm 4 by running this command:
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.8/secrets-4.7.8.tgz
What does this command do?
Helm downloads the core plugin package from the v4.7.8 release. The core package provides the wrapper used by this deployment.
ⓧ helm-secrets is not installed
Helm is ready from the earlier setup. You only need to add the plugin that handles encrypted values.
- Check your Helm major version by running this command:
helm version --short
Why check the Helm version?
Helm 3 installs the plugin from its tagged source. Helm 4 installs the packaged core plugin from the release assets.
- Choose the tab matching the first number in the version output.
Helm 3
- Install helm-secrets v4.7.8 for Helm 3 by running this command:
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.7.8
What does this command do?
Helm downloads the helm-secrets source at the v4.7.8 tag. It registers the plugin under the secrets command.
Helm 4
- Install the helm-secrets v4.7.8 core package for Helm 4 by running this command:
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v4.7.8/secrets-4.7.8.tgz
What does this command do?
Helm downloads the core plugin package from the v4.7.8 release. The core package provides the wrapper used by this deployment.
- Confirm the pinned plugin is available by running this command:
helm secrets -v
What should this confirm?
The command should print 4.7.8. This proves Helm can invoke the pinned helm-secrets plugin.
The plugin is ready. Helm can now pass encrypted values through SOPS during an upgrade.
Plugin version not showing?
- Check that the installation command completed without a network failure.
- Confirm that you selected the installation command for your Helm major version.
- Run the version check in the same terminal where Helm is available.
Ask for help with the installation error: Help me troubleshoot my helm-secrets installation.
Deploy the encrypted values
SOPS needs the path to your age private key before it can decrypt password-demo/secrets.enc.yaml. The key remains at ~/.config/sops/age/keys.txt outside the project.
- Set the age key path for this terminal session by running this command:
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
What does this command do?
The SOPS_AGE_KEY_FILE environment variable tells SOPS which private key file can unlock the encrypted values. The exported value applies to commands run from this terminal session.
Helm-secrets wraps the upgrade command so SOPS can decrypt the encrypted values on the fly. The plugin removes its decrypted temporary file after Helm finishes.
Before you deploy, do you expect the release upgrade to need an unencrypted password file in the project?
- Upgrade the password-demo release with both values files by running this command:
helm secrets upgrade password-demo ./password-demo \
--namespace default \
-f password-demo/values.yaml \
-f password-demo/secrets.enc.yaml \
--wait
What happens during this upgrade?
- The first -f option loads the non-secret settings from password-demo/values.yaml.
- The second -f option passes password-demo/secrets.enc.yaml through SOPS.
- The decrypted database password is available to Helm while it renders the chart.
- The --wait option keeps the command active until the upgraded resources are ready.
You should see output confirming that the release was upgraded. The result should report a deployed release in the default namespace.
Encrypted upgrade failed?
- Confirm that your terminal is still in the helm-password-demo directory.
- Confirm that SOPS_AGE_KEY_FILE points to ~/.config/sops/age/keys.txt in this terminal session.
- Check that password-demo/secrets.enc.yaml remains encrypted with the public recipient matching your private key.
Ask for help with the deployment output: Help me troubleshoot my encrypted Helm upgrade.
Verify the upgraded release
The upgrade output is your first signal. Helm's stored release state provides a separate check that the encrypted deployment completed.
Before you check, what release state do you expect after the encrypted upgrade finished successfully?
- Confirm the release state by running this command:
helm status password-demo --namespace default
What does this status show?
Helm reports the release namespace. It also reports the current revision and deployment state.
You should see the password-demo release in the deployed state. This confirms Helm accepted the encrypted values upgrade.
- Confirm the password-demo workload is ready by running this command:
kubectl rollout status deployment/password-demo --namespace default
What does this rollout check prove?
Kubectl watches the password-demo Deployment until its latest rollout is complete. A successful result confirms the workload is running after the Helm upgrade.
That encrypted handoff is complete. Your release is live from encrypted values while the age private key stays outside the project.
Rollout still waiting?
- Confirm that Docker is still running for the local kind cluster.
- Check that kubectl still uses the kind-helm-password-demo context.
- Review the Helm upgrade output for a chart rendering or decryption failure.
Ask for help with the rollout state: Help me diagnose why password-demo is not completing its rollout.
Your chart now deploys from ciphertext without keeping the password in an unencrypted project file. Next, you will check the running container to prove it received the real password.
Verify the App Password
Your encrypted Helm release is running from secrets.enc.yaml.
The final proof lives inside the running pod. Its DATABASE_PASSWORD environment variable shows the value that the application received from the Kubernetes Secret.
In this step, get ready to:
- Run a command inside the live password-demo pod.
- Confirm that DATABASE_PASSWORD contains the decrypted password.
Inspect the pod environment
The kubectl command can execute a program inside an existing container. The env program prints the environment available to the application.
Before you run this check, predict whether the container holds ciphertext or the original password.
- Inspect the live application environment by running this command:
kubectl exec deploy/password-demo -- env
What does this command do?
- The exec subcommand runs a program inside a container.
- The deploy/password-demo target selects a pod from the password-demo Deployment.
- The -- separator keeps the arguments for kubectl separate from the program that runs in the container.
- The env program prints each environment variable that the application can read.
In the output, you will see DATABASE_PASSWORD=super-secret-password.
That closes the loop. Your running application received the real password from the encrypted deployment.
Can't see the password variable?
- Confirm that your terminal still uses the kind-helm-password-demo context.
- Check that the password-demo Deployment still has a running pod.
- If DATABASE_PASSWORD is missing, confirm that the Deployment reads the password key from password-demo-secret.
Help me diagnose the missing password variable.
Your encrypted secret now works from file to application. Next up, you will commit the chart and scan the repository for exposed credentials.
Commit and Scan the Chart
Your running pod has already displayed DATABASE_PASSWORD=super-secret-password. The encrypted deployment delivered the intended value.
A safe Helm release also needs a repository that contains ciphertext alone. Committing the private key would undo the protection.
Git will preserve the encrypted chart as version history. Gitleaks will inspect the current files plus that history for exposed secrets.
In this step, get ready to:
- Prepare a safe repository snapshot of the chart.
- Commit the encrypted chart without the age private key.
- Scan the repository for exposed secrets.
Prepare the repository snapshot
Git uses a staging area to define the exact snapshot for a commit. Reviewing that snapshot gives you one final chance to catch an unsafe file.
The SOPS configuration stays inside helm-password-demo. The private age key remains outside the repository at ~/.config/sops/age/keys.txt.
- Prepare the repository snapshot by running these commands:
git init
git add .
git status
git diff --cached
What do these commands prepare?
- The first line creates the .git repository metadata inside helm-password-demo.
- The second line copies the current project contents into Git's staging area.
- The third line lists the paths selected for the commit.
- The fourth line displays the staged content for a final safety review.
You'll see password-demo/.sops.yaml, password-demo/Chart.yaml, password-demo/secrets.enc.yaml, password-demo/values.yaml, and the chart templates in the staged file list.
The staged diff shows ciphertext in password-demo/secrets.enc.yaml. It does not show super-secret-password.
Seeing an unsafe staged file?
- Stop before committing if the staged list contains a plain-text password file.
- Confirm the private key remains at ~/.config/sops/age/keys.txt outside helm-password-demo.
- Check that password-demo/values.yaml contains non-secret values only.
Help me review the staged Helm chart before I commit it.
Create the first commit
A commit turns the reviewed staging snapshot into durable version history. The history scan needs this commit before it can inspect the repository record.
- Create the initial commit by running these commands:
git commit -m 'Initial project version'
git status
What did Git record?
- The first line records the staged chart as the repository's initial commit.
- The second line checks whether any uncommitted changes remain.
You'll see a commit summary for the chart files. Git then reports a clean working tree.
Commit not created?
- Check that the staged file list appeared before you ran the commit.
- Follow Git's terminal guidance if it requests an author name or email.
- Run the commit block again after completing the requested configuration.
Help me complete the initial Git commit for my encrypted Helm chart.
Scan for leaked secrets
One Gitleaks mode inspects the files currently stored on disk. The other mode inspects patches recorded in Git history.
Before you run the scans, do you expect the encrypted values file to produce a finding.
- Scan both repository surfaces by running these commands:
gitleaks dir
gitleaks git
What did Gitleaks inspect?
- The first scan checks the current contents of files under helm-password-demo.
- The second scan checks the patches stored in the Git repository.
- Together they cover the working tree plus the committed chart history.
Strong finish. Both Gitleaks scans complete with no findings.
Your repository now stores ciphertext in password-demo/secrets.enc.yaml. The running pod still receives the real password through the decrypted Kubernetes Secret.
Did Gitleaks report a finding?
- Read the reported path before changing any files.
- Confirm password-demo/values.yaml does not contain database.password.
- Confirm the age private key remains at ~/.config/sops/age/keys.txt outside the repository.
Help me investigate the Gitleaks finding without exposing the secret.
Secret mission
Audit the Secret Exposure Boundary
Encrypted Git files solve one part of secret handling. Audit the repository boundary first. Then trace the value through the release into the running pod.
Clean Up Your Resources
Clean Up Your Resources
Your project runs entirely on your laptop, so it creates no ongoing cloud costs. Choose whether to keep the local resources available, pause the runtime, or delete everything.
Resources you used:
- The local kind cluster helm-password-demo. It contains the Helm release. It also contains the Kubernetes Secret. The Deployment and pod run inside the same cluster.
- The local helm-password-demo folder. It contains your Git repository and chart files.
- The age private key at ~/.config/sops/age/keys.txt. It decrypts the SOPS-encrypted password-demo/secrets.enc.yaml file.
Keep everything running
No action is needed. Choose this option if you want the deployed app ready for more experiments.
- Keep Docker running so the local cluster remains available.
- Keep the helm-password-demo folder so your encrypted chart remains available.
- Keep ~/.config/sops/age/keys.txt outside the repository so the encrypted values remain decryptable.
- Store a protected backup of the age private key outside the repository.
Pause - I'll come back to this later
Stop Docker to release the memory used by the local cluster. Your project folder and decryption key remain ready for later.
- Stop Docker using your operating system's app controls.
- Keep the helm-password-demo folder on your laptop.
- Keep ~/.config/sops/age/keys.txt outside the repository.
- Press Cmd+Space on macOS or the Windows key on Windows when you return.
- Type Docker into the search bar.
- Press Enter to restart Docker.
- Wait for Docker to finish starting before you resume the project.
Delete - I don't want to use this again
This option removes the cluster, the repository, and the local decryption key. Choose it when you do not plan to reopen the project.
Protect Your Last Decryption Key
The age private key is the only local key that can decrypt password-demo/secrets.enc.yaml. Deleting it makes every remaining copy of that encrypted file unreadable.
Deleting this key is permanent. Keep a protected backup if you need to use any copy of the encrypted repository again.
macOS
- Delete all three local resources by running these commands:
kind delete cluster --name helm-password-demo
cd ..
rm -rf helm-password-demo
rm ~/.config/sops/age/keys.txt
What Do These Commands Remove?
- The first command deletes the helm-password-demo cluster. Its Helm release, Secret, Deployment, and pod are removed with it.
- The second command moves your terminal to the folder above helm-password-demo.
- The third command deletes the project folder and its Git repository.
- The fourth command deletes the age private key from ~/.config/sops/age/keys.txt.
Cleanup Command Failed?
If cluster deletion fails, confirm Docker is still running. Run the cluster deletion command again after Docker is ready.
If the project folder remains, close any editor or terminal still using it. Run the cleanup commands again from the folder above helm-password-demo.
Help me finish the cleanup.
Cleanup complete. Your local cluster, project folder, and decryption key are gone.
Windows
- Delete all three local resources from PowerShell by running these commands:
kind delete cluster --name helm-password-demo
Set-Location ..
Remove-Item -Recurse -Force helm-password-demo
Remove-Item ~/.config/sops/age/keys.txt
What Do These Commands Remove?
- The first command deletes the helm-password-demo cluster. Its Helm release, Secret, Deployment, and pod are removed with it.
- The second command moves PowerShell to the folder above helm-password-demo.
- The third command deletes the project folder and its Git repository.
- The fourth command deletes the age private key from ~/.config/sops/age/keys.txt.
Cleanup Command Failed?
If cluster deletion fails, confirm Docker is still running. Run the cluster deletion command again after Docker is ready.
If the project folder remains, close any editor or terminal still using it. Run the cleanup commands again from the folder above helm-password-demo.
Help me finish the cleanup.
Cleanup complete. Your local cluster, project folder, and decryption key are gone.
Linux
- Delete all three local resources by running these commands:
kind delete cluster --name helm-password-demo
cd ..
rm -rf helm-password-demo
rm ~/.config/sops/age/keys.txt
What Do These Commands Remove?
- The first command deletes the helm-password-demo cluster. Its Helm release, Secret, Deployment, and pod are removed with it.
- The second command moves your terminal to the folder above helm-password-demo.
- The third command deletes the project folder and its Git repository.
- The fourth command deletes the age private key from ~/.config/sops/age/keys.txt.
Cleanup Command Failed?
If cluster deletion fails, confirm Docker is still running. Run the cluster deletion command again after Docker is ready.
If the project folder remains, close any editor or terminal still using it. Run the cleanup commands again from the folder above helm-password-demo.
Help me finish the cleanup.
Cleanup complete. Your local cluster, project folder, and decryption key are gone.
Nice Work!
Nice Work!
That is a wrap! Your local Kubernetes app now receives its database password from a Helm chart whose committed values stay encrypted through SOPS using an age key.
What you learned:
- Exposed the limits of base64 encoding by decoding a Kubernetes Secret back into the plain password.
- Protected the committed password with SOPS ciphertext in password-demo/secrets.enc.yaml. Kept the age private key outside the repository at ~/.config/sops/age/keys.txt.
- Deployed the encrypted values through helm-secrets so the running pod received the real credential. Scanned the Git repository with Gitleaks to confirm the encrypted file produced no finding.
- Secret Mission: Extended the encrypted Helm workflow through an optional challenge that pushed your secret-management skills further.
Ready to quiz yourself?