Введение
При развёртывании приложений в Kubernetes одной из первых проблем безопасности становится управление секретами (secret management). В начале разработки велик соблазн захардкодить пароли прямо в файлах значений Helm, однако такой подход создаёт серьёзные риски и операционные трудности. В этой статье я расскажу, как перенёс пароль администратора Grafana из захардкоженного Helm-значения в AWS Systems Manager Parameter Store с помощью External Secrets Operator.
Проблема: захардкоженные секреты
Я разворачивал Grafana в кластере EKS с помощью Helm-чарта kube-prometheus-stack. Начальная конфигурация выглядела так:
grafana:
enabled: true
adminPassword: "ChangeMe123!" # TODO: перенести в Kubernetes Secret
Для тестирования в песочнице это приемлемо, но на практике возникает ряд проблем:
-
Пароль виден в системе контроля версий.
-
Любой, у кого есть доступ к репозиторию, может увидеть учётные данные.
-
Ротация паролей требует изменения кода и повторного развёртывания.
-
Отсутствует централизованное управление секретами между окружениями.
Решение: External Secrets Operator
Рассмотрев несколько вариантов (Sealed Secrets, SOPS, нативные секреты Kubernetes), я выбрал External Secrets Operator по ряду причин:
-
Нативная интеграция с сервисами AWS (Secrets Manager и SSM Parameter Store).
-
Централизованное хранение секретов за пределами кластера.
-
Поддержка автоматической ротации секретов.
-
Возможность повторного использования в нескольких кластерах и пространствах имён.
-
Экономичность при использовании SSM Parameter Store (стандартные параметры бесплатны).
Обзор архитектуры
Архитектура External Secrets Operator состоит из нескольких взаимодействующих компонентов:
External Secrets Operator — работает в кластере и синхронизирует секреты из внешних источников. ClusterSecretStore — описывает способ подключения к AWS SSM Parameter Store. ExternalSecret — указывает, какие секреты нужно получить и где их хранить. IRSA (IAM Roles for Service Accounts) — предоставляет оператору учётные данные AWS. Kubernetes Secret — итоговый секрет, создаваемый в кластере для использования приложениями.
Руководство по внедрению
Шаг 1: установка External Secrets Operator
Поскольку я использую ArgoCD с паттерном app-of-apps, я создал новое приложение ArgoCD для развёртывания External Secrets Operator:
# application/argocd-apps/external-secrets-operator.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets-operator
namespace: argocd
spec:
project: default
source:
chart: external-secrets
repoURL: https://charts.external-secrets.io
targetRevision: 0.11.0
helm:
releaseName: external-secrets
values: |
installCRDs: true
serviceAccount:
create: true
name: external-secrets
destination:
server: https://kubernetes.default.svc
namespace: external-secrets-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Шаг 2: настройка IAM-роли для сервисного аккаунта (IRSA)
External Secrets Operator нуждается в правах на чтение из AWS SSM Parameter Store. Для создания необходимых IAM-политики и роли я воспользовался Terraform:
# IAM-политика для External Secrets
resource "aws_iam_policy" "external_secrets_policy" {
name = "ExternalSecretsPolicy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ssm:GetParameter",
"ssm:GetParameters"
]
Resource = "arn:aws:ssm:${var.region}:${var.account}:parameter/eks_project/*"
},
{
Effect = "Allow"
Action = ["kms:Decrypt"]
Resource = "*"
Condition = {
StringEquals = {
"kms:ViaService" = "ssm.${var.region}.amazonaws.com"
}
}
}
]
})
}
Разрешение на расшифровку KMS необходимо, поскольку параметры типа SSM SecureString по умолчанию шифруются с помощью KMS.
Затем я создал IAM-роль с политикой доверия, позволяющей сервисному аккаунту External Secrets выполнять её принятие (assume):
resource "aws_iam_role" "external_secrets_role" {
name = "ExternalSecretsRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${replace(data.aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" =
"system:serviceaccount:external-secrets-system:external-secrets"
}
}
}]
})
}
Шаг 3: аннотация сервисного аккаунта
После создания IAM-роли я обновил приложение ArgoCD для External Secrets Operator, добавив в сервисный аккаунт аннотацию с ARN роли:
serviceAccount:
create: true
name: external-secrets
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/ExternalSecretsRole
Шаг 4: создание ClusterSecretStore
Ресурс ClusterSecretStore описывает, как External Secrets подключается к AWS SSM Parameter Store. Изначально я попробовал использовать пространство имён SecretStore, но столкнулся с ошибкой валидации: External Secrets требует, чтобы пространство имён SecretStore совпадало с пространством имён сервисного аккаунта. Использование ClusterSecretStore решило эту проблему:
# application/k8-manifests/monitoring/manifests/external-secrets.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-parameter-store
spec:
provider:
aws:
service: ParameterStore
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secrets-system
ClusterSecretStore может использоваться ресурсами ExternalSecret в любом пространстве имён, что делает его переиспользуемым в масштабах всего кластера при сохранении безопасности через RBAC и ограниченную IAM-политику.
Шаг 5: создание ресурса ExternalSecret
Ресурс ExternalSecret указывает, какой секрет нужно получить из AWS и как оформить его в Kubernetes. Helm-чарт Grafana требует наличия в секрете как имени пользователя, так и пароля, поэтому я использовал шаблон, в котором захардкоженное имя пользователя сочетается с паролем из Parameter Store:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: grafana-admin-credentials
namespace: monitoring
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-parameter-store
kind: ClusterSecretStore
target:
name: grafana-admin-secret
creationPolicy: Owner
template:
data:
admin-user: "admin"
admin-password: "{{ .password }}"
data:
- secretKey: password
remoteRef:
key: /eks_project/grafana_password
Функция шаблонов позволяет объединять статические значения (например, имя пользователя) с динамическими, полученными из AWS, формируя корректно оформленный секрет для Grafana.
Шаг 6: обновление Helm-значений Grafana
Наконец, я обновил Helm-значения Grafana, указав ссылку на секрет, созданный External Secrets:
grafana:
enabled: true
admin:
existingSecret: grafana-admin-secret
passwordKey: admin-password
userKey: admin-user
Возникшие трудности и их решения
Трудность 1: нехватка ресурсов при установке
При развёртывании External Secrets Operator поды не могли запланироваться — возникала ошибка «0/2 nodes are available: 2 Too many pods». Это указывало на то, что кластер EKS достиг максимального числа подов.
Решение: я установил Cluster Autoscaler для автоматического масштабирования группы узлов, когда поды не удаётся запланировать, а также metrics-server для мониторинга ресурсов. После масштабирования кластера поды External Secrets успешно развернулись.
Трудность 2: ошибка валидации пространства имён SecretStore
Поначалу я попытался использовать SecretStore в пространстве имён monitoring, ссылаясь на сервисный аккаунт External Secrets в пространстве имён external-secrets-system. Это завершилось ошибкой: «invalid Auth.JWT.ServiceAccountRef: namespace should either be empty or match the namespace of the SecretStore».
Решение: я перешёл на ClusterSecretStore, который может ссылаться на сервисные аккаунты в любом пространстве имён. Это, к слову, рекомендуемый подход для общей инфраструктурной конфигурации секретов.
Трудность 3: пароль Grafana не обновлялся
После настройки секрета Grafana продолжала использовать старый захардкоженный пароль. Это происходит потому, что Grafana сохраняет пароль администратора в своей базе данных при первом запуске и не обновляет его автоматически при изменении секрета.
Решение: сначала я попробовал удалить только PersistentVolumeClaim, но это не помогло. В итоге я удалил всё приложение ArgoCD для мониторинга и дал ArgoCD заново развернуть его с нуля. Такой подход подтвердил корректность декларативной конфигурации и гарантировал, что Grafana инициализируется с паролем из External Secret. В продакшн-окружении с существующими дашбордами и данными пароль пришлось бы сбросить вручную — через Grafana CLI или admin API, — а не уничтожать и пересоздавать всё приложение.
Стоимость решения
Одним из ключевых решений стал выбор между AWS Secrets Manager и SSM Parameter Store. Сравнение затрат:
| Сервис | Стоимость хранения | Стоимость API-вызовов |
|---|---|---|
Secrets Manager |
$0,40 за секрет в месяц |
$0,05 за 10 тыс. вызовов |
Parameter Store |
Бесплатно (стандартный уровень) |
Бесплатно (стандартный уровень) |
Для моего сценария SSM Parameter Store оказался очевидным выбором.
Полученные преимущества
По завершении внедрения я получил ряд существенных преимуществ:
Централизованное управление секретами: все секреты теперь хранятся в AWS SSM Parameter Store — это единый источник истины для нескольких кластеров и окружений. Повышенная безопасность: секреты больше не фигурируют в Git-репозиториях или файлах Helm-значений; доступ контролируется через IAM-политики AWS. Упрощённая ротация: пароли можно ротировать в AWS Parameter Store, не трогая Kubernetes-манифесты и не запуская повторные развёртывания. Автоматическая синхронизация: External Secrets автоматически синхронизирует изменения из AWS в Kubernetes раз в час. Переиспользуемость: ClusterSecretStore можно применять для других приложений и секретов во всём кластере.
Рассмотренные альтернативы
Прежде чем остановиться на External Secrets Operator, я оценил несколько альтернатив:
Sealed Secrets: шифрует секреты для хранения в Git. Проще, чем External Secrets, но секреты по-прежнему остаются в репозитории и требуют повторного запечатывания при ротации. SOPS: шифрует значения в YAML-файлах. Хорошо подходит для GitOps, но требует ручной ротации и обработки дешифровки. AWS Secrets Manager CSI Driver: монтирует секреты как файлы в подах. Менее гибок, чем External Secrets, и по-прежнему влечёт расходы на Secrets Manager. Нативные Kubernetes Secrets: самый простой подход, но не дружественный к GitOps и лишённый централизованного управления.
Рекомендации и лучшие практики
На основе полученного опыта я могу дать следующие рекомендации по внедрению External Secrets:
Используйте ClusterSecretStore для общей инфраструктуры: он гибче пространственных SecretStore и упрощает управление IRSA. Выбирайте SSM Parameter Store вместо Secrets Manager ради экономии: если вам не нужны расширенные функции вроде автоматической ротации, Parameter Store бесплатен и вполне достаточен. Ограничивайте область действия IAM-политик: используйте конкретные префиксы путей к параметрам в IAM-политиках, чтобы минимизировать область доступа. Применяйте шаблоны для секретов с несколькими ключами: функция шаблонов позволяет объединять статические и динамические значения в одном секрете. Планируйте начальные развёртывания заранее: приложения, которые хранят пароли в базах данных (например, Grafana), могут потребовать особой обработки при миграции с захардкоженных значений.
Заключение
Переход с захардкоженных секретов на External Secrets Operator с AWS SSM Parameter Store существенно улучшил уровень безопасности моего кластера EKS. Несмотря на то что начальная настройка потребовала некоторой отладки, преимущества централизованного управления секретами, автоматической синхронизации и упрощённой ротации делают это решение оправданным для продакшн-развёртываний Kubernetes.
Связка IRSA, External Secrets Operator и AWS Parameter Store обеспечивает надёжное и экономичное решение, которое органично вписывается в существующие GitOps-процессы на базе ArgoCD. Для команд, запускающих нагрузки на EKS, этот подход представляет значительный шаг вперёд по сравнению с хранением секретов в Git или их ручным управлением в Kubernetes.
Полные файлы конфигурации и код Terraform, упомянутые в этой статье, могут послужить отправной точкой для реализации аналогичных решений по управлению секретами в ваших собственных окружениях.