Secrets: типы, base64 vs encryption, etcd encryption at rest
Secret в Kubernetes выглядит как ConfigMap — те же поля, те же способы подключения к Pod. Но это иллюзия безопасности. Внутри — несколько важных отличий, и одно главное правило: base64 это НЕ шифрование. Понимать это — обязательно, чтобы не сделать кластер дырявым.
Docker Compose secrets и configs: безопасная передача паролей
Структура Secret
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: production
type: Opaque
data:
db_password: c3VwZXJzZWNyZXQxMjM=
api_key: YWJjZGVmZ2hpams=
stringData:
jwt_secret: "plain-text-here"
Поля:
type— категория Secret. DefaultOpaque. Влияет на ожидаемые ключи (например,kubernetes.io/tlsтребуетtls.crtиtls.key).data— словарь string → base64. Каждое значение должно быть валидным base64 (echo -n "supersecret123" | base64).stringData— словарь string → plain text. API server автоматически кодирует в base64 и кладёт вdataпри создании. Удобно для написания YAML вручную. После applystringDataисчезает — остаётся толькоdata.
При коллизии (ключ есть и в data, и в stringData) — stringData побеждает. API server конвертирует stringData → data, перетирая.
БАЗА: base64 это не encryption
data:
db_password: c3VwZXJzZWNyZXQxMjM=
c3VwZXJzZWNyZXQxMjM= выглядит как зашифрованный текст. Это не зашифрованный текст. Это просто кодирование (encoding), которое любой человек, любая программа, любой пайплайн декодирует за миллисекунду:
echo "c3VwZXJzZWNyZXQxMjM=" | base64 -d
# supersecret123
base64 — это alphabet-to-bytes representation. Нужно для двух целей в Kubernetes Secrets:
- Поддержка binary content в YAML (бинарь не валидный UTF-8, его нельзя положить в YAML напрямую)
- Совместимость с системами, которые не толерируют управляющие символы (newlines, null bytes) в значениях
base64 не защищает данные. Любой, кто получил YAML файл или сделал kubectl get secret, легко его декодирует.
“У нас Secret, значит безопасно” — самая частая ошибка джунов. Если у пользователя есть RBAC-доступ get secrets в namespace — он видит данные в открытом виде после base64 decode (это тривиальная операция). Secret защищён ровно настолько, насколько защищён RBAC и encryption at rest. base64 — никакая не защита.
etcd encryption at rest
Secret хранится в etcd. По умолчанию etcd хранит plaintext (после base64 decode):
# На master-node
ETCDCTL_API=3 etcdctl get /registry/secrets/default/app-secrets
# /registry/secrets/default/app-secrets
# k8s
# ...
# db_password\x12supersecret123 ← в открытом виде
То есть кто угодно с доступом к etcd (root на master node, backup etcd, copy etcd.db) видит Secret-значения как plaintext. Это серьёзная проблема для compliance (PCI-DSS, HIPAA, SOC2).
Решение — encryption at rest: настроить kube-apiserver шифровать Secret-данные перед записью в etcd, и расшифровывать при чтении. etcd хранит ciphertext, не plaintext.
Конфигурация на kube-apiserver
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded 32-byte key>
- identity: {}
И флаг kube-apiserver: --encryption-provider-config=/etc/kubernetes/encryption-config.yaml.
Provider-ы
identity— нет шифрования (plaintext, default если encryption-config не настроен). Используется как fallback в списке провайдеров.aescbc— AES-CBC с PKCS#7 padding. 32-байтный ключ. Быстрый, надёжный. Рекомендуется для большинства case-ов.aesgcm— AES-GCM. Authenticated encryption. Требует ротации ключей каждые ~200 000 записей (иначе risk nonce collision).secretbox— XSalsa20+Poly1305. Modern, secure. Быстрее aescbc для маленьких данных.kms— внешний KMS (AWS KMS, GCP KMS, HashiCorp Vault, Azure Key Vault). DEK envelope encryption: данные шифруются локальным DEK, DEK шифруется master key в KMS. Production-grade решение для highly regulated сред.
Порядок провайдеров
Список провайдеров читается по порядку:
- Чтение: пробуется каждый провайдер до тех пор, пока один не расшифрует.
- Запись: используется первый провайдер.
Это позволяет ротацию: добавить новый провайдер первым, потом перешифровать существующие Secret-ы командой kubectl get secrets -A -o json | kubectl replace -f -.
Если encryption-config настроен, но до этого в кластере уже были созданы Secrets — они хранятся в etcd как plaintext. Они начнут шифроваться только при следующем write. Чтобы перешифровать все существующие Secret-ы, нужно kubectl get secrets --all-namespaces -o json | kubectl replace -f - (forces rewrite). Без этого старые Secret-ы остаются в plaintext.
Встроенные типы Secret
Поле type в Secret — не просто метка. Оно сообщает controller-ам и kubelet о назначении и ожидаемой структуре. Семь built-in типов:
1. Opaque (default)
Generic Secret. Никаких ограничений на ключи и значения.
apiVersion: v1
kind: Secret
metadata: { name: app-secrets }
type: Opaque
data:
db_password: ...
api_key: ...
2. kubernetes.io/service-account-token
Auto-managed. Привязан к ServiceAccount, содержит JWT-токен для аутентификации Pod в API server.
type: kubernetes.io/service-account-token
metadata:
annotations:
kubernetes.io/service-account.name: my-sa
С Kubernetes v1.24+ эти Secret-ы не создаются автоматически при создании ServiceAccount (раньше создавались). Теперь токен генерируется ad-hoc через TokenRequest API и mount-ится через projected volume. Это безопаснее: токены короткоживущие и автоматически ротируются.
Если очень нужен long-lived токен (для CI/CD, например) — можно создать вручную с этой аннотацией. API server сам сгенерирует и положит JWT в data поле.
3. kubernetes.io/dockerconfigjson
Для imagePullSecrets — авторизация в private registries.
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64 of ~/.docker/config.json>
Создаётся через kubectl create secret docker-registry:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass \
[email protected]
4. kubernetes.io/tls
Для TLS-сертификатов. Обязательные ключи: tls.crt (сертификат + chain) и tls.key (private key).
type: kubernetes.io/tls
data:
tls.crt: <base64 cert>
tls.key: <base64 key>
Создаётся через kubectl create secret tls:
kubectl create secret tls web-tls \
--cert=./tls.crt \
--key=./tls.key
Используется Ingress контроллерами (Ingress.spec.tls.secretName), service mesh-ами (Istio, Linkerd), webhook серверами.
5. kubernetes.io/basic-auth
Username + password в фиксированных ключах.
type: kubernetes.io/basic-auth
stringData:
username: admin
password: s3cret
kubectl create secret generic basic --type=kubernetes.io/basic-auth \
--from-literal=username=admin --from-literal=password=s3cret
6. kubernetes.io/ssh-auth
SSH private key. Обязательный ключ ssh-privatekey.
type: kubernetes.io/ssh-auth
data:
ssh-privatekey: <base64 of private key>
Используется GitOps-операторами (Flux, Argo CD) для git-доступа.
7. bootstrap.kubernetes.io/token
Для kubeadm bootstrap — токены, которые позволяют новым node присоединяться к кластеру.
type: bootstrap.kubernetes.io/token
metadata:
name: bootstrap-token-abcdef
namespace: kube-system
data:
token-id: abcdef
token-secret: ...
Кроме встроенных, можно создать любой пользовательский тип (example.com/my-type). Но без специальной обработки controller-ами он функционально равен Opaque.
Поле type нельзя изменить после создания. Если ошиблись — удалить и создать заново. Тип влияет на validation: например, kubernetes.io/tls без tls.crt/tls.key API server отклонит.
Создание Secret: imperative и declarative
Imperative (быстрее для CKAD)
# Generic (Opaque)
kubectl create secret generic app-secrets \
--from-literal=db_password=supersecret123 \
--from-literal=api_key=abcdef
# Из файла
kubectl create secret generic ssh-key --from-file=id_rsa
# TLS
kubectl create secret tls web-tls --cert=tls.crt --key=tls.key
# Docker registry
kubectl create secret docker-registry regcred \
--docker-server=quay.io \
--docker-username=$USER \
--docker-password=$PASS \
--docker-email=$EMAIL
# Basic-auth
kubectl create secret generic creds --type=kubernetes.io/basic-auth \
--from-literal=username=admin --from-literal=password=s3cret
Declarative с stringData
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
db_password: supersecret123
api_key: abcdef
stringData удобнее для ручного написания, чем data с base64. API server автоматически кодирует.
Лимит размера
Тот же что у ConfigMap — 1 MiB. Etcd лимит распространяется на все объекты. Для больших sensitive данных (большие certs, keystore-ы, encrypted blobs) — разбивать на несколько Secret-ов, либо использовать external secret store (Vault, AWS Secrets Manager).
Inspection
# Список
kubectl get secret
# Detail (значения скрыты)
kubectl describe secret app-secrets
# data:
# ====
# db_password: 14 bytes
# api_key: 6 bytes
# Получить значение (вернёт base64)
kubectl get secret app-secrets -o jsonpath='{.data.db_password}'
# c3VwZXJzZWNyZXQxMjM=
# Декодировать
kubectl get secret app-secrets -o jsonpath='{.data.db_password}' | base64 -d
# supersecret123
kubectl describe намеренно показывает только размер, не значение — чтобы случайно не утечь в логи или скриншоты.