External Secrets Operator + AWS SSM в Kubernetes EKS

Введение

При развёртывании приложений в 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, упомянутые в этой статье, могут послужить отправной точкой для реализации аналогичных решений по управлению секретами в ваших собственных окружениях.

© 2026 meganuke