Как использовать Secrets в Pod: env, volume, imagePullSecrets, projected
API для подключения Secret к Pod почти зеркальный к ConfigMap — те же env, envFrom, volume. Но есть важные отличия: tmpfs storage, дополнительные механизмы (imagePullSecrets, ServiceAccount token mounting), и критический момент про exposure через env. Этот урок собирает всё в один полный сценарий.
Volume permissions: UID mismatch и chown в entrypoint
env: одно значение через secretKeyRef
Зеркально к configMapKeyRef:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:1.0
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db_password
- name: API_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: api_key
optional: false
Семантика та же: optional: false (default) — Pod не стартует, если Secret или ключ отсутствует. optional: true — переменная просто не будет установлена.
Главная проблема env-метода для Secret: значения видны в /proc/<pid>/environ (cat /proc/1/environ | tr '\0' '\n') и через ps eww <pid> команду от пользователя с правами читать /proc. Любой процесс в Pod (или sidecar-контейнер sharing PID namespace) увидит секреты. Это архитектурное ограничение Linux env vars — не специфично для Kubernetes. Альтернатива — volume mount, разбираем ниже.
envFrom: все ключи Secret как env vars
spec:
containers:
- name: app
image: myapp:1.0
envFrom:
- secretRef:
name: app-secrets
- secretRef:
name: third-party-creds
prefix: TP_
Каждый ключ Secret-а становится env-переменной с тем же именем. Опционально с префиксом. Имена должны быть валидными C_IDENTIFIER — иначе переменная пропускается с warning event.
Можно комбинировать envFrom для ConfigMap и Secret — Kubernetes сольёт всё в один environment блок:
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: app-secrets }
Удобно для apps в стиле Twelve-Factor App: одна команда — все нужные env-переменные.
Volume mount: tmpfs in-memory
Это самый безопасный способ, плюс — не подвержен exposure через env:
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: secrets
mountPath: /etc/app/secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: app-secrets
defaultMode: 0400
items:
- key: db_password
path: db_password
- key: api_key
path: api_key
mode: 0440
Каждый ключ Secret становится файлом в mountPath. Приложение читает файл когда нужно — секрет не висит в process environ.
Tmpfs, не диск
Файлы Secret в volume mount материализуются на tmpfs (in-memory filesystem) на node, а не на обычном диске. Это означает:
- Секреты не записываются на диск в plaintext
- При рестарте node секреты исчезают (Pod должен пересоздаться, и kubelet перематериализует)
- На overcommit node это тратит RAM (учитывается в kubelet memory tracking, но не в Pod limits)
Это даёт защиту: если кто-то снимет диск node с целью посмотреть секреты — они недоступны (диск не содержит plaintext данных).
ConfigMap-файлы тоже идут через tmpfs (хотя они не sensitive). Это унификация механизма — kubelet использует один и тот же volume plugin (secret/configMap).
Permissions: defaultMode
defaultMode: 0400 (rw для owner, ничего для других). По умолчанию 0644. Для Secret рекомендуется выставлять 0400 или 0440.
UID/GID файлов определяется spec.securityContext.fsGroup и runAsUser Pod-а.
items для cherry-pick
С items можно mount только нужные ключи, переименовать пути, выставить individual mode. Без items — все ключи.
imagePullSecrets: для private registries
Когда image хостится в private registry (Docker Hub приватный repo, ECR, GCR, Quay, Harbor), kubelet нужны credentials для pull. Эти credentials передаются через Secret типа kubernetes.io/dockerconfigjson и ссылка из Pod через imagePullSecrets:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/myteam/myapp:1.0
Создание Secret:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=robot \
--docker-password=$TOKEN
imagePullSecrets указывается на уровне Pod (spec.imagePullSecrets), не Container. Может быть список Secret-ов — kubelet пробует каждый.
Альтернатива — прописать imagePullSecrets в ServiceAccount. Тогда любой Pod, использующий этот SA, автоматически получит pull credentials:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
imagePullSecrets:
- name: regcred
Это удобнее, если все Pod в namespace тянут из одного private registry.
ServiceAccount tokens: projected volume
Каждый Pod имеет ассоциированный ServiceAccount (по умолчанию default в своём namespace). У ServiceAccount есть JWT-токен, который Pod использует для аутентификации в API server (например, чтобы делать kubectl get из Pod).
С Kubernetes v1.21+ механизм поменялся:
Старый (до v1.21): ServiceAccount controller создавал Secret типа kubernetes.io/service-account-token с long-lived JWT, и kubelet монтировал его в Pod по пути /var/run/secrets/kubernetes.io/serviceaccount/.
Новый (v1.21+, default с v1.24): токен не хранится в Secret. При запуске Pod kubelet делает запрос к TokenRequest API, получает короткоживущий (default 1 час) JWT, монтирует через projected volume. Каждые ~50 минут токен авто-ротируется.
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: kube-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: kube-api-access
projected:
sources:
- serviceAccountToken:
expirationSeconds: 3600
path: token
- configMap:
name: kube-root-ca.crt
items: [{ key: ca.crt, path: ca.crt }]
- downwardAPI:
items:
- path: namespace
fieldRef: { fieldPath: metadata.namespace }
Этот блок добавляется в Pod автоматически, если spec.automountServiceAccountToken не false (default true). Поэтому видеть это в обычных манифестах не приходится.
Что важно знать:
- Отключить mount через
automountServiceAccountToken: false(на ServiceAccount или Pod уровне). Это рекомендуется для Pod-ов, которые не общаются с API server (большинство application Pod-ов). - Кастомный токен для интеграции с external services — через
serviceAccountTokenв projected volume с audience (audience: vault). Это позволяет federated auth — Vault через OIDC проверяет JWT кубовый.
Best practice: для Pod-ов, которые не делают запросы к kube-apiserver, ставить automountServiceAccountToken: false. Это уменьшает blast radius при компрометации: даже если злоумышленник захватил контейнер, у него нет JWT для запросов к кубу.
External Secrets Operator (ESO)
В production обычно не хранят Secret-ы в Kubernetes напрямую — их хранят в специализированных Secret Manager: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault. У них есть audit log, ротация, fine-grained access, версионирование.
External Secrets Operator — open-source controller, который синхронизирует данные из этих store-ов в K8s Secret. В кластере остаётся обычный Secret, но source-of-truth — внешний store:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: app-secrets }
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
target: { name: app-secrets }
data:
- secretKey: db_password
remoteRef: { key: prod/app/db, property: password }
ESO регулярно (по refreshInterval) тянет данные из Vault и обновляет Secret. Pod подключается к Secret обычными механизмами.
CKAD не требует ESO напрямую, но знание этого паттерна — обязательно для production интервью.
CSI Secret Store Driver
Альтернатива ESO. CSI-драйвер (Container Storage Interface) монтирует секреты как volume напрямую из external store, минуя Kubernetes Secret object:
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: app-secrets
SecretProviderClass — CRD, описывает откуда тянуть (Vault, AWS SM, etc) и какие ключи в какие пути mount.
Преимущество: данные не материализуются в etcd как Secret. Они идут напрямую из external store в volume. Это compliance-плюс — etcd не содержит sensitive data.
Минус: env-маппинг отсутствует (только volume). Поэтому часто комбинируют: CSI mount + secretObjects field, который дополнительно создаёт K8s Secret из mount-нутого — и Pod может использовать secretKeyRef.
Killer-момент: env exposure vs volume privacy
Развёрнуто разбираем главную опасность env-метода для Secret:
# Pod использует env: DB_PASSWORD из secretKeyRef
# Внутри контейнера:
$ env | grep DB_PASSWORD
DB_PASSWORD=supersecret123
$ cat /proc/1/environ | tr '\0' '\n' | grep DB
DB_PASSWORD=supersecret123
$ ps eww 1
1 Ss 0:00 ./myapp DB_PASSWORD=supersecret123 OTHER_VAR=foo ...
ps eww (с расширенным env флагом) показывает env vars процесса. Если внутри контейнера есть другие пользователи или другие процессы (sidecar) — все они видят пароль.
С volume mount:
# Pod использует volume mount Secret в /etc/secrets/db_password
$ env | grep DB
# (ничего)
$ ps eww 1
1 Ss 0:00 ./myapp
$ cat /etc/secrets/db_password
supersecret123
Чтобы увидеть Secret, нужно прочитать конкретный файл — что приложение делает только в момент доступа к БД. Эта эфемерность даёт лучшую гигиену секретов.
Особенно опасно: некоторые приложения (Node.js, Python) логируют env vars при ошибке или включают их в crash reports. Sentry, Datadog, error trackers могут случайно отправить secrets в third-party SaaS. Volume mount — безопаснее.
Полный production-grade пример
apiVersion: v1
kind: Pod
metadata:
name: production-app
spec:
serviceAccountName: app-sa
automountServiceAccountToken: false # apps без API access — без token
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/myteam/app:1.0
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef: { name: app-config, key: log_level }
envFrom:
- configMapRef: { name: feature-flags }
volumeMounts:
- name: secrets
mountPath: /etc/app/secrets
readOnly: true
- name: tls
mountPath: /etc/app/tls
readOnly: true
volumes:
- name: secrets
secret:
secretName: app-secrets
defaultMode: 0400
- name: tls
secret:
secretName: web-tls
defaultMode: 0400
Здесь Pod:
- Имеет специальный SA
app-sa, но не mount-ит token (apps не делает API запросов) - Тянет image из private registry с pull secret
regcred - Конфигурацию (non-sensitive) — через ConfigMap (env)
- Секреты (DB password, API keys) — через volume mount (privacy)
- TLS-сертификаты — отдельным TLS Secret volume