External Secrets¶
Secrets never live in Git. The External Secrets Operator (ESO) reads values from GCP Secret Manager and AWS Secrets Manager and projects them into Kubernetes Secret objects that pods envFrom at runtime.
Installed by k8s/envs/dev/sys/external-secrets/application.yaml. The cluster-scoped store definitions live in k8s/envs/dev/sys/manifests/external-secrets-config.yaml and are deployed by the separate sys-resources Application.
Chart and source¶
| Field | Value |
|---|---|
| Helm chart | external-secrets |
| Repository | https://charts.external-secrets.io |
| Version | 0.10.3 |
| Destination namespace | external-secrets (created by Argo CD) |
| Argo CD Application | external-secrets |
The chart installs the ESO controller, the webhook, and the CRDs (ClusterSecretStore, SecretStore, ExternalSecret, PushSecret).
ClusterSecretStores¶
Two stores are registered cluster-wide. Both are ClusterSecretStore (not namespaced SecretStore) so any namespace can reference them.
gsm-tomoda — GCP Secret Manager¶
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: gsm-tomoda
spec:
provider:
gcpsm:
projectID: development-485000
No auth block — ESO authenticates via Workload Identity using the ESO controller's KSA, which is bound to a GCP SA with roles/secretmanager.secretAccessor on the project. That binding is set up in Terraform alongside the cluster (see Infrastructure → GCP).
This store backs every application secret in the cluster — JWT secret, OAuth client secret, Stripe keys, email API keys, Postgres password, Redis password. The full inventory is in Secrets Management.
aws-sm-tomoda — AWS Secrets Manager¶
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-sm-tomoda
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-eso-credentials
namespace: external-secrets
key: access-key
secretAccessKeySecretRef:
name: aws-eso-credentials
namespace: external-secrets
key: secret-access-key
Unlike GCP, AWS has no Workload Identity equivalent for GKE pods, so this store authenticates with a static IAM user credential pair. The user is tomoda-eso-reader-{env} (provisioned by AWS Terraform), and the credentials are stored in the K8s Secret aws-eso-credentials in the external-secrets namespace.
That bootstrap secret is not in Git — it has to be created by hand after running the AWS Terraform module:
kubectl create secret generic aws-eso-credentials -n external-secrets \
--from-literal=access-key=<ESO_READER_ACCESS_KEY_ID> \
--from-literal=secret-access-key=<ESO_READER_SECRET_ACCESS_KEY>
This store is used for things that only AWS can produce — currently the S3 uploader's IAM credentials.
Per-app ExternalSecret CRs¶
Once the stores exist, any namespace can pull values:
# k8s/envs/dev/sys/manifests/external-secrets-config.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: google-oauth-secret
namespace: monitoring
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: gsm-tomoda
target:
name: google-oauth-credentials
creationPolicy: Owner
data:
- secretKey: client-secret
remoteRef:
key: oauth-client-secret
The result is a regular K8s Secret named google-oauth-credentials in the monitoring namespace, refreshed hourly from GCP SM. Grafana's Deployment then valueFrom: secretKeyRefs it as an environment variable.
The same pattern is used by the tomoda app's backend-secrets-{dev,prod} Secrets (under k8s/envs/{dev,prod}/middleware/tomoda/).
Refresh and rotation¶
- Refresh interval is per-ExternalSecret; the convention is
1h. ESO re-reads the upstream value and patches the K8s Secret if it changed. - Rotating a value upstream (in GCP SM or AWS SM) propagates into the cluster within the refresh window. Pods consuming the Secret via
envFromwill not pick up the new value until they restart — they only read env vars at process start. Forcing a rollout is the standard remedy:kubectl rollout restart deployment/<name>. - Force-refreshing an ExternalSecret immediately: annotate it with
force-syncor delete the target Secret (ESO recreates it on the next reconcile).
Operational notes¶
- Webhook dependency. The ESO controller's mutating webhook is required for
ExternalSecretadmission. If the controller is unhealthy, new ExternalSecrets cannot be created — existing K8s Secrets remain in place but stop refreshing. - The bootstrap secret is a single point of failure for AWS. If
aws-eso-credentialsis missing or its keys are wrong, every AWS-backed ExternalSecret fails. Existing K8s Secrets stay intact (creationPolicy: Owner means ESO won't delete on auth failure) but new ones can't be created. - No secret values are logged. ESO logs key names and store references only.
For the secret inventory, see Secrets Management.