"The Vault" — Secret Management with HashiCorp Vault and Sealed Secrets
"The Vault" — Secret Management with HashiCorp Vault and Sealed Secrets
It was a Tuesday morning, and Alex, NovaCraft’s newly minted Platform Lead, was staring at a Slack notification that made his blood run cold. A red, flashing alert from their continuous integration pipeline screamed: “SECRET DETECTED IN GIT COMMIT.”
Someone had accidentally committed a production database password to a public-facing Git repository. The security team was already on a war footing, and the all-hands-on-deck incident response was in full swing. Alex’s mind raced. He had spent the last six months evangelizing Kubernetes, migrating services, and building what he thought was a robust platform. But this single, simple mistake had exposed a fundamental weakness in their security posture.
As the initial chaos subsided and the immediate fire was extinguished—the commit was scrubbed, the password rotated, and access logs audited—Alex was left with a lingering sense of unease. He had followed the Kubernetes gospel, using standard Secret objects to manage sensitive information. But the incident had laid bare a painful truth: Kubernetes Secrets, by themselves, are not enough.
The Illusion of Security: Why Kubernetes Secrets Fall Short
For many, the name Secret in Kubernetes implies a level of security that doesn’t quite match reality. While they are a step up from hardcoding credentials in container images or environment variables, they have significant limitations, especially as an organization scales.
A Kubernetes Secret is just a Base64-encoded string. Anyone with API access to the cluster can read it. Base64 is encoding, not encryption. It’s like writing a postcard in a language that anyone with a decoder ring can read.
Here’s a breakdown of the core issues Alex was now grappling with:
| Limitation | Description |
|---|---|
| Weak by Default | As mentioned, Secrets are merely Base64 encoded. While there are options for encryption at rest (e.g., using a KMS provider with the API server), this isn’t the default configuration and requires careful setup. Even then, anyone with get access to Secrets in a namespace can retrieve the plaintext value. |
| Static and Unaudited | Secrets are static. Once created, they often live forever unless manually rotated. There’s no built-in mechanism for leasing, renewing, or automatically revoking them. Auditing who accessed which secret and when is a complex, after-the-fact process of sifting through Kubernetes API server logs. |
| GitOps Nightmare | This was the trigger for NovaCraft’s incident. How do you store your application manifests, including the need for Secrets, in Git without storing the plaintext secrets themselves? Committing a standard Secret manifest means committing the Base64-encoded (i.e., plaintext) secret. This is a cardinal sin of security. |
| No Centralized Management | In a large organization with multiple clusters and teams, secrets become scattered. There’s no single source of truth, making it impossible to enforce consistent policies, manage rotation schedules, or get a global view of the secret landscape. |
Alex realized that to truly secure NovaCraft’s platform, he needed to move beyond the basics. He needed a system that could provide centralized management, strong encryption, dynamic secrets, and a secure workflow for GitOps. His research led him to a powerful trifecta of tools designed to solve these very problems: HashiCorp Vault, Sealed Secrets, and the External Secrets Operator.
This chapter is the story of how Alex went “Beyond the Cluster” to build a production-grade secret management solution. We’ll dive deep into the architecture of these tools, compare their strengths, and walk through a hands-on implementation that you can replicate on your own machine.
The Fort Knox of Secrets: An Introduction to HashiCorp Vault
Alex’s first and most significant discovery was HashiCorp Vault. Vault is an open-source tool that provides a centralized, secure, and highly available solution for managing secrets. It’s not just a key-value store; it’s a complete platform for secrets management, encryption as a service, and identity-based access.
Vault’s Architecture: A Fortress Model
To understand Vault, you need to understand its core architectural principles. Think of it as a digital fortress with multiple layers of defense.

-
Storage Backend: Vault doesn’t store data itself. It relies on a separate storage backend for persistence. This could be a cloud storage service like Amazon S3 or Google Cloud Storage, a database like PostgreSQL, or even a local file system. The key is that Vault considers the storage backend to be untrusted. All data is encrypted by Vault before it is written to the storage backend.
-
The Barrier: The cryptographic boundary that protects Vault is called the Barrier. Everything that passes through the barrier is encrypted. This is the core of Vault’s security model. Even if an attacker gains access to the storage backend, the data is useless without the encryption keys, which are kept within the barrier.
-
Seal/Unseal: When a Vault server starts, it’s in a sealed state. It knows where its data is, but it doesn’t know how to decrypt it. To become operational, it must be unsealed. This process involves providing a set of unseal keys. By default, Vault uses an algorithm called Shamir’s Secret Sharing to split the master key into multiple shards. A certain threshold of these shards is required to reconstruct the master key and unseal the Vault. This prevents a single person from having unilateral access to all secrets.
-
Auth Methods: Before a client can interact with Vault, it must authenticate. Vault supports a wide range of auth methods, from traditional username/password and tokens to more advanced methods like AWS IAM, Kubernetes Service Accounts, and GitHub teams. Authentication is about verifying who you are.
-
Policies: Once authenticated, Vault uses policies to determine what you are allowed to do. Policies are written in HashiCorp Configuration Language (HCL) and provide fine-grained control over which paths and operations a client can access. Vault operates on a principle of deny by default.
-
Secrets Engines: This is where the magic happens. Secrets engines are responsible for storing, generating, or encrypting data. There are many types:
- KV (Key-Value): The simplest secrets engine, for storing arbitrary secrets.
- Database: Dynamically generates database credentials with a specific time-to-live (TTL).
- AWS: Dynamically generates AWS IAM credentials.
- Transit: Provides encryption as a service, allowing applications to encrypt data without having access to the encryption keys.
The Power of Dynamic Secrets
The concept of dynamic secrets was a game-changer for Alex. Instead of storing static, long-lived credentials in Vault, you configure a secrets engine (like the database or AWS engine) with a set of high-privilege credentials. Then, when an application needs to access the database, it asks Vault for credentials. Vault connects to the database, generates a new set of credentials with the exact permissions the application needs, and sets a TTL on them. When the TTL expires, Vault automatically revokes the credentials.
This approach has profound security implications:
- Reduced Risk of Leaks: The credentials are short-lived. Even if they are leaked, their window of usability is minimal.
- No More Credential Sprawl: You don’t have static credentials scattered across your infrastructure.
- Automated Rotation: Rotation is built-in. There’s no need for manual password changes.
- Fine-Grained Auditing: Every secret checkout is a discrete event that can be audited.
Getting Secrets into Pods: The Vault Agent Injector
So, Vault is amazing. But a crucial question remains: how does an application running in a Kubernetes Pod get a secret from Vault? You don’t want to bundle a Vault token with your application image. That would just be trading one secret for another.
This is where the Vault Agent Injector comes in. It’s a Kubernetes admission controller that intercepts Pod creation events and, based on a set of annotations, automatically injects a Vault Agent container into the Pod. This agent is a lightweight client that authenticates with Vault and manages the lifecycle of secrets for the application.
Here’s the workflow:
-
Annotate your Deployment: You add specific annotations to your Kubernetes
Deployment,StatefulSet, orPodmanifest. These annotations tell the injector to act on this Pod, which Vault role to use, and which secrets to retrieve.annotations:vault.hashicorp.com/agent-inject: 'true'vault.hashicorp.com/role-name: 'myapp-role'vault.hashicorp.com/agent-inject-secret-database-config.json: 'database/creds/myapp-role' -
Pod Creation is Intercepted: When you
kubectl applyyour manifest, the Vault Agent Injector (which is aMutatingAdmissionWebhook) intercepts the request to the Kubernetes API server. -
The Pod is Mutated: The injector modifies the Pod specification, adding two containers:
- An
initcontainer that authenticates with Vault using the Pod’s Service Account Token and fetches the initial set of secrets. - A
sidecarcontainer that runs alongside your application. This sidecar keeps the secrets fresh (renewing leases) and can even re-fetch them if they change.
- An
-
Secrets are Shared: The agent containers share a memory volume with your application container. The secrets are written to files on this volume.
-
Application Consumes Secrets: Your application simply reads the secrets from the well-known file path (e.g.,
/vault/secrets/database-config.json). It doesn’t need any Vault-specific logic. The application remains completely unaware of Vault’s existence.
This elegant pattern decouples your application from the secret management system. Your developers can focus on writing code, not on integrating with Vault. The platform team, led by Alex, can manage the entire secret lifecycle centrally and securely.
The GitOps Conundrum: Sealed Secrets
Vault is a powerful solution for centralized secret management and dynamic credentials. But it doesn’t directly solve the original problem that triggered Alex’s security incident: how to safely store Kubernetes Secret manifests in a Git repository.
This is the exact problem that Sealed Secrets was created to solve. It’s a beautifully simple and effective tool for “one-way” encryption of secrets.
The Core Idea: Encrypt for the Cluster
Sealed Secrets works on a simple but powerful premise: anyone can encrypt a secret, but only the controller running in the target cluster can decrypt it.
It consists of two components:
-
A Controller: This runs in your Kubernetes cluster. On startup, it generates a public/private key pair. The private key stays securely within the controller in the cluster, while the public key is made available to developers.
-
A CLI tool (
kubeseal): Developers use this tool, along with the controller’s public key, to encrypt a standard KubernetesSecretinto a new custom resource called aSealedSecret.
Here’s the workflow:
-
Author a standard
Secret: A developer starts by creating a regular KubernetesSecretmanifest on their local machine. This file contains the sensitive data in the usual Base64-encoded format.Terminal window # Create a secret locallykubectl create secret generic my-db-password --from-literal=password='S3cr3tP@ssw0rd!' --dry-run=client -o yaml > my-secret.yaml -
Seal it with
kubeseal: The developer then pipes this localSecretmanifest into thekubesealcommand.kubesealfetches the public key from the controller running in the cluster and uses it to encrypt thedatafields of the secret.Terminal window # Seal the secretcat my-secret.yaml | kubeseal > sealed-my-secret.yaml -
Commit the
SealedSecret: The output is aSealedSecretmanifest. This file is safe to commit to Git. The sensitive data is now encrypted with the public key, and the originalSecretfile can be discarded.apiVersion: bitnami.com/v1alpha1kind: SealedSecretmetadata:name: my-db-passwordnamespace: defaultspec:encryptedData:password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq... # Encrypted value -
Deploy with GitOps: Your GitOps tool (like ArgoCD or Flux) picks up the
SealedSecretfrom the repository and applies it to the cluster. -
The Controller Unseals: The Sealed Secrets controller, running in the cluster, is the only entity with the private key. It detects the new
SealedSecretresource, decrypts theencryptedData, and creates a standard KubernetesSecretin the cluster.
This workflow is perfect for GitOps. The source of truth in Git contains no plaintext secrets, yet the final, necessary Secret is available to your applications in the cluster. It solves the “secret in Git” problem elegantly and securely.
The Bridge to a Wider World: External Secrets Operator
Both Vault and Sealed Secrets are powerful, but they represent two distinct philosophies. Vault is a full-blown, centralized secret management system. Sealed Secrets is a focused tool for a specific GitOps workflow. What if you already have secrets in an external provider, like AWS Secrets Manager, Google Secret Manager, or Azure Key Vault, and you just want to sync them into your Kubernetes cluster?
This is where the External Secrets Operator (ESO) shines. It acts as a bridge, synchronizing secrets from an external source into Kubernetes Secret objects.
How It Works: A Syncing Mechanism
The External Secrets Operator extends the Kubernetes API with its own set of Custom Resource Definitions (CRDs), primarily:
SecretStore: This resource defines how to connect to an external secret management system. It contains the connection details and authentication credentials for a specific provider (e.g., AWS region, Vault address).ExternalSecret: This resource specifies which secrets to fetch from aSecretStoreand how to transform them into a KubernetesSecret.
The workflow is straightforward:
-
Define a
SecretStore: You create aSecretStore(orClusterSecretStorefor a cluster-wide scope) that points to your external provider.apiVersion: external-secrets.io/v1beta1kind: SecretStoremetadata:name: aws-secrets-managerspec:provider:aws:service: SecretsManagerregion: us-east-1auth:jwt:serviceAccountRef:name: my-app-sa -
Create an
ExternalSecret: You define anExternalSecretthat references theSecretStoreand specifies the remote secret to fetch.apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata:name: my-db-credentialsspec:refreshInterval: "1h"secretStoreRef:name: aws-secrets-managerkind: SecretStoretarget:name: my-db-credentials-secret # Name of the K8s Secret to createdata:- secretKey: usernameremoteRef:key: my-app/db/credentialsproperty: username- secretKey: passwordremoteRef:key: my-app/db/credentialsproperty: password -
The Operator Syncs: The External Secrets Operator detects the
ExternalSecretresource. It uses the information in the referencedSecretStoreto connect to the external provider, fetches the specified secret (my-app/db/credentialsin this case), and creates (or updates) a native KubernetesSecretnamedmy-db-credentials-secretwith the retrieved data. -
Automatic Refresh: The operator will periodically re-fetch the secret based on the
refreshInterval, ensuring that the KubernetesSecretstays in sync with the external source.
ESO is the perfect tool when you have an established secret management system outside of Kubernetes and need a simple, reliable way to get those secrets into your cluster for your applications to consume.
Comparing the Approaches: Vault vs. Sealed Secrets vs. External Secrets
Alex now had three powerful tools in his arsenal. But which one was right for which job? He created a table to clarify their strengths and use cases.
| Feature | HashiCorp Vault | Sealed Secrets | External Secrets Operator |
|---|---|---|---|
| Primary Use Case | Centralized, dynamic secret management for the entire enterprise. | Safely storing Kubernetes Secret manifests in Git for GitOps workflows. | Syncing secrets from an existing external provider into Kubernetes. |
| Secret Storage | Stores secrets in its own encrypted backend (S3, Consul, etc.). | The encrypted SealedSecret is stored in Git. The plaintext Secret is in the cluster. | Secrets are stored in the external provider (AWS Secrets Manager, etc.). |
| Dynamic Secrets | Yes, this is a core feature. Can generate short-lived credentials on the fly. | No. It only encrypts and decrypts static secrets. | No. It syncs existing secrets; it doesn’t generate them. |
| GitOps Friendly | Can be, using the Vault Agent Injector and Kubernetes auth, but requires careful setup. | Yes, this is its entire purpose. | Yes, the ExternalSecret and SecretStore manifests are declarative and live in Git. |
| Complexity | High. It’s a powerful but complex system to deploy and manage. | Low. Very simple to set up and use. | Medium. Requires configuring the operator and the SecretStore for your provider. |
| Best For… | Organizations needing a single source of truth for all secrets, strong auditing, and dynamic credentials. | Teams practicing GitOps who need a simple way to handle Secret manifests. | Teams who already use a cloud provider’s secret manager and need to integrate it with Kubernetes. |
Alex realized they didn’t have to choose just one. They could use them together:
- Vault as the central, authoritative source of truth for all critical secrets and for generating dynamic database credentials.
- External Secrets Operator to sync secrets from Vault into their Kubernetes clusters.
- Sealed Secrets for the few, simple secrets that were managed directly by development teams and needed to be stored in Git for specific applications.
This hybrid approach offered the best of all worlds: the power and security of Vault, the GitOps-native workflow of Sealed Secrets, and the bridging capability of the External Secrets Operator.
Hands-On: Building a Secure Secrets Workflow
Theory is great, but there’s no substitute for getting your hands dirty. In this section, we’ll walk through a complete, end-to-end example of setting up Vault and Sealed Secrets on a local minikube cluster. Follow along to see how these pieces fit together in a real-world scenario.
Prerequisites
Before we begin, make sure you have the following tools installed on your macOS machine:
Start your minikube cluster:
minikube start --driver=dockerStep 1: Deploy HashiCorp Vault
We’ll use the official HashiCorp Helm chart to deploy Vault. This will set up Vault in standalone mode, which is perfect for our local testing.
-
Add the HashiCorp Helm repository:
Terminal window helm repo add hashicorp https://helm.releases.hashicorp.comhelm repo update -
Install the Vault chart:
We’ll install Vault with a few custom values. We’ll enable the UI and set the service type to
NodePortso we can access it from our local machine.Terminal window helm install vault hashicorp/vault --set "server.dev.enabled=true" --set "ui.enabled=true" --set "server.service.type=NodePort"This will deploy Vault in “dev” mode, which is an in-memory, pre-unsealed, single-node instance. This is great for development but should never be used in production.
-
Access the Vault UI:
Once the Vault pod is running, you can access the UI. First, get the
NodePortof the Vault service:Terminal window export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services vault-ui)export NODE_IP=$(minikube ip)echo "http://$NODE_IP:$NODE_PORT"Open the printed URL in your browser. You should see the Vault UI. The root token for dev mode is
root.
Step 2: Store and Retrieve a Secret with Vault Agent
Now that Vault is running, let’s configure it to work with Kubernetes and inject a secret into a sample application.
-
Exec into the Vault pod:
Terminal window kubectl exec -it vault-0 -- /bin/sh -
Enable Kubernetes authentication:
Inside the Vault pod’s shell, run the following commands. This tells Vault to trust the Kubernetes cluster’s service account tokens for authentication.
Terminal window # Enable the Kubernetes auth methodvault auth enable kubernetes# Configure the Kubernetes auth methodvault write auth/kubernetes/config \token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \kubernetes_host="https://kubernetes.default.svc" \kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt -
Create a policy and a role:
We need to create a policy that grants read access to a specific secret path, and a role that binds this policy to a Kubernetes service account.
Terminal window # Create a policy that allows reading a secretvault policy write myapp-policy - <<EOFpath "secret/data/myapp/config" {capabilities = ["read"]}EOF# Create a role that binds the policy to a service accountvault write auth/kubernetes/role/myapp-role \bound_service_account_names=myapp-sa \bound_service_account_namespaces=default \policies=myapp-policy \ttl=24h -
Create a secret:
Let’s put a secret in Vault for our application to consume.
Terminal window vault kv put secret/myapp/config username="my-app-user" password="my-app-password"Exit the Vault pod’s shell by typing
exit. -
Deploy the application:
Now, we’ll deploy a simple application with the necessary annotations for the Vault Agent Injector. We’ll also create the
ServiceAccountthat we bound our Vault role to.Create a file named
myapp.yaml:apiVersion: v1kind: ServiceAccountmetadata:name: myapp-sa---apiVersion: apps/v1kind: Deploymentmetadata:name: myappspec:replicas: 1selector:matchLabels:app: myapptemplate:metadata:labels:app: myappannotations:vault.hashicorp.com/agent-inject: "true"vault.hashicorp.com/role-name: "myapp-role"vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"spec:serviceAccountName: myapp-sacontainers:- name: myappimage: busyboxcommand: ["sh", "-c", "while true; do sleep 5; done"]Apply the manifest:
Terminal window kubectl apply -f myapp.yaml -
Verify the secret injection:
Once the
myapppod is running, exec into it and check for the secret.Terminal window kubectl exec -it $(kubectl get pods -l app=myapp -o jsonpath='{.items[0].metadata.name}') -- shInside the pod, you’ll find the secret in
/vault/secrets/config:Terminal window cat /vault/secrets/configYou should see the username and password we stored in Vault!
{"data": {"password": "my-app-password","username": "my-app-user"},"metadata": {...}}
Step 3: Set Up Sealed Secrets for GitOps
Now, let’s tackle the GitOps workflow with Sealed Secrets.
-
Install the Sealed Secrets controller:
We’ll use the official Helm chart for this as well.
Terminal window helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secretshelm repo updatehelm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system -
Install the
kubesealCLI:On macOS, you can use Homebrew:
Terminal window brew install kubeseal -
Create a secret and seal it:
Let’s create a new secret and seal it.
Terminal window # Create a secret locallykubectl create secret generic my-sealed-secret --from-literal=super-secret=so-secret --dry-run=client -o yaml > my-secret.yaml# Seal the secretcat my-secret.yaml | kubeseal > my-sealed-secret.yamlTake a look at
my-sealed-secret.yaml. Thesuper-secretvalue is now encrypted. -
Deploy the sealed secret:
Apply the
SealedSecretmanifest to your cluster:Terminal window kubectl apply -f my-sealed-secret.yaml -
Verify the unsealing:
The Sealed Secrets controller will decrypt the
SealedSecretand create a regular KubernetesSecret. Check for it:Terminal window kubectl get secret my-sealed-secret -o yamlYou’ll see the
datafield contains the Base64-encoded version of our original secret. You can decode it to verify:Terminal window echo 'c28tc2VjcmV0' | base64 --decodeIt should print
so-secret.
Debugging and Troubleshooting
- Vault Agent Injector not working: Check the logs of the
vault-agent-injectorpod. It’s usually a problem with the webhook configuration or RBAC permissions. - Sealed Secret not unsealing: Check the logs of the
sealed-secrets-controllerpod in thekube-systemnamespace. It might be a permissions issue or a problem with the key. kubesealcan’t fetch the certificate: Make sure yourkubectlis configured correctly and you have access to the cluster. You can also fetch the certificate manually and use the--certflag.
Key Takeaways
- Kubernetes Secrets alone are not enough for a secure, scalable secret management solution.
- HashiCorp Vault provides a centralized, secure, and auditable way to manage secrets, with powerful features like dynamic secrets.
- The Vault Agent Injector is an elegant way to get secrets from Vault into your Kubernetes pods without modifying your application code.
- Sealed Secrets is a simple and effective tool for managing secrets in a GitOps workflow, allowing you to store encrypted secrets safely in your Git repositories.
- A hybrid approach, using Vault as the central source of truth and Sealed Secrets for GitOps workflows, can provide a robust and flexible secret management strategy.
The Path Forward
Alex leaned back in his chair, a sense of accomplishment washing over him. The incident had been a wake-up call, but it had also been a catalyst for building a much more secure and robust platform. He had a clear path forward for managing secrets at NovaCraft, one that combined the strengths of Vault, Sealed Secrets, and the External Secrets Operator.
But as he closed his laptop for the day, a new thought crept into his mind. Now that the platform was more secure, how could he make it more observable? How could he get deep insights into the performance of the applications running on the cluster? His next challenge was already taking shape: a deep dive into the world of observability with Prometheus and Grafana.
This is Part 5 of Season 2 of The Container Odyssey.