Learn to implement Sealed Secrets controller for encrypting Kubernetes secrets in Git repositories, enabling secure GitOps workflows with encrypted Helm values and automated secret management.
Prerequisites
- Kubernetes cluster with admin access
- Basic understanding of Kubernetes secrets
- Git repository for GitOps workflows
What this solves
Kubernetes secrets contain sensitive data like passwords, API keys, and certificates, but storing them in Git repositories poses security risks. Sealed Secrets encrypts your secrets using asymmetric cryptography, allowing you to safely commit encrypted secrets to Git while the controller automatically decrypts them in your cluster. This enables secure GitOps workflows where all configuration, including sensitive data, lives in version control without compromising security.
Step-by-step installation
Install kubectl and verify cluster access
Ensure you have kubectl installed and configured to access your Kubernetes cluster before proceeding with Sealed Secrets installation.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
kubectl cluster-info
kubectl get nodes
Install Helm package manager
Sealed Secrets controller can be installed via Helm charts, which simplifies deployment and configuration management.
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
helm version
Add Sealed Secrets Helm repository
Add the official Sealed Secrets Helm repository and update the repository index to get the latest chart versions.
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm repo update
helm search repo sealed-secrets
Create dedicated namespace for Sealed Secrets
Deploy the Sealed Secrets controller in its own namespace to isolate it from application workloads and simplify RBAC management.
kubectl create namespace sealed-secrets-system
kubectl get namespaces
Install Sealed Secrets controller with Helm
Deploy the controller with recommended security settings including resource limits and security contexts.
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace sealed-secrets-system \
--set-string fullnameOverride=sealed-secrets-controller \
--set resources.limits.cpu=1000m \
--set resources.limits.memory=1Gi \
--set resources.requests.cpu=100m \
--set resources.requests.memory=128Mi
Verify controller deployment
Check that the Sealed Secrets controller pod is running and the service is accessible within the cluster.
kubectl get pods -n sealed-secrets-system
kubectl get svc -n sealed-secrets-system
kubectl logs -n sealed-secrets-system deployment/sealed-secrets-controller
Install kubeseal CLI tool
The kubeseal client encrypts your secrets locally before committing them to Git repositories.
KUBESEAL_VERSION='0.24.0'
wget "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz"
tar -xvzf kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz kubeseal
sudo install -o root -g root -m 0755 kubeseal /usr/local/bin/kubeseal
kubeseal --version
Fetch controller public key
Retrieve the public key from the controller to encrypt secrets locally. This key is automatically generated when the controller starts.
kubeseal --fetch-cert --controller-name=sealed-secrets-controller --controller-namespace=sealed-secrets-system > mykey.pem
cat mykey.pem
Configure encrypted secrets for applications
Create a test application namespace
Create a namespace where you'll deploy applications that use encrypted secrets.
kubectl create namespace myapp
kubectl get namespaces
Create a regular Kubernetes secret
Start by creating a standard Kubernetes secret that contains your sensitive data. This will be encrypted using kubeseal.
kubectl create secret generic database-credentials \
--from-literal=username=admin \
--from-literal=password=super-secret-password \
--from-literal=host=postgresql.example.com \
--namespace=myapp \
--dry-run=client -o yaml > database-secret.yaml
cat database-secret.yaml
Encrypt the secret with kubeseal
Transform the regular secret into a SealedSecret that can be safely stored in Git repositories.
kubeseal --format=yaml --cert=mykey.pem < database-secret.yaml > database-sealed-secret.yaml
cat database-sealed-secret.yaml
Apply the encrypted secret
Deploy the SealedSecret to your cluster. The controller will automatically decrypt it and create the corresponding Secret.
kubectl apply -f database-sealed-secret.yaml
kubectl get sealedsecrets -n myapp
kubectl get secrets -n myapp
Verify secret decryption
Check that the controller successfully decrypted the SealedSecret and created the regular Kubernetes Secret.
kubectl describe secret database-credentials -n myapp
kubectl get secret database-credentials -n myapp -o jsonpath='{.data.username}' | base64 -d
echo
kubectl get secret database-credentials -n myapp -o jsonpath='{.data.password}' | base64 -d
Integrate with Helm charts
Create Helm chart with sealed secrets
Set up a sample Helm chart that uses SealedSecrets for managing sensitive configuration values.
helm create myapp-chart
cd myapp-chart
Configure Helm templates for SealedSecrets
Create a template that generates SealedSecret resources from Helm values.
{{- if .Values.sealedSecrets.enabled }}
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: {{ include "myapp-chart.fullname" . }}-secrets
namespace: {{ .Release.Namespace }}
spec:
encryptedData:
{{- range $key, $value := .Values.sealedSecrets.encryptedData }}
{{ $key }}: {{ $value }}
{{- end }}
template:
metadata:
name: {{ include "myapp-chart.fullname" . }}-secrets
namespace: {{ .Release.Namespace }}
{{- end }}
Update Helm values with encrypted data
Configure the values file to include encrypted secret data that was generated with kubeseal.
sealedSecrets:
enabled: true
encryptedData:
database-url: AgBy3i4OJSWK+PiTySYZZA9rO5QtQyqQ7MAYRNxgqhXNr...
api-key: AgBybHRQF2XP4QnZBJYA8hW6N0iyOuOG2JXyB7QaT8Tc9...
Deploy application with encrypted secrets
Install the Helm chart which will create the SealedSecret and automatically decrypt it.
helm install myapp . --namespace myapp
kubectl get sealedsecrets -n myapp
kubectl get secrets -n myapp
Automate GitOps workflows
Set up Git repository structure
Organize your GitOps repository to separate plain configuration from encrypted secrets.
mkdir -p gitops-repo/{applications,sealed-secrets,helm-charts}
cd gitops-repo
Create automation script for secret encryption
Build a script that automates the process of encrypting secrets and updating SealedSecret manifests.
#!/bin/bash
set -e
NAMESPACE=${1:-default}
SECRET_NAME=${2:-app-secrets}
CERT_FILE=${3:-controller.pem}
if [ ! -f "$CERT_FILE" ]; then
echo "Fetching controller certificate..."
kubeseal --fetch-cert --controller-name=sealed-secrets-controller \
--controller-namespace=sealed-secrets-system > "$CERT_FILE"
fi
echo "Creating temporary secret..."
kubectl create secret generic "$SECRET_NAME" \
--from-env-file=secrets.env \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml > temp-secret.yaml
echo "Encrypting secret..."
kubeseal --format=yaml --cert="$CERT_FILE" < temp-secret.yaml > "sealed-secrets/${NAMESPACE}-${SECRET_NAME}.yaml"
echo "Cleaning up..."
rm temp-secret.yaml
echo "SealedSecret created: sealed-secrets/${NAMESPACE}-${SECRET_NAME}.yaml"
chmod +x scripts/encrypt-secrets.sh
Create environment-specific sealed secrets
Generate encrypted secrets for different environments while maintaining the same workflow.
DATABASE_URL=postgresql://admin:password@postgres.production.example.com:5432/myapp
API_SECRET_KEY=prod-secret-key-here
REDIS_PASSWORD=redis-production-password
./scripts/encrypt-secrets.sh production app-config
ls -la sealed-secrets/
Configure CI/CD pipeline integration
Set up a CI/CD workflow that automatically encrypts secrets and updates your GitOps repository.
name: Update Sealed Secrets
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
encrypt-secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubeseal
run: |
KUBESEAL_VERSION='0.24.0'
wget "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz"
tar -xvzf kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz kubeseal
sudo install -o root -g root -m 0755 kubeseal /usr/local/bin/kubeseal
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
- name: Update sealed secrets
run: |
echo "${{ secrets[format('{0}_SECRETS', github.event.inputs.environment)] }}" > secrets.env
./scripts/encrypt-secrets.sh ${{ github.event.inputs.environment }} app-config
- name: Commit changes
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git add sealed-secrets/
git commit -m "Update sealed secrets for ${{ github.event.inputs.environment }}"
git push
Security and key management
Backup controller private key
Export the controller's private key for disaster recovery purposes. Store this key securely outside the cluster.
kubectl get secret sealed-secrets-key -n sealed-secrets-system -o yaml > sealed-secrets-master-key.yaml
Configure key rotation policy
Set up automated key rotation by configuring the controller to generate new keys periodically.
keyRotation:
enabled: true
schedule: "0 0 1 " # Monthly rotation
retainOldKeys: 3
helm upgrade sealed-secrets sealed-secrets/sealed-secrets \
--namespace sealed-secrets-system \
--values key-rotation-values.yaml
Monitor sealed secrets operations
Set up monitoring and alerting for sealed secrets controller operations and failures.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: sealed-secrets-controller
namespace: sealed-secrets-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: sealed-secrets
endpoints:
- port: http
interval: 30s
path: /metrics
kubectl apply -f monitoring.yaml
Verify your setup
# Check controller status
kubectl get pods -n sealed-secrets-system
kubectl get svc -n sealed-secrets-system
Verify kubeseal connectivity
kubeseal --version
kubeseal --fetch-cert --controller-name=sealed-secrets-controller --controller-namespace=sealed-secrets-system
Check sealed secrets in applications
kubectl get sealedsecrets --all-namespaces
kubectl get secrets --all-namespaces | grep -v default-token
Test secret decryption
echo "test-value" | kubectl create secret generic test-secret --dry-run=client --from-file=/dev/stdin -o yaml | kubeseal --format=yaml --cert=mykey.pem | kubectl apply -f -
kubectl get secret test-secret -o jsonpath='{.data.stdin}' | base64 -d
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| kubeseal: cannot fetch certificate | Controller not running or wrong namespace | kubectl get pods -n sealed-secrets-system and verify controller name |
| SealedSecret stuck in pending | Controller can't decrypt or wrong scope | Check kubectl logs -n sealed-secrets-system deployment/sealed-secrets-controller |
| Secret not created after applying SealedSecret | RBAC issues or controller errors | Verify controller has permissions: kubectl auth can-i create secrets --as=system:serviceaccount:sealed-secrets-system:sealed-secrets-controller |
| Encryption fails with certificate error | Using wrong public key or expired certificate | Re-fetch certificate: kubeseal --fetch-cert > mykey.pem |
| Helm chart deployment fails | SealedSecret CRD not installed | Check CRDs: kubectl get crd sealedsecrets.bitnami.com |
Next steps
- Deploy applications to Kubernetes with Helm charts and production best practices
- Integrate HashiCorp Vault with Kubernetes secrets management for secure container orchestration
- Configure Kubernetes network policies with Calico CNI for microsegmentation and security enforcement
- Set up External Secrets Operator for dynamic secret management with cloud providers
- Implement Kubernetes Pod Security Standards and admission controllers for policy enforcement
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
KUBESEAL_VERSION="${KUBESEAL_VERSION:-0.24.0}"
NAMESPACE="${NAMESPACE:-sealed-secrets-system}"
# Cleanup function for rollback on error
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
kubectl delete namespace "$NAMESPACE" --ignore-not-found=true 2>/dev/null || true
helm repo remove sealed-secrets 2>/dev/null || true
rm -f kubectl kubeseal get_helm.sh kubeseal-*.tar.gz 2>/dev/null || true
}
trap cleanup ERR
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Install Sealed Secrets for Kubernetes"
echo ""
echo "Options:"
echo " -n, --namespace NAMESPACE Namespace for Sealed Secrets (default: sealed-secrets-system)"
echo " -v, --version VERSION kubeseal version (default: 0.24.0)"
echo " -h, --help Show this help message"
echo ""
echo "Environment variables:"
echo " KUBESEAL_VERSION Override kubeseal version"
echo " NAMESPACE Override namespace"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-v|--version)
KUBESEAL_VERSION="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
;;
esac
done
echo -e "${BLUE}Sealed Secrets Installation Script${NC}"
echo -e "${BLUE}===================================${NC}"
# Detect distribution
echo -e "${YELLOW}[1/9] Detecting Linux distribution...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect Linux distribution${NC}"
exit 1
fi
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
# Check prerequisites
echo -e "${YELLOW}[2/9] Checking prerequisites...${NC}"
if [[ $EUID -ne 0 ]]; then
if ! command -v sudo &> /dev/null; then
echo -e "${RED}This script requires root privileges or sudo${NC}"
exit 1
fi
SUDO="sudo"
else
SUDO=""
fi
# Update package manager
echo -e "${YELLOW}[3/9] Updating package manager...${NC}"
$SUDO $PKG_UPDATE
# Install dependencies
echo -e "${YELLOW}Installing dependencies...${NC}"
case "$PKG_MGR" in
apt)
$SUDO $PKG_INSTALL curl wget gpg apt-transport-https ca-certificates
;;
dnf|yum)
$SUDO $PKG_INSTALL curl wget tar gzip
;;
esac
# Install kubectl
echo -e "${YELLOW}[4/9] Installing kubectl...${NC}"
if ! command -v kubectl &> /dev/null; then
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod 755 kubectl
$SUDO install -o root -g root -m 755 kubectl /usr/local/bin/kubectl
rm kubectl
echo -e "${GREEN}kubectl installed successfully${NC}"
else
echo -e "${GREEN}kubectl already installed${NC}"
fi
# Verify cluster access
echo -e "${YELLOW}Verifying cluster access...${NC}"
if ! kubectl cluster-info &> /dev/null; then
echo -e "${RED}Cannot access Kubernetes cluster. Please configure kubectl first.${NC}"
exit 1
fi
echo -e "${GREEN}Kubernetes cluster access verified${NC}"
# Install Helm
echo -e "${YELLOW}[5/9] Installing Helm...${NC}"
if ! command -v helm &> /dev/null; then
case "$PKG_MGR" in
apt)
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | $SUDO tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | $SUDO tee /etc/apt/sources.list.d/helm-stable-debian.list
$SUDO apt update
$SUDO $PKG_INSTALL helm
;;
dnf|yum)
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
rm get_helm.sh
;;
esac
echo -e "${GREEN}Helm installed successfully${NC}"
else
echo -e "${GREEN}Helm already installed${NC}"
fi
# Add Sealed Secrets Helm repository
echo -e "${YELLOW}[6/9] Adding Sealed Secrets Helm repository...${NC}"
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm repo update
echo -e "${GREEN}Helm repository added and updated${NC}"
# Create namespace
echo -e "${YELLOW}[7/9] Creating namespace: $NAMESPACE${NC}"
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
echo -e "${GREEN}Namespace created: $NAMESPACE${NC}"
# Install Sealed Secrets controller
echo -e "${YELLOW}[8/9] Installing Sealed Secrets controller...${NC}"
helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets \
--namespace "$NAMESPACE" \
--set-string fullnameOverride=sealed-secrets-controller \
--set resources.limits.cpu=1000m \
--set resources.limits.memory=1Gi \
--set resources.requests.cpu=100m \
--set resources.requests.memory=128Mi \
--wait --timeout=300s
echo -e "${GREEN}Sealed Secrets controller installed${NC}"
# Install kubeseal CLI
echo -e "${YELLOW}[9/9] Installing kubeseal CLI...${NC}"
if ! command -v kubeseal &> /dev/null; then
wget "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz"
tar -xzf "kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz" kubeseal
chmod 755 kubeseal
$SUDO install -o root -g root -m 755 kubeseal /usr/local/bin/kubeseal
rm kubeseal "kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz"
echo -e "${GREEN}kubeseal CLI installed successfully${NC}"
else
echo -e "${GREEN}kubeseal CLI already installed${NC}"
fi
# Verification
echo -e "${YELLOW}Verifying installation...${NC}"
echo -e "${BLUE}Checking controller deployment:${NC}"
kubectl get pods -n "$NAMESPACE"
kubectl get svc -n "$NAMESPACE"
# Wait for controller to be ready
echo -e "${YELLOW}Waiting for controller to be ready...${NC}"
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=sealed-secrets -n "$NAMESPACE" --timeout=120s
# Test kubeseal connectivity
echo -e "${YELLOW}Testing kubeseal connectivity...${NC}"
if kubeseal --fetch-cert > /dev/null; then
echo -e "${GREEN}kubeseal can communicate with the controller${NC}"
else
echo -e "${RED}Warning: kubeseal cannot fetch certificate from controller${NC}"
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${BLUE}Next steps:${NC}"
echo "1. Create a secret: kubectl create secret generic mysecret --from-literal=password=mypassword --dry-run=client -o yaml"
echo "2. Seal the secret: kubectl create secret generic mysecret --from-literal=password=mypassword --dry-run=client -o yaml | kubeseal -o yaml > mysealedsecret.yaml"
echo "3. Apply the sealed secret: kubectl apply -f mysealedsecret.yaml"
Review the script before running. Execute with: bash install.sh