Kubernetes Secrets and the External Secrets Operator
Create and consume secrets in Kubernetes on IronSled — kubectl create secret from literals/files/YAML, Opaque data vs stringData, mounting secrets as environment variables or files, image-pull secrets, rotating secrets and restarting pods, and using the External Secrets Operator (ESO) to sync secrets from AWS Secrets Manager into your namespace.
Secrets hold the sensitive values your application needs at runtime — database passwords, API keys, tokens, and certificates. On IronSled you have two ways to manage them: create Kubernetes Secret objects directly, or let the External Secrets Operator (ESO) sync them into your namespace from AWS Secrets Manager. This page covers both, plus how to consume a secret from a pod and how to rotate one.
Kubernetes Secrets are base64-encoded, not encrypted, in the object definition. Never commit a raw Secret manifest to Git. For anything sensitive in a shared or production environment, prefer the External Secrets Operator so the source of truth stays in AWS Secrets Manager.
Understanding Kubernetes Secrets
A Kubernetes Secret is an object that stores sensitive data as key-value pairs. Secret values are base64-encoded (not encrypted by default) and can be:
- Exposed to a container as environment variables
- Mounted into a container as files
- Used by the kubelet to pull images from a private registry
Secrets are namespaced — a Secret lives in one namespace and is only usable by pods in that same namespace.
Creating Secrets
From literal values
kubectl create secret generic my-secret \
--from-literal=username=admin \
--from-literal=password='S3cr3tP@ss!' \
-n my-namespace
# Verify
kubectl get secret my-secret -n my-namespaceFrom files
echo -n 'admin' > ./username.txt
echo -n 'S3cr3tP@ss!' > ./password.txt
kubectl create secret generic my-secret \
--from-file=username=./username.txt \
--from-file=password=./password.txt \
-n my-namespace
rm ./username.txt ./password.txtFrom a YAML manifest (base64-encoded)
Values in the data field must be base64-encoded:
echo -n 'admin' | base64 # YWRtaW4=
echo -n 'S3cr3tP@ss!' | base64 # UzNjcjN0UEBzcyE=apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: my-namespace
type: Opaque
data:
username: YWRtaW4=
password: UzNjcjN0UEBzcyE=Using stringData (no base64)
Use stringData to provide plain-text values that Kubernetes encodes for you:
apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: my-namespace
type: Opaque
stringData:
username: admin
password: S3cr3tP@ss!Secret Types
Docker registry (image pull)
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=password \
--docker-email=user@example.com \
-n my-namespaceTLS certificate
kubectl create secret tls my-tls-secret \
--cert=path/to/cert.pem \
--key=path/to/key.pem \
-n my-namespaceUsing Secrets in Pods
As environment variables
Map individual keys with secretKeyRef:
spec:
containers:
- name: my-container
image: my-image
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: my-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-secret
key: passwordOr import every key at once with envFrom:
spec:
containers:
- name: my-container
image: my-image
envFrom:
- secretRef:
name: my-secretAs a mounted volume
spec:
containers:
- name: my-container
image: my-image
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: my-secretFor image pull
spec:
containers:
- name: my-container
image: registry.example.com/my-image:tag
imagePullSecrets:
- name: regcredViewing and Managing Secrets
# List secrets
kubectl get secrets -n my-namespace
# Describe (shows metadata, not values)
kubectl describe secret my-secret -n my-namespace
# View the full object (values base64-encoded)
kubectl get secret my-secret -n my-namespace -o yaml
# Decode a specific key
kubectl get secret my-secret -n my-namespace \
-o jsonpath='{.data.password}' | base64 -d
# Delete a secret
kubectl delete secret my-secret -n my-namespaceRotating Secrets
Update a secret in place and roll the workload so pods pick up the new value:
# Update the secret
kubectl create secret generic my-secret \
--from-literal=password='NewP@ssword!' \
--dry-run=client -o yaml | kubectl apply -f -
# Restart pods to pick up the change
kubectl rollout restart deployment my-app -n my-namespaceSecrets consumed as environment variables do not update automatically — a pod restart is required. Secrets mounted as volumes update automatically after a short delay (unless mounted with subPath, which disables auto-updates).
External Secrets Operator (ESO)
IronSled clusters run the External Secrets Operator (ESO), which syncs secrets from AWS Secrets Manager into native Kubernetes Secret objects in your namespace. This keeps the source of truth in a dedicated, audited secret store while your application still reads an ordinary Kubernetes Secret. During onboarding, an AWS role scoped to your project is created so you can store secrets under a fixed naming convention:
<environment>/ironsled/applications/<project>/<secret>
# Example: dev/ironsled/applications/my-app/db-connectionCreate an ExternalSecret
Add an ExternalSecret to your Helm chart or manifest. It references the cluster's shared ClusterSecretStore and writes a Kubernetes Secret named by target.name. Use dataFrom.extract to pull every key from a JSON secret (typical for RDS credentials):
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-app
namespace: my-namespace
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-cluster-secret-store
kind: ClusterSecretStore
target:
name: my-app
creationPolicy: Owner
dataFrom:
- extract:
key: dev/ironsled/applications/my-app/db-connectionTo map specific keys instead of extracting everything, use data with a remoteRef per key:
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-cluster-secret-store
kind: ClusterSecretStore
target:
name: my-app
creationPolicy: Owner
data:
- secretKey: db-host
remoteRef:
key: dev/ironsled/applications/my-app/db-connection
property: host
- secretKey: db-password
remoteRef:
key: dev/ironsled/applications/my-app/db-connection
property: password
- secretKey: db-user
remoteRef:
key: dev/ironsled/applications/my-app/db-connection
property: usernameConsume the synced Secret
ESO creates a normal Kubernetes Secret (named by target.name), so your Deployment references it the same way as any other Secret:
spec:
containers:
- name: my-app
image: registry.example.com/my-app:latest
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: my-app
key: passworddata vs. dataFrom
datamaps individual remote keys (or properties within a JSON secret) to specific keys in the generated Kubernetes Secret. Each entry sets asecretKeyand aremoteRef(key, optionalproperty, optionalversion). Use it when you want explicit control over each key.dataFrompulls multiple values at once —extractcopies all key-value pairs from a JSON secret into the Kubernetes Secret;finddiscovers secrets by name pattern or path. Use it for bulk sync without listing each key.
Best Practices
- Never commit secrets to Git. Add secret manifests,
*.key, and*.pemfiles to.gitignore, and prefer ESO so plain-text values never live in the repo. - Restrict access with RBAC — grant
geton only the specific Secrets a workload needs, rather than blanket secret access in the namespace. - Rotate regularly, and remember env-var secrets require a
kubectl rollout restartto take effect. - Isolate by namespace — keep each application's secrets in its own namespace.
Troubleshooting
Secret not found
kubectl get secret my-secret -n my-namespace
kubectl get secrets -A | grep my-secretPod cannot access a secret
kubectl get pod my-pod -n my-namespace -o yaml | grep -A5 secretKeyRef
kubectl auth can-i get secrets -n my-namespace \
--as=system:serviceaccount:my-namespace:defaultSecret not updating in a pod — env-var secrets do not auto-update; restart the workload with kubectl rollout restart deployment my-app -n my-namespace. Volume-mounted secrets update automatically unless mounted with subPath.
ExternalSecret not syncing — check its status and events for the reason (missing remote key, store permissions, or a malformed remoteRef):
kubectl describe externalsecret my-app -n my-namespaceSet Up Git and GPG Commit Signing
Set up Git with GPG commit signing for IronSled GitLab — a platform requirement. Covers installing GnuPG on macOS/Windows, generating an RSA key pair, adding your public key to GitLab, configuring git to sign every commit, verifying the Verified badge, IDE integration, and troubleshooting gpg signing errors.
Author a Helm Chart for Your Application
Author a Helm chart to deploy your application on IronSled — scaffold a chart with helm create, configure Chart.yaml and values.yaml (image repository and tag, imagePullSecrets, securityContext, service, ingress, resources, autoscaling, liveness/readiness probes), point the chart at your uploaded image in the internal registry, install and upgrade with helm, verify the release, and roll back. External vendors must commit their own chart and README.