Kubernetes secrets
ocync’s Helm chart exposes four orthogonal patterns for getting registry credentials into the workload pod: envFrom, External Secrets Operator, CSI Secrets Store, and Workload Identity. Pick the one that matches how secrets are managed in your cluster; combinations are fine (e.g., Workload Identity for AWS plus envFrom for a Docker Hub PAT).
The chart values referenced below are documented inline in charts/ocync/values.yaml. Working CI fixtures live in charts/ocync/ci/.
envFrom
Simplest pattern. Create a Secret out-of-band and reference it via envFrom; the ocync config substitutes ${VAR_NAME} from environment variables.
# values.yaml
config:
registries:
hub:
url: docker.io
auth_type: basic
credentials:
username: ${DOCKER_USERNAME}
password: ${DOCKER_PASSWORD}
# ... targets ...
envFrom:
- secretRef:
name: ocync-credentials
kubectl create secret generic ocync-credentials \
--from-literal=DOCKER_USERNAME=youruser \
--from-literal=DOCKER_PASSWORD="$(cat ~/dockerhub-pat)"
This pattern composes with everything below: Workload Identity covers cloud auth, envFrom covers token-based registries in the same pod.
External Secrets Operator
When secrets live in AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, or a similar external store, External Secrets Operator (ESO) keeps a native Secret synchronized from the source of truth. The chart can render the ExternalSecret resource for you so the entire deployment is declarative:
# values.yaml
externalSecrets:
enabled: true
refreshInterval: "1h"
secretStoreRef:
name: aws-secret-store
kind: ClusterSecretStore
target:
name: ocync-credentials
creationPolicy: Owner
data:
- secretKey: DOCKER_USERNAME
remoteRef:
key: prod/ocync/docker-username
- secretKey: DOCKER_PASSWORD
remoteRef:
key: prod/ocync/docker-password
envFrom:
- secretRef:
name: ocync-credentials
The chart renders ExternalSecret (external-secrets.io/v1); ESO writes the ocync-credentials Secret; the workload consumes it via envFrom. ESO must already be installed in the cluster — the chart does not pre-check CRD presence and kubectl apply will surface a no matches for kind "ExternalSecret" error if it is missing.
For the underlying SecretStore / ClusterSecretStore (which provider, how it authenticates), see the ESO provider docs for your target store.
CSI Secrets Store
When you want secrets mounted as files (not env vars) and synced into a native Secret as a side effect, the Secrets Store CSI Driver is the standard pattern. The chart renders a SecretProviderClass (secrets-store.csi.x-k8s.io/v1) and you consume it via extraVolumes:
# values.yaml
secretProviderClass:
enabled: true
provider: aws # | azure | gcp | vault
parameters:
objects: |
- objectName: prod/ocync/docker-username
objectAlias: DOCKER_USERNAME
objectType: secretsmanager
- objectName: prod/ocync/docker-password
objectAlias: DOCKER_PASSWORD
objectType: secretsmanager
secretObjects:
- secretName: ocync-credentials
type: Opaque
data:
- objectName: DOCKER_USERNAME
key: DOCKER_USERNAME
- objectName: DOCKER_PASSWORD
key: DOCKER_PASSWORD
extraVolumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: ocync
extraVolumeMounts:
- name: secrets-store
mountPath: /mnt/secrets-store
readOnly: true
envFrom:
- secretRef:
name: ocync-credentials
Both the CSI driver and the cloud-specific provider plugin (AWS, Azure, GCP, or Vault) must be installed. As with ExternalSecrets, the chart does not pre-check CRD presence.
First-pod startup race. The CSI driver populates the synced Secret (the one referenced by envFrom) only after the volume has mounted. envFrom resolves at container start, so on a brand-new pod the Secret may not exist yet and Kubernetes surfaces CreateContainerConfigError. The pod recovers automatically once the driver finishes the first sync (typically within seconds). This is expected behavior of the CSI Secrets Store Driver, not a chart bug; if you need deterministic ordering, populate the Secret out-of-band (or via ExternalSecrets) and have the driver merely refresh it.
Workload Identity
For cloud registries (ECR, ECR Public, GAR, ACR), the cleanest path is no Secret at all — bind the workload to a cloud IAM identity and let the SDK resolve credentials on each pod. The chart’s workloadIdentity block sets the right ServiceAccount annotation (and pod label, for Azure) for your provider:
# values.yaml -- AWS IRSA
workloadIdentity:
provider: aws
aws:
roleArn: arn:aws:iam::123456789012:role/ocync-irsa
# values.yaml -- GKE Workload Identity
workloadIdentity:
provider: gcp
gcp:
serviceAccount: ocync@my-project.iam.gserviceaccount.com
# values.yaml -- Azure AD Workload Identity (AKS)
workloadIdentity:
provider: azure
azure:
clientId: 00000000-0000-0000-0000-000000000000
tenantId: 11111111-1111-1111-1111-111111111111 # optional
The Azure provider sets both the SA annotation (azure.workload.identity/client-id) AND the pod label (azure.workload.identity/use: "true"). Without the pod label, the AAD mutating webhook does not inject the projected SA token and the credential chain falls through to managed identity / Azure CLI.
EKS Pod Identity is not represented in workloadIdentity because it is configured cluster-side via PodIdentityAssociation (not via the pod spec). Set up the association out-of-band and link it to the chart’s ServiceAccount; no chart values are needed.
AWS shared-config files
Use this pattern when one ECR registry needs credentials distinct from the ambient chain (see ECR per-registry profile). The aws_profile config field reads from a credentials file mounted at the path in AWS_SHARED_CREDENTIALS_FILE; this section covers two production-grade ways to populate that file.
The recommended secret-store layout is to store the entire INI blob — including the [profile-name] header — as a single string value at one key. This keeps the chart values minimal and avoids per-field templating.
External Secrets Operator (AWS shared-config)
The third party’s credentials are mirrored into AWS Secrets Manager, Vault, Azure Key Vault, GCP Secret Manager, or another store. ESO syncs to a native Secret that the chart mounts as a file:
# values.yaml
externalSecrets:
enabled: true
refreshInterval: "1h"
secretStoreRef:
name: cluster-secret-store
kind: ClusterSecretStore
data:
- secretKey: credentials
remoteRef:
key: vendor/aws-creds # value: full INI blob, one string
extraVolumes:
- name: aws-creds
secret:
secretName: my-release-ocync # default ExternalSecret target
extraVolumeMounts:
- name: aws-creds
mountPath: /etc/aws
readOnly: true
env:
- name: AWS_SHARED_CREDENTIALS_FILE
value: /etc/aws/credentials
Rotation handled upstream; audit trail in the source store; nothing plaintext in your repos.
If your secret store holds the access key and secret key as separate fields, flatten them upstream into a single INI blob value. The chart does not currently expose ESO’s target.template: for in-cluster assembly; if you have a hard requirement to keep them separate at rest, open an issue describing the constraint.
CSI Secrets Store driver (AWS shared-config)
For clusters where policy mandates the CSI driver and forbids long-lived Secret resources. Same source-of-truth as the ESO path, but the driver mounts directly from the cloud secret store:
# values.yaml
secretProviderClass:
enabled: true
provider: aws # | azure | gcp | vault
parameters:
objects: |
- objectName: vendor/aws-creds
objectType: secretsmanager
objectAlias: credentials
extraVolumes:
- name: aws-creds
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: ocync # release name (matches the existing CSI section above)
extraVolumeMounts:
- name: aws-creds
mountPath: /etc/aws
readOnly: true
env:
- name: AWS_SHARED_CREDENTIALS_FILE
value: /etc/aws/credentials
The CSI race-on-startup behavior described in the CSI Secrets Store section above applies here too: the file may not exist for the first few seconds after a brand-new pod starts.
Combining patterns
A typical mixed deployment (mirror from Docker Hub to ECR using IRSA + envFrom):
# values.yaml
config:
registries:
hub:
url: docker.io
auth_type: basic
credentials:
username: ${DOCKER_USERNAME}
password: ${DOCKER_PASSWORD}
ecr: { url: 123456789012.dkr.ecr.us-east-1.amazonaws.com }
defaults:
source: hub
targets: ecr
mappings:
- from: library/nginx
to: nginx
# IRSA for ECR (no Secret needed)
workloadIdentity:
provider: aws
aws:
roleArn: arn:aws:iam::123456789012:role/ocync-irsa
# envFrom for Docker Hub PAT (created out-of-band or via ExternalSecrets)
envFrom:
- secretRef:
name: ocync-credentials
What the chart does not render
- Native
Secretresources. Encouraging secrets invalues.yamlis an anti-pattern; the chart consumes Secrets but never creates them. - SealedSecrets templates. SealedSecrets produces a normal
Secret, which is picked up byenvFromfrom the first pattern. - Vault Agent Injector annotations. Driven entirely by
podAnnotationsandserviceAccount.annotations; no chart-specific gating needed.