Почему шифрование данных в Kubernetes - это не опция, а необходимость
Kubernetes по умолчанию хранит все объекты кластера, включая Secret, в etcd в кодировке base64 и без шифрования. Пароль от базы данных, токен API и приватный ключ TLS лежат в хранилище состояния как обычный текст: любой, кто получил доступ к etcd или к его снапшоту, читает их напрямую. Защита собирается из трех независимых слоев: шифрование данных в покое в etcd через EncryptionConfiguration, вынос секретов за пределы кластера (HashiCorp Vault, Sealed Secrets) и шифрование постоянных томов средствами CSI-драйвера на базе LUKS или облачного KMS.
Рабочий порядок действий такой: включить провайдер шифрования secretbox или kms для kube-apiserver, перезаписать существующие Secret, чтобы старые записи тоже закрылись криптографией, ограничить сетевой доступ к порту 2379, затем настроить управление секретами и шифрование томов. Ни один из шагов не требует простоя приложений, если заранее сделан бэкап etcd и не меняется содержимое Secret.
Ниже собраны проверенные команды и манифесты: версии API, флаги kube-apiserver, примеры StorageClass с шифрованием, интеграция Vault через sidecar и CSI-драйвер, а также перечень ошибок, которые чаще всего приводят к потере данных или к отказу API-сервера после перезапуска.
Требования регуляторов делают шифрование обязательным, а не желательным. PCI DSS требует защищать хранимые данные держателей карт, GDPR - применять соразмерные технические меры защиты персональных данных, ФЗ-152 - обеспечивать конфиденциальность при обработке. Проверка, которая проходит на аудите, выглядит одинаково: снапшот etcd и бэкап тома не содержат читаемых секретов. Общий подход к защите данных на разных уровнях разобран в материале про безопасность систем хранения и обработки данных.
Что и где хранится: etcd, секреты и постоянные тома
etcd - единственный источник истины о состоянии кластера. В нем лежат Pod, Deployment, ConfigMap, Secret, ServiceAccount, роли RBAC, Custom Resource и служебные объекты. Ключи хранятся в виде путей: например, Secret с именем mysecret в namespace default лежит по ключу /registry/secrets/default/mysecret. Значение самого объекта Secret - те же YAML-поля, только data закодирован в base64.
| Данные | Где лежат | Защита по умолчанию |
|---|---|---|
| Secret: пароли, токены, ключи | etcd, ключ /registry/secrets/<namespace>/<name> | base64, без шифрования |
| ConfigMap, манифесты, метаданные | etcd | открытый текст |
| Данные приложений | локальные диски узлов или внешняя СХД (PV) | зависит от драйвера и файловой системы |
| Трафик между компонентами | порты 6443, 2379, 2380 | TLS или mTLS, если сертификаты настроены |
| Бэкапы etcd | файл снапшота | нет, если шифровать отдельно |
Трафик между kube-apiserver, kubelet и etcd обычно уже закрыт TLS. Соединения клиентов к API-серверу идут на 6443, клиентский трафик к etcd - на 2379, обмен между узлами etcd - на 2380. Шифрование при передаче защищает от перехвата в сети, но не влияет на то, как байты лежат на диске. Это два разных периметра, и закрывать их нужно по отдельности.
Модель угроз: доступ к etcd, бэкапы и перехват трафика
Практических векторов атак немного, и каждый закрывается конкретной мерой:
- Прямой доступ к etcd. Незакрытый порт 2379 в сети или отсутствие клиентской аутентификации дает полный доступ ко всем секретам кластера. Лечится ограничением сети и обязательным mTLS, шифрование etcd здесь дополняет защиту.
- Кража снапшота etcd. Файл бэкапа, сохраненный в S3 или на NFS без шифрования, содержит все секреты в открытом виде. Шифрование данных в покое делает снапшот бесполезным без ключа.
- Физический или административный доступ к диску узла. Снятый диск или снимок виртуальной машины с etcd и томами приложений читается на любой другой машине.
- Перехват трафика (MITM). Закрывается проверкой сертификатов, отказом от параметра --insecure-port и строгим TLS на всех компонентах.
- Избыточные права внутри кластера. Пользователь с правом get secrets в широком namespace читает секреты легально, без взлома. Здесь работает RBAC и вынос секретов во внешнее хранилище.
Приоритет выстраивается по вероятности и цене: сначала закрывается доступ к API и etcd, затем включается шифрование данных в покое в etcd, после этого шифруются тома с персональными или платежными данными. Управление ключами выносится на отдельный уровень, о чем подробно написано в статье про шифрование данных при передаче и хранении, ключи и гибридные модели.
Встроенное шифрование секретов и etcd: настройка EncryptionConfiguration
EncryptionConfiguration - это YAML-файл, который читает kube-apiserver. В нем перечислены провайдеры шифрования и ключи. Порядок провайдеров в списке определяет приоритет: первый используется для записи новых объектов, остальные - для чтения старых. Провайдер identity означает отсутствие шифрования; он должен идти последним, иначе данные продолжат писаться в открытом виде.
Выбор провайдера шифрования: aescbc, secretbox или KMS
Доступные провайдеры различаются стойкостью и моделью хранения ключа:
- identity - шифрования нет, используется как fallback для чтения старых объектов.
- aescbc - AES в режиме CBC с дополнением PKCS#7, ключ длиной 32 байта. Из-за отсутствия проверки целостности и особенностей дополнения для новых кластеров этот режим не рекомендуют.
- secretbox - XSalsa20-Poly1305, аутентифицированное шифрование, ключ 32 байта. Оптимальный выбор для большинства кластеров: ключ лежит в файле на control-plane, криптография стойкая.
- aesgcm - AES-GCM, ключ 32 байта. Быстрее secretbox, но требует ротации ключа примерно каждые 200 000 записей из-за риска повторного использования nonce.
- kms - внешний сервис управления ключами (HashiCorp Vault Transit, облачный KMS). Ключ шифрования ключей не покидает KMS, в файле конфигурации хранится только endpoint. Используйте apiVersion v2: версия v1 объявлена устаревшей.
Провайдеры комбинируются. Типовая схема для продакшена: kms первым, secretbox вторым как локальный резерв, identity последним на время миграции. Когда все объекты перезаписаны, identity убирают.
Пошаговая настройка: генерация ключа, правка манифеста, проверка
Шаг 1. Сгенерируйте ключ длиной 32 байта и сохраните его в защищенном месте вне кластера:
head -c 32 /dev/urandom | base64
Шаг 2. Создайте файл /etc/kubernetes/enc/encryption-config.yaml на control-plane с правами 600 и владельцем root:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- kms:
apiVersion: v2
name: vault-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
timeout: 3s
- secretbox:
keys:
- name: key1
secret: <сгенерированный base64-ключ>
- identity: {}
Шаг 3. Добавьте в /etc/kubernetes/manifests/kube-apiserver.yaml флаг и монтирование файла. Правка манифеста статического пода автоматически перезапустит kube-apiserver, поэтому работы планируйте на окно обслуживания.
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
- --encryption-provider-config-automatic-reload=true
volumeMounts:
- name: enc
mountPath: /etc/kubernetes/enc
readOnly: true
volumes:
- name: enc
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
Для кластеров, собранных через kubeadm, файл находится по этому же пути. Если control-plane несколько, повторите шаги на каждом узле: ключи должны совпадать, иначе объекты, записанные на одном узле, не расшифруются на другом.
Шаг 4. Убедитесь, что API-сервер поднялся: kubectl get pods -n kube-system и kubectl get --raw='/readyz?verbose'.
Шаг 5. Создайте тестовый секрет и прочитайте его напрямую из etcd:
kubectl create secret generic demo-secret --from-literal=password='SuperSecret123' ETCDCTL_API=3 etcdctl \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ get /registry/secrets/default/demo-secret
В выводе перед полезной нагрузкой появится префикс вида k8s:enc:secretbox:v1:key1:. Если видите читаемый YAML с base64-значением, шифрование не применилось: проверьте порядок провайдеров, путь к файлу и логи kube-apiserver.
Шаг 6. Перезапишите все существующие секреты, потому что новые настройки действуют только на операции записи:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Команда перезапишет объекты с новыми ревизиями, после чего kube-apiserver зашифрует их текущим ключом. Часть системных секретов может конфликтовать при replace: такие объекты пересоздаются их контроллерами, ошибки в выводе стоит разобрать отдельно.
Ротация ключей и миграция данных без простоя
Ключ меняется без остановки API. Добавьте новый ключ key2 в список keys первым, перезапустите kube-apiserver (через правку манифеста или перезагрузку конфигурации при включенном автоматическом перечитывании), выполните перезапись всех секретов повторно, затем удалите key1 из конфигурации. До удаления старого ключа убедитесь, что в etcd не осталось объектов с префиксом k8s:enc:secretbox:v1:key1: - иначе их чтение сломается.
Перед любой ротацией делайте свежий снапшот etcd и храните его вместе с копией ключа. Перезапись нескольких тысяч секретов дает заметную нагрузку на etcd: следите за метрикой etcd_disk_backend_commit_duration_seconds и разносите операцию по namespace.
Внешние решения для управления секретами: HashiCorp Vault и Sealed Secrets
Встроенное шифрование закрывает данные на диске, но оставляет ключ на control-plane, не дает аудита обращений и не умеет выдавать короткоживущие учетные данные. Когда секретов много, они меняются каждые несколько часов или требуют разграничения доступа по командам, в игру вступают внешние хранилища. Подробный разбор всех вариантов с таблицей сравнения есть в руководстве про хранение паролей в Kubernetes: Secrets, External Secrets и Vault.
HashiCorp Vault: интеграция через Agent Injector и CSI
Vault хранит секреты вне etcd, ведет журнал аудита и умеет выдавать динамические учетные данные с TTL: например, логин и пароль к PostgreSQL, живущие 15 минут. Установка в кластер выполняется через Helm:
helm repo add hashicorp <официальный репозиторий HashiCorp> helm install vault hashicorp/vault --namespace vault --create-namespace vault auth enable kubernetes vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
Первый способ доставки - Agent Injector. Он подставляет в Pod sidecar, который получает секрет из Vault и пишет его в общий emptyDir. Приложение читает файл, секрета в etcd нет вообще:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "app"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/app/db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "secret/data/app/db" -}}
postgres://{{ .Data.data.user }}:{{ .Data.data.password }}@db:5432/app
{{- end }}
Второй способ - Vault Secrets Store CSI Driver. Он монтирует секреты как файлы в том через драйвер secrets-store.csi.k8s.io и описывается объектом SecretProviderClass. Вариант удобен тем, что меняет только spec.volumes в Deployment, а не добавляет sidecar-контейнеры. Для синхронизации секретов Vault в обычные Secret (например, чтобы подхватывались через envFrom) применяют External Secrets Operator: он создает и обновляет Secret из внешнего хранилища по расписанию.
Sealed Secrets: шифрование секретов для GitOps
Sealed Secrets решает другую задачу: позволяет безопасно хранить секреты в Git. В кластере работает контроллер, который генерирует пару ключей RSA. Публичным ключом утилита kubeseal шифрует Secret, приватным контроллер расшифровывает его в кластере и создает обычный Secret.
kubectl create secret generic app-secret \ --from-literal=password='S3cr3t' \ --dry-run=client -o yaml | kubeseal --format yaml > sealedsecret.yaml kubectl apply -f sealedsecret.yaml
Результат можно коммитить в Git: без приватного ключа контроллера расшифровать его нельзя. Ограничения: SealedSecret привязан к namespace и имени объекта, поэтому перенос между средами требует перешифрования; приватный ключ лежит в Secret sealed-secrets-key в kube-system, и его потеря означает невозможность расшифровать все ранее созданные SealedSecret. Резервную копию ключа храните в защищенном хранилище, а ротацию ключа контроллера планируйте вместе с перешифрованием всех манифестов. Базовая работа с самими объектами Secret, включая генераторы и проверку содержимого, разобрана в руководстве по Kubernetes Secrets.
Шифрование томов с помощью CSI-драйверов
Секреты в etcd - только часть картины. Данные приложений лежат на PersistentVolume, и снапшот такого тома читается как обычная файловая система. Container Storage Interface (CSI) стандартизирует подключение хранилищ, а часть драйверов умеет шифровать тома прозрачно для приложения: на уровне хранилища или через dm-crypt на узле.
Обзор CSI-драйверов с поддержкой шифрования
- AWS EBS CSI - шифрование через KMS, для новых томов включено по умолчанию, ключ задается параметром StorageClass.
- GCP Persistent Disk CSI - шифрование ключами CMEK, ключ указывается при создании диска.
- Azure Disk CSI - шифрование ключами из Key Vault, поддерживается смена ключа без пересоздания тома.
- Longhorn - шифрование томов через LUKS/dm-crypt, требуется пакет cryptsetup на узлах и ключ в Secret.
- Rook Ceph - шифрование OSD через dm-crypt, включается в манифесте CephCluster параметром encrypted для устройств.
- Локальные CSI-драйверы (например, для прямых дисков) - шифрование чаще делают на уровне хоста через LUKS или mdadm с crypt, а драйвер лишь предоставляет устройство.
Проверяйте документацию конкретного драйвера перед выбором: не все поддерживают шифрование, а часть облачных решений шифрует только данные на диске, но не трафик между узлом и СХД. Общая логика подключения хранилищ, типы томов и динамический провижионинг описаны в статье про интеграцию СХД с Kubernetes через CSI и Persistent Volumes.
Настройка StorageClass с шифрованием: пример для Longhorn
Сначала создайте Secret с парольной фразой шифрования в namespace Longhorn, затем StorageClass со ссылкой на него. Ключ передается как параметр csi.storage.k8s.io/node-publish-secret-name:
apiVersion: v1 kind: Secret metadata: name: longhorn-crypto namespace: longhorn-system stringData: CRYPTO_KEY_VALUE: "замените-на-длинную-парольную-фразу" CRYPTO_KEY_PROVIDER: "secret" CRYPTO_KEY_CIPHER: "aes-xts-plain64" CRYPTO_KEY_HASH: "sha256" CRYPTO_KEY_SIZE: "256" CRYPTO_PBKDF: "argon2i" --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: longhorn-encrypted provisioner: driver.longhorn.io allowVolumeExpansion: true reclaimPolicy: Delete volumeBindingMode: Immediate parameters: numberOfReplicas: "3" staleReplicaTimeout: "30" encrypted: "true" csi.storage.k8s.io/node-publish-secret-name: longhorn-crypto csi.storage.k8s.io/node-publish-secret-namespace: longhorn-system
Дальше создается PVC со ссылкой на этот класс:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: longhorn-encrypted
resources:
requests:
storage: 10Gi
Проверка: в объекте тома Longhorn должно быть encrypted: true, а на узле, где том подключен, команда dmsetup ls --target crypt покажет устройство с типом crypt. Если устройства нет, проверьте, установлен ли cryptsetup на каждом узле и включено ли шифрование в настройках драйвера. Парольную фразу храните вне кластера: при ее потере данные тома не восстановит ни один бэкап.
Пошаговый план включения шифрования в работающем кластере
Последовательность, проверенная на боевых кластерах: оценить текущее состояние, сделать бэкап etcd, включить шифрование данных в покое, вынести секреты во внешнее хранилище, закрыть тома, проверить приложения, настроить мониторинг, провести ротацию ключей. Изменения лучше сначала прогнать на staging с копией манифестов, а на прод переносить после успешной проверки восстановления из снапшота.
Проверка текущего состояния и бэкап etcd
Сначала выясните, включено ли шифрование. Признаки простые: наличие флага --encryption-provider-config в манифесте kube-apiserver и префикс k8s:enc: в значениях объектов etcd.
kubectl -n kube-system get pod kube-apiserver-<node> -o yaml | grep encryption ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%F-%H%M).db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-2026-09-17-1200.db --write-out=table
Снапшот сохраняет текущее состояние, включая те секреты, которые еще не перезаписаны. Один и тот же снапшот используется и для отката, и для проверки: восстановите его на тестовом узле, поднимите etcd и убедитесь, что кластер читается. Файл снапшота шифруйте отдельно, иначе он становится главной утечкой.
Тестирование и мониторинг после включения
После применения настроек проверьте четыре вещи: kube-apiserver запущен и готов, тестовый Secret читается приложением, значение в etcd имеет префикс k8s:enc:, старые секреты перезаписаны. Для контроля состояния держите в мониторинге метрики API-сервера apiserver_encryption_config_controller_automatic_reload_success_total и apiserver_encryption_config_controller_automatic_reload_failure_total: рост второй метрики означает, что конфигурация не перечиталась и новые данные пишутся старым ключом. Ошибки расшифровки видны в логах kube-apiserver по ключевым словам failed to decrypt и decrypt: проверьте их после ротации ключей. Алерт стоит ставить на любую ошибку шифрования, а не на порог: такие сбои быстро приводят к недоступности объектов.
Типичные ошибки и как их избежать
- Потеря ключа шифрования. Без ключа данные в etcd не читаются, восстановление возможно только из снапшота, сделанного до шифрования. Храните ключи в Vault, HSM или защищенном сейфе, отдельно от бэкапов.
- identity первым в списке провайдеров. Новые объекты снова пишутся в открытом виде, шифрование не работает при формально примененной конфигурации.
- Отсутствие перезаписи секретов. Старые записи остаются незашифрованными, аудит покажет расхождение.
- Нет бэкапа before изменений. Любая правка kube-apiserver без снапшота - риск потерять кластер из-за опечатки в YAML.
- Права на файл конфигурации. Файл с ключом должен быть доступен только root, иначе ключ читается любым пользователем узла.
- Несовпадение ключей между control-plane узлами. Объекты, записанные на одном узле, не расшифровываются на другом.
- Игнорирование ротации. Ключ, живущий годами без смены, повышает цену его компрометации.
Потеря ключей шифрования: как не остаться без данных
При утере ключа объекты в etcd остаются физически целыми, но бесполезными: API-сервер вернет ошибку расшифровки при чтении. Единственный путь восстановления - снапшот etcd, снятый до включения шифрования, либо копия ключа. Отсюда правило: бэкап etcd и ключ хранятся вместе как единый набор восстановления, но в разных системах доступа. При использовании провайдера kms ключ шифрования ключей находится во внешней системе, и потеря локального файла конфигурации не приводит к потере данных. Для парольных фраз томов действует та же логика: утеря CRYPTO_KEY_VALUE делает том нечитаемым.
Несовместимость версий и форматов конфигурации
Формат EncryptionConfiguration версионируется отдельно от Kubernetes. Актуальная версия apiserver.config.k8s.io/v1 работает начиная с Kubernetes 1.13, более старые beta-версии удалены, поэтому в манифестах старых кластеров их нужно заменить до обновления. Провайдер kms v2 стабилен с версии 1.29, а kms v1 объявлен устаревшим: при апгрейде кластера переносите конфигурацию на v2, иначе получите предупреждения и возможную остановку расшифровки. Перед обновлением Kubernetes проверьте три вещи: версию API в файле конфигурации, наличие плюгина KMS на узлах и совместимость CSI-драйвера с новой версией. Для CSI ориентируйтесь на матрицу поддержки драйвера и спецификацию CSI, которую он реализует.
Сравнение подходов и выбор оптимального стека
| Подход | Сложность | Уровень защиты | Где ключ | GitOps |
|---|---|---|---|---|
| EncryptionConfiguration (secretbox) | Низкая | Средний | Файл на control-plane | Нет |
| Провайдер kms (Vault, облачный KMS) | Средняя | Высокий | Во внешней системе | Нет |
| Sealed Secrets | Низкая | Средний | Приватный ключ контроллера | Да |
| External Secrets Operator + Vault | Высокая | Высокий | В Vault | Да, без самих секретов |
| CSI-шифрование томов | Зависит от драйвера | Высокий для данных на диске | KMS или парольная фраза | Нет |
Критерии выбора: безопасность, сложность, производительность
Оценивайте четыре параметра. Первый - от кого защищаем: от кражи диска и бэкапа достаточно шифрования etcd и томов, от внутреннего нарушителя с правами в кластере нужно внешнее хранилище с аудитом. Второй - стоимость поддержки: Vault требует резервирования, обновлений и распечатывания unseal-ключей, Sealed Secrets обслуживается самим контроллером. Третий - производительность: secretbox и LUKS добавляют единицы процентов накладных расходов на CPU узлов, облачные KMS добавляют сетевой вызов при каждой операции записи секрета, поэтому для kms-провайдера важен кэш DEK в v2. Четвертый - требования compliance к ротации ключей и журналу доступа.
Рекомендации для разных сценариев
Для небольшого кластера или dev-среды хватит встроенного шифрования secretbox и Sealed Secrets для GitOps: настройка занимает час, ключи лежат в двух местах, аудит минимальный.
Для enterprise-контура разумный стек выглядит так: kms-провайдер v2 на базе Vault или облачного KMS, External Secrets Operator для доставки секретов, CSI-шифрование для томов с персональными данными, отдельный контур управления ключами и алерты на ошибки расшифровки.
Для облачных кластеров проще опираться на управляемый KMS провайдера и шифрование томов, включенное по умолчанию в StorageClass, а собственное шифрование etcd оставить как дополнительный слой. Если нужен кластер с управляемой control-plane и встроенной интеграцией с хранилищем, подойдет Timeweb Cloud: там доступны серверы, хранилище и Kubernetes, а ключи и тома удобно разделять между средами.
Практический шаг, который можно сделать сегодня: снять снапшот etcd, проверить наличие флага --encryption-provider-config, прогнать перезапись секретов в staging и убедиться, что в дампе etcd вместо пароля стоит префикс k8s:enc:. Дальше по списку - вынос секретов в Vault или Sealed Secrets и шифрование томов с данными, которые нельзя потерять.