Learning Platform
Глоссарий Troubleshooting
Урок 07.04 · 22 мин
Продвинутый
secretKeyRefenvFromtmpfsimagePullSecretsServiceAccountprojected volumeExternal Secrets Operator

Как использовать 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 — переменная просто не будет установлена.

WARNING

Главная проблема 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 данных).

NOTE

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 — все ключи.

Secret через volume mount → tmpfs
Secret в etcdЗашифрован (если encryption-config) или plaintext+base64 (если нет). API server возвращает kubelet'у при mount.
GET через kubelet
kube-apiserverDecrypt at boundary: расшифровывает данные перед отправкой kubelet'у. По защищённому TLS-каналу.
TLS over network
kubelet on nodeСоздаёт tmpfs mount /var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~secret/<name>/. Tmpfs — in-memory filesystem, никогда не записывается на диск.
bind-mount в контейнер
контейнерВидит файлы по mountPath. Permissions defaultMode/items.mode. Приложение читает через обычный file read. Secret НЕ висит в env, не виден через ps eww.

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 кубовый.
TIP

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, нужно прочитать конкретный файл — что приложение делает только в момент доступа к БД. Эта эфемерность даёт лучшую гигиену секретов.

DANGER

Особенно опасно: некоторые приложения (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

Проверка знанийKnowledge check
Security audit обнаружил в Sentry crash reports вашего приложения утечку DB password. Pod использует env var DB_PASSWORD из secretKeyRef. Какова первопричина, и как переделать архитектуру?
ОтветAnswer
Первопричина — exposure через process environment. Когда приложение крашится, Sentry SDK (и большинство других error trackers) автоматически собирают весь process environ как часть crash report и отправляют в SaaS. DB_PASSWORD оказывается в environ потому что Kubernetes установил его через secretKeyRef в env var. Это видно через любой механизм: env command, /proc/1/environ, ps eww, любая crash-reporting tool. Решение — переключиться на volume mount: 1) удалить env section с secretKeyRef из Pod; 2) добавить volume.secret с secretName; 3) добавить volumeMount по пути /etc/app/secrets/db_password; 4) изменить код приложения — читать пароль из файла, не из env. Файлы не попадают в crash reports автоматически — нужно явное чтение, что приложение делает только при подключении к БД. Дополнительно: можно zeroize пароль в памяти после использования (open connection, освободить string), хотя в Go/Java garbage-collected runtimes это сложно. Для maximum security — использовать short-lived credentials через Vault dynamic secrets или IAM database authentication (где Pod получает временный token из IAM, а не статичный пароль).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какие преимущества даёт mount Secret через volume вместо env var (с точки зрения безопасности)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5