Kubernetes для самых маленьких. Часть 40: Как Windows-поды входят в Active Directory через gMSA?

Все части серии «Kubernetes для самых маленьких»

В прошлой части на Windows-воркере тестового кластера заработали обычные нагрузки: hello собран под Windows, csi-driver-smb монтирует на узле общие папки, логи и метрики собирают Loki и Prometheus тестового кластера. Но у Windows-приложений есть потребность, которой у Linux-приложений обычно нет: они живут в домене Active Directory. Сайт на IIS пускает пользователей по Windows-аутентификации, служба ходит в SQL Server со строкой Integrated Security=SSPI вместо пароля, отчёты складываются в общую папку, права на которую выданы учётной записи, а не адресу. Всё это держится на Kerberos и доменной учётной записи процесса. А контейнер в домен не входит. Сегодня — как Windows-под получает доменную личность: групповые управляемые учётные записи служб (gMSA), спецификация учётных данных, CRD GMSACredentialSpec и вебхук, который их связывает, право use в RBAC, узел в домене против плагина CCG для узла вне домена, проверка nltest и klist и типичные ошибки.

Как устроен Active Directory, разобрано в части 3 серии про Windows Server, протокол Kerberos — в части 6, DNS домена — в части 7. Домен тот же — corp.example.com с NetBIOS-именем CORP, контроллер dc1.corp.example.com. В сети стенда он стоит по адресу 192.168.10.53 — тот самый DNS-сервер зоны corp.example.com, который в части 16 служил примером stub-зоны. Ещё два сервера домена — файловый srv1.corp.example.com и sql1.corp.example.com с SQL Server, у которого SPN MSSQLSvc/sql1.corp.example.com:1433 зарегистрирован на учётной записи svc-sql, как в части 6. Кластерная часть, как и весь блок, — в тестовом кластере из части 38: команды kubectl и helm идут в контекст lab, а Windows-узел — k8s-lab-win-1 (192.168.10.25).


Зачем поду доменная учётная запись

Встроенная аутентификация Windows (Integrated Windows Authentication) устроена так, что приложение не знает ни одного пароля. Процесс работает в сеансе входа определённой учётной записи, и LSA — подсистема безопасности Windows, процесс lsass.exe — хранит для этого сеанса билеты Kerberos. Когда процесс открывает \\srv1.corp.example.com\reports или подключается к SQL Server, библиотека SSPI берёт билет на нужную службу из кеша сеанса (или получает его у контроллера) и вкладывает его в протокол приложения через механизм согласования SPNEGO: клиент и сервер выбирают Kerberos, а если не вышло — NTLM. Сервер расшифровывает билет своим ключом и узнаёт, кто пришёл и в каких группах состоит. Обмены AS, TGS и AP по полям — в части 6 серии про Windows Server.

Отсюда требование: процесс должен работать от имени учётной записи, которую знает домен. На обычном сервере это либо учётная запись компьютера (у служб, работающих как Local System или Network Service), либо учётная запись службы с паролем.

Варианты без gMSA и почему они плохи:

  • Логин и пароль в Secret (часть 12), а приложение явно аутентифицируется ими: SQL-логин в строке подключения, net use с паролем. Пароль живёт в кластере в обратимом виде, меняется руками во всех местах сразу, а многие программы так не умеют вовсе — пул приложений IIS, например, хранит пароль своей учётной записи в собственной конфигурации.
  • Тома csi-driver-smb (часть 24, на Windows-узлах — прошлая часть): шару монтирует узел учётной записью из Secret. Это решает только доступ к файлам, все поды с томом работают от одной учётной записи, и пароль по-прежнему лежит в кластере.
  • Ввести контейнер в домен нельзя. Вход в домен создаёт в каталоге учётную запись компьютера с собственным паролем, который машина хранит в своём реестре и меняет раз в 30 дней, и защищённый канал Netlogon с контроллером. Контейнер же создаётся из образа, живёт минуты, существует в десяти копиях и исчезает без следа: каждому поду понадобилась бы своя учётная запись компьютера, которую надо создать до запуска и удалить после, а пароль от неё пришлось бы как-то доставить внутрь. Windows-контейнеры в домен не вводятся в принципе.

gMSA снимает все три проблемы: одна доменная учётная запись на много машин, пароль которой генерирует и меняет сам Active Directory и отдаёт только разрешённым хостам. Хост при запуске контейнера получает пароль gMSA и передаёт контейнеру, и в сети контейнер выглядит как компьютер домена по имени gMSA.


Как устроена gMSA

Управляемые учётные записи служб появились в Windows Server 2008 R2 как MSA (standalone MSA): учётная запись для служб одного компьютера с паролем, который этот компьютер меняет сам. В Windows Server 2012 к ним добавились групповые, gMSA: та же идея, но учётной записью одновременно пользуются многие серверы — ферма веб-серверов за балансировщиком, узлы с контейнерами.

Объект в каталоге. gMSA — объект класса msDS-GroupManagedServiceAccount, наследника класса computer. Поэтому у неё всё как у учётной записи компьютера: sAMAccountName со знаком доллара на конце (gmsa-reports$), флаг WORKSTATION_TRUST_ACCOUNT (4096) в userAccountControl, SPN, атрибут dNSHostName. Имя ограничено 15 символами — пределом NetBIOS-имени компьютера. По умолчанию такие объекты создаются в контейнере CN=Managed Service Accounts,DC=corp,DC=example,DC=com.

Пароль никто не задаёт. Его вычисляет контроллер домена. На каждом контроллере, начиная с Windows Server 2012, работает служба распространения ключей — KDS (Microsoft Key Distribution Service, библиотека kdssvc.dll). Она хранит корневой ключ KDS: объект в разделе Configuration (CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=corp,DC=example,DC=com), один на лес и реплицируемый на все контроллеры леса. Из корневого ключа, SID учётной записи и номера текущего интервала времени контроллер детерминированно выводит пароль gMSA. Поэтому любой контроллер в любой момент вычисляет одно и то же значение, пароль не нужно ни хранить, ни реплицировать, ни менять командой: наступил новый интервал — получился новый пароль. Длина интервала — атрибут msDS-ManagedPasswordInterval, по умолчанию 30 дней; задаётся только при создании учётной записи.

Кто может получить пароль. Хост читает пароль из атрибута msDS-ManagedPassword по LDAP. Атрибут вычисляемый: в базе его нет, контроллер строит значение в момент запроса и отдаёт только тем, кто перечислен в атрибуте msDS-GroupMSAMembership (в PowerShell — параметр PrincipalsAllowedToRetrieveManagedPassword), и только по зашифрованному соединению. В ответе — текущий пароль, предыдущий и время до следующей смены: предыдущий нужен, чтобы службы на других хостах, ещё не успевшие перейти на новый пароль, могли расшифровать билеты. Из пароля хост сам выводит ключи Kerberos — AES с солью из realm и имени, как в части 6.

Всем остальным атрибут не возвращается, включая администраторов домена. Это защита списком доступа, а не волшебство: администратор домена может дописать себя в msDS-GroupMSAMembership и после этого прочитать пароль, поэтому само изменение этого атрибута стоит отслеживать аудитом.

 dc1.corp.example.com                               k8s-lab-win-1
 ┌──────────────────────────────────────┐           (K8S-LAB-WIN-1$ в группе k8s-win-nodes)
 │ KDS:                                 │                  │
 │   корневой ключ (раздел              │                  │
 │   Configuration, один на лес)        │                  │
 │   + SID gmsa-reports$                │                  │
 │   + номер 30-дневного интервала      │                  │
 │   = пароль gmsa-reports$             │                  │
 │     (вычисляется, а не хранится)     │                  │
 └──────────────────────────────────────┘                  │
        │                                                  │
        │◄──── 1. Kerberos: TGT для K8S-LAB-WIN-1$ ────────┤
        │◄──── 2. LDAP с шифрованием: msDS-ManagedPassword ┤
        │                                                  │
        │   3. K8S-LAB-WIN-1$ (через группу k8s-win-nodes) │
        │      есть в msDS-GroupMSAMembership?             │
        │                                                  │
        ├──── 4. да: текущий и предыдущий пароль ─────────►│
        │        нет: атрибут не возвращается              │

Получив пароль, хост может сделать всё, что умеет компьютер домена с этим именем: получить TGT от имени gmsa-reports$, установить защищённый канал Netlogon, расшифровывать билеты, выданные на SPN этой учётной записи.


Как под становится gMSA

Цепочка длинная, и в ней участвуют и Kubernetes, и Windows:

 администратор                              kube-apiserver
   │ kubectl apply: Deployment reports,          │
   │ windowsOptions.gmsaCredentialSpecName:      │
   │   gmsa-reports                              │
   └────────────────────────────────────────────►│
                                                 │ 1. mutating-вебхук gMSA:
                                                 │    GMSACredentialSpec gmsa-reports
                                                 │    → JSON в gmsaCredentialSpec пода
                                                 │ 2. validating-вебхук gMSA:
                                                 │    SubjectAccessReview — может ли
                                                 │    SA win-demo/reports делать use
                                                 │    над gmsa-reports?
                                                 ▼
                                               etcd ──► планировщик: k8s-lab-win-1
 ┌─ k8s-lab-win-1 ────────────────────────────────────────────────────────────┐
 │ kubelet ── CRI, credential_spec (JSON) ──► containerd ──► hcsshim ──► HCS  │
 │                                                                        │   │
 │                         3. HCS: контейнер + экземпляр CCG ◄────────────┘   │
 │                                    │                                       │
 │ 4. CCG: пароль gmsa-reports$ ◄─────┼──── LDAP ──── dc1                     │
 │    от имени K8S-LAB-WIN-1$ (узел в домене)                                 │
 │    или учётной записи из плагина (узел вне домена)                         │
 │                                    ▼                                       │
 │ 5. контейнер: SYSTEM и NETWORK SERVICE в сети — CORP\gmsa-reports$         │
 │    ──── Kerberos AS-REQ gmsa-reports$ ────► dc1 (KDC)                      │
 └────────────────────────────────────────────────────────────────────────────┘
  1. Mutating-вебхук. В манифесте пода указано только имя спецификации — поле securityContext.windowsOptions.gmsaCredentialSpecName. Admission-вебхук (внешний HTTPS-сервис, которому kube-apiserver отправляет объект на изменение или проверку перед записью; механизм разобран в части 28) находит объект GMSACredentialSpec с этим именем и дописывает его содержимое — JSON спецификации — в соседнее поле gmsaCredentialSpec. Поле с именем остаётся.
  2. Validating-вебхук. Тот же сервис проверяет, что ServiceAccount пода имеет право use на эту спецификацию, и что JSON в gmsaCredentialSpec совпадает с содержимым объекта — подсунуть свою спецификацию в обход имени нельзя.
  3. kubelet и containerd. kubelet передаёт JSON в CRI-запросе на создание контейнера (поле credential_spec в контексте безопасности Windows-контейнера), containerd кладёт его в OCI-спецификацию, hcsshim — прослойка containerd для Windows (часть 36) — просит HCS (Host Compute Service, служба Windows, которая создаёт контейнеры) создать контейнер вместе с экземпляром Container Credential Guard (CCG).
  4. CCG — компонент Windows (процесс ccg.exe), который получает для контейнера пароль gMSA. Если узел в домене, он читает его от имени учётной записи компьютера узла; если не в домене — от имени доменного пользователя, учётные данные которого ему отдаёт плагин (об этом ниже). Пароль передаётся подсистеме безопасности контейнера; ни в образе, ни в API кластера, ни в файлах пода его нет.
  5. Внутри контейнера gMSA становится учётной записью «компьютера», которым является контейнер. Всё, что ходит в сеть от имени компьютера — процессы, работающие как Local System и Network Service, служба Netlogon, — аутентифицируется в домене как CORP\gmsa-reports$. Локальные пользователи контейнера ContainerAdministrator и ContainerUser остаются локальными и в сеть от имени gMSA не ходят.

Последний пункт — главное, что нужно помнить при сборке образа: приложение должно работать от Network Service или Local System. whoami внутри при этом покажет nt authority\network service, а не имя gMSA — локальная личность процесса не меняется, меняется личность компьютера в сети.


Active Directory: корневой ключ KDS и gMSA

Всё в этом разделе выполняется на dc1 или на машине с модулем ActiveDirectory от имени администратора домена (или учётной записи, которой делегировано право создавать объекты msDS-GroupManagedServiceAccount). Подробнее о модуле — в части 13 серии про Windows Server.

Корневой ключ KDS создаётся один раз на лес, и сначала проверяем, нет ли его уже. Командлет Get-KdsRootKey для этого не всегда годится: на нашем стенде он под учётной записью администратора домена не вернул ничего, хотя ключи в лесу есть. Надёжнее посмотреть сами объекты ключей в разделе Configuration:

PS C:\> Get-ADObject -SearchBase 'CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=corp,DC=example,DC=com' `
          -Filter 'objectClass -eq "msKds-ProvRootKey"' -Properties whenCreated | Select-Object Name, whenCreated

Name                                 whenCreated
----                                 -----------
feb72f87-81f1-5918-f9a4-99db486b8d34 15.05.2019 22:38:10
1ace02ad-d162-1a3f-dd4d-8818bb898217 20.02.2015 13:59:56

Домен стенда старше серии, и ключей в нём два — gMSA работают и с ними, создавать новый не нужно. В новом лесу ключа нет, и его создают командой Add-KdsRootKey -EffectiveImmediately. Несмотря на имя параметра, ключ начинает использоваться только через 10 часов: это время дано репликации, чтобы ключ успел попасть на все контроллеры леса. Если контроллер, у которого ключа ещё нет, получит запрос пароля gMSA, он не сможет его вычислить. В учебном домене с единственным контроллером ожидание можно обойти, задав время действия в прошлом — Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10), — но в домене с несколькими контроллерами так делать нельзя. Попытка создать gMSA раньше этого времени заканчивается ошибкой New-ADServiceAccount : Key does not exist — это не опечатка в имени, а ещё не вступивший в силу ключ.

Группа узлов. Пароль gMSA будут читать Windows-узлы кластера. Выдавать право каждому узлу по отдельности неудобно, поэтому заводим доменно-локальную группу (почему именно доменно-локальную — правило AGDLP из части 3) и OU для узлов кластера, чтобы на них не действовали групповые политики, рассчитанные на обычные серверы:

PS C:\> New-ADOrganizationalUnit -Name Kubernetes -Path 'OU=Servers,DC=corp,DC=example,DC=com'
PS C:\> New-ADGroup -Name k8s-win-nodes -GroupScope DomainLocal -GroupCategory Security `
          -Path 'OU=Kubernetes,OU=Servers,DC=corp,DC=example,DC=com' -Description 'Windows-узлы Kubernetes: чтение паролей gMSA'

gMSA. Одна учётная запись на приложение — не одна на кластер: её права в домене получат все поды, которым она разрешена.

PS C:\> New-ADServiceAccount -Name gmsa-reports -DNSHostName gmsa-reports.corp.example.com `
          -ServicePrincipalNames 'host/gmsa-reports', 'host/gmsa-reports.corp.example.com' `
          -PrincipalsAllowedToRetrieveManagedPassword k8s-win-nodes `
          -KerberosEncryptionType AES128, AES256 -ManagedPasswordIntervalInDays 30
  • -DNSHostName обязателен: у «компьютера» должно быть DNS-имя, хотя записи в DNS для него никто не создаёт.
  • -ServicePrincipalNames — контейнеры сами SPN не регистрируют, поэтому хотя бы host/ задают при создании. Если приложение в поде принимает входящую Windows-аутентификацию, понадобятся SPN для имени, по которому к нему обращаются клиенты, — об этом в разделе про имена.
  • -KerberosEncryptionType AES128, AES256 записывает в msDS-SupportedEncryptionTypes значение 24: билеты для этой учётной записи будут только AES. Без явного значения контроллер может считать, что учётная запись умеет только RC4, а в домене, где RC4 выключен (часть 6), аутентификация не пройдёт.
PS C:\> Get-ADServiceAccount gmsa-reports -Properties DNSHostName, ServicePrincipalNames, userAccountControl, `
          PrincipalsAllowedToRetrieveManagedPassword, msDS-ManagedPasswordInterval, msDS-SupportedEncryptionTypes

DNSHostName                                : gmsa-reports.corp.example.com
msDS-ManagedPasswordInterval               : 30
msDS-SupportedEncryptionTypes              : 24
ObjectClass                                : msDS-GroupManagedServiceAccount
PrincipalsAllowedToRetrieveManagedPassword : {CN=k8s-win-nodes,OU=Kubernetes,OU=Servers,DC=corp,DC=example,DC=com}
SamAccountName                             : gmsa-reports$
ServicePrincipalNames                      : {host/gmsa-reports.corp.example.com, host/gmsa-reports}
SID                                        : S-1-5-21-3623811015-3361044348-30300820-1187
userAccountControl                         : 4096

ObjectClass и userAccountControl: 4096 — те самые признаки «компьютера». А вот пароль администратору домена не достаётся:

PS C:\> (Get-ADServiceAccount gmsa-reports -Properties msDS-ManagedPassword).'msDS-ManagedPassword'
PS C:\>

Пусто: администратора нет в msDS-GroupMSAMembership.


Узел в домене

Есть два способа дать узлу право получать пароль. Первый, исходный и описанный в документации Kubernetes, — ввести Windows-узел в домен и добавить его учётную запись компьютера в группу k8s-win-nodes. Второй — оставить узел вне домена и поставить плагин CCG; он разобран ниже, и там же — почему в тестовом кластере выбран первый.

Узел k8s-lab-win-1 из части 38 сейчас в рабочей группе и разрешает имена через роутер. Для входа в домен ему нужен DNS-сервер, знающий зону corp.example.com и её SRV-записи (как клиент находит контроллер — часть 3). Адрес узла после установки Calico живёт не на физическом адаптере Ethernet, а на виртуальном vEthernet (Ethernet), который HNS создал при подключении адаптера к виртуальному коммутатору (часть 37); DNS задаём на нём:

PS C:\> Get-DnsClientServerAddress -AddressFamily IPv4 | Where-Object ServerAddresses

InterfaceAlias               Interface Address ServerAddresses
                             Index     Family
--------------               --------- ------- ---------------
vEthernet (Ethernet)                14 IPv4    {192.168.10.1}

PS C:\> Set-DnsClientServerAddress -InterfaceAlias 'vEthernet (Ethernet)' -ServerAddresses 192.168.10.53

Внешние имена dc1 разрешает через свои форвардеры (часть 7), так что узел продолжит видеть и ghcr.io, и остальной интернет.

Вход в домен требует перезагрузки, поэтому узел сначала осушают (drain — часть 35):

$ kubectl drain k8s-lab-win-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
node/k8s-lab-win-1 cordoned
Warning: ignoring DaemonSet-managed Pods: calico-system/calico-node-windows-q54h2, kube-system/csi-smb-node-win-tmwv5, kube-system/kube-proxy-windows-z9qdq, monitoring/alloy-windows-f6ns9, monitoring/kps-prometheus-windows-exporter-l45kh
evicting pod win-demo/reports-writer-67b96b59b-g9wrq
evicting pod win-demo/aspnetapp-769559d8ff-vk445
evicting pod win-demo/aspnetapp-769559d8ff-dkp9z
evicting pod win-demo/hello-win-759c899954-9g74w
evicting pod win-demo/hello-win-759c899954-vwdq6
...
node/k8s-lab-win-1 drained
PS C:\> Add-Computer -DomainName corp.example.com -OUPath 'OU=Kubernetes,OU=Servers,DC=corp,DC=example,DC=com' `
          -Credential (Get-Credential CORP\adm-join) -Restart

Что меняется на узле после входа в домен:

  • имя: короткое имя остаётся k8s-lab-win-1, и объект Node в кластере тот же — kubelet регистрирует узел по короткому имени хоста. А основным DNS-суффиксом становится corp.example.com: полное имя узла теперь k8s-lab-win-1.corp.example.com, и узел сам регистрирует его A-запись в зоне домена. Запись k8s-lab-win-1.example.com в зоне example.com остаётся как была;
  • время: источник времени, который в части 38 мы задали вручную (/syncfromflags:manual), вход в домен не меняет, поэтому службу времени переключаем на иерархию домена командой w32tm /config /syncfromflags:domhier /update и перезапуском w32time — после этого она берёт время у контроллера. Для Kerberos это хорошо: допуск расхождения часов — пять минут, а поды живут по часам узла. Устройство времени в домене — тема части 12 серии про Windows Server;
  • групповые политики: на узел начинают действовать политики домена. Правила файрвола, настройки обновлений с автоматической перезагрузкой, ограничения на службы, рассчитанные на обычные серверы, могут сломать узел кластера — например, перезагрузить его без drain. Поэтому узлы живут в своей OU, и наследование политик на ней блокируют, привязывая только то, что узлу действительно нужно (о групповых политиках — часть 24 той же серии):
PS C:\> Set-GPInheritance -Target 'OU=Kubernetes,OU=Servers,DC=corp,DC=example,DC=com' -IsBlocked Yes

Политики учётных записей (пароли, блокировка, сроки билетов Kerberos) задаются на уровне домена и действуют всё равно; блокировка наследования их не отключает.

После перезагрузки добавляем узел в группу и возвращаем его в работу:

PS C:\> Add-ADGroupMember k8s-win-nodes -Members 'K8S-LAB-WIN-1$'
$ kubectl uncordon k8s-lab-win-1
node/k8s-lab-win-1 uncordoned

Тонкость с группой: членство в группах попадает в PAC билета в момент выдачи TGT (часть 6), а узел получил свой TGT при загрузке, когда ещё не был в k8s-win-nodes. До перезагрузки или до продления TGT контроллер будет считать, что права у узла нет. Вместо перезагрузки достаточно сбросить билеты учётной записи компьютера — сеанса SYSTEM с идентификатором 0x3e7 — и проверить доступ к паролю командой Test-ADServiceAccount из модуля ActiveDirectory:

PS C:\> Install-WindowsFeature RSAT-AD-PowerShell
PS C:\> klist -li 0x3e7 purge

Current LogonId is 0:0x5c1a2
Targeted LogonId is 0:0x3e7
        Deleting all tickets:
        Ticket(s) purged!
PS C:\> Test-ADServiceAccount gmsa-reports
True

True — узел получил пароль gMSA. False означает, что узла нет в группе (или группы нет в PrincipalsAllowedToRetrieveManagedPassword), либо билет со старым набором групп ещё в кеше. Проверка Test-ADServiceAccount выполняется на самом узле — она проверяет права того компьютера, на котором запущена, — в его консоли или по RDP. Из сеанса SSH со входом по ключу или из Enter-PSSession она падает с Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running: у такого сеанса нет сетевых учётных данных пользователя (проблема «двойного прыжка»), и модулю ActiveDirectory нечем войти в веб-службы AD на контроллере. Там, где консоли нет, её запускают задачей планировщика от имени SYSTEM — то есть от имени самого узла.

Правило из документации Microsoft: при входе узла в домен узел и gMSA должны быть в одном домене, иначе узел пароль не получит.


Спецификация учётных данных

Credential spec — JSON-документ, который говорит CCG, какую gMSA дать контейнеру и в каком она домене. Секретов в нём нет: только имена, SID и GUID домена. Его генерирует командлет New-CredentialSpec из модуля CredentialSpec (галерея PowerShell) на машине в домене с модулем ActiveDirectory — например, на самом k8s-lab-win-1:

PS C:\> Install-PackageProvider NuGet -Force
PS C:\> Install-Module CredentialSpec -Force
PS C:\> New-CredentialSpec -AccountName gmsa-reports -Path C:\credspecs\gmsa-reports.json
PS C:\> Get-Content C:\credspecs\gmsa-reports.json | ConvertFrom-Json | ConvertTo-Json -Depth 4 -Compress
{"CmsPlugins":["ActiveDirectory"],"DomainJoinConfig":{"Sid":"S-1-5-21-3623811015-3361044348-30300820","MachineAccountName":"gmsa-reports","Guid":"7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9","DnsTreeName":"corp.example.com","DnsName":"corp.example.com","NetBiosName":"CORP"},"ActiveDirectoryConfig":{"GroupManagedServiceAccounts":[{"Name":"gmsa-reports","Scope":"corp.example.com"},{"Name":"gmsa-reports","Scope":"CORP"}]}}

Без -Path файл ложится в каталог CredentialSpecs Docker (C:\ProgramData\Docker\CredentialSpecs), которого на узле с containerd нет. Здесь JSON выведен одной строкой; та же структура в читаемом виде — в YAML объекта кластера ниже. Поля:

Поле Что это
CmsPlugins источник учётных данных; для gMSA всегда ActiveDirectory
DomainJoinConfig.Sid SID домена (без RID)
DomainJoinConfig.MachineAccountName имя gMSA без $ — под этим именем контейнер будет «компьютером» в домене
DomainJoinConfig.Guid GUID домена — тот же, что показывает nltest /dsgetdc
DnsTreeName, DnsName, NetBiosName имя леса, DNS-имя и NetBIOS-имя домена gMSA
GroupManagedServiceAccounts список gMSA, по записи на DNS-имя и на NetBIOS-имя домена

Если в контейнере служба должна работать от имени другой gMSA, чем «компьютер» контейнера, её добавляют параметром -AdditionalAccounts: она попадёт в GroupManagedServiceAccounts, и службу настраивают на неё как на учётную запись службы. Для узла вне домена в ActiveDirectoryConfig добавляется раздел HostAccountConfig — о нём ниже.


CRD и вебхук в кластере

Внешние компоненты для gMSA развивает SIG Windows в репозитории kubernetes-sigs/windows-gmsa: CRD GMSACredentialSpec, admission-вебхук и Helm-чарт, который ставит их вместе. Сама поддержка gMSA в API пода (поля windowsOptions) стабильна с Kubernetes 1.18, а CRD и вебхук остаются внешними — в apiserver их нет.

Ставить можно скриптом deploy-gmsa-webhook.sh из репозитория, который выпускает сертификат вебхука через CSR API кластера и применяет манифесты (так написано в задаче «Configure GMSA» документации Kubernetes), или чартом. Чарт удобнее: он обновляется как любой Helm-релиз (часть 27), а сертификат вебхука по умолчанию выпускает cert-manager — в тестовый кластер он поставлен в части 38 так же, как в части 19 в основной. Чарт создаёт для этого собственный Issuer и Certificate и просит cert-manager вписать CA в конфигурации вебхуков.

# gmsa-values.yaml
replicaCount: 2
nodeSelector:
  kubernetes.io/os: linux
podDisruptionBudget:
  enabled: true
  minAvailable: 1
randomHostname: true
  • nodeSelector — образ вебхука собран под Linux; на Windows-узел под и так не попадёт из-за taint, но правило стенда — явно привязывать Linux-нагрузки к Linux-узлам;
  • две реплики и PDB — вебхук стоит на пути создания каждого пода в кластере, об этом ниже;
  • randomHostname — вебхук задаёт подам с gMSA случайное имя хоста из 15 символов; зачем — в разделе про имена.
$ helm repo add windows-gmsa https://raw.githubusercontent.com/kubernetes-sigs/windows-gmsa/master/charts/repo
$ helm repo update
$ helm search repo windows-gmsa/gmsa
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
windows-gmsa/gmsa       0.14.1          0.14.1          Windows GMSA Configuration
$ helm install gmsa windows-gmsa/gmsa -n kube-system --version 0.14.1 -f gmsa-values.yaml
NAME: gmsa
LAST DEPLOYED: Sat Nov  7 13:15:18 2026
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None

Что появилось в кластере:

  • CRD gmsacredentialspecs.windows.k8s.io (из каталога crds/ чарта): группа windows.k8s.io, версия v1 (хранимая) и старая v1alpha1, ресурс кластерный (scope: Cluster) — спецификация одна на кластер, а не на неймспейс. Схема описывает поле credspec с теми же ключами, что в JSON выше. У CRD есть аннотация api-approved.kubernetes.io со ссылкой на KEP: группы *.k8s.io зарезервированы за проектом и требуют одобренного предложения (часть 28);
  • Deployment вебхука (образ registry.k8s.io/gmsa-webhook/k8s-gmsa-webhook:v0.14.1), Service перед ним и ServiceAccount с правами читать gmsacredentialspecs и создавать localsubjectaccessreviews;
  • MutatingWebhookConfiguration и ValidatingWebhookConfiguration: apiserver отправляет вебхуку создание подов и добавление эфемерных контейнеров (подресурс pods/ephemeralcontainers — через него kubectl debug добавляет отладочный контейнер, и без этого правила такой контейнер обошёл бы проверку).

У конфигураций failurePolicy: Fail: если вебхук недоступен, apiserver отклоняет запрос. Вебхук не знает заранее, Windows-под перед ним или Linux, поэтому ему приходит создание каждого пода в кластере, кроме подов неймспейса самого релиза (kube-system) и неймспейсов с меткой windows.k8s.io/disabled: "true". Обе реплики вебхука упали — в кластере не создаётся ни один под вне kube-system: ни новый, ни пересоздаваемый ReplicaSet'ом. Отсюда две реплики, PDB и привычка смотреть на вебхук первым, когда поды «вдруг перестали создаваться». Риски вебхуков с failurePolicy: Fail разобраны в части 28.


GMSACredentialSpec и право use

JSON из New-CredentialSpec превращается в объект кластера: те же ключи под полем credspec.

apiVersion: windows.k8s.io/v1
kind: GMSACredentialSpec
metadata:
  name: gmsa-reports
credspec:
  CmsPlugins:
    - ActiveDirectory
  DomainJoinConfig:
    Sid: S-1-5-21-3623811015-3361044348-30300820
    MachineAccountName: gmsa-reports
    Guid: 7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9
    DnsTreeName: corp.example.com
    DnsName: corp.example.com
    NetBiosName: CORP
  ActiveDirectoryConfig:
    GroupManagedServiceAccounts:
      - Name: gmsa-reports
        Scope: corp.example.com
      - Name: gmsa-reports
        Scope: CORP

Имя объекта произвольное, но, как у любого объекта Kubernetes, — строчными буквами, цифрами и дефисами. В примерах документации встречается gmsa-WebApp1 со смешанным регистром; в кластере такое имя не создастся, а gmsaCredentialSpecName в поде должен совпадать с именем объекта буква в букву. Удобно называть объект как саму gMSA.

Объект не содержит секретов, поэтому его можно хранить в Git рядом с остальными манифестами. Секрет — пароль gMSA — остаётся в Active Directory, и право его получить определяет домен, а не кластер. Кластер решает другое: какие поды могут попросить узел воспользоваться этим правом. Для этого вебхук проверяет у ServiceAccount пода глагол use — один из особых глаголов RBAC, который существует не для операций API, а для таких проверок (часть 29):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gmsa-reports-user
rules:
  - apiGroups: ["windows.k8s.io"]
    resources: ["gmsacredentialspecs"]
    resourceNames: ["gmsa-reports"]
    verbs: ["use"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: reports
  namespace: win-demo
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: reports-gmsa
  namespace: win-demo
subjects:
  - kind: ServiceAccount
    name: reports
    namespace: win-demo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gmsa-reports-user

Как работает проверка. Вебхук создаёт LocalSubjectAccessReview — запрос к API авторизации «разрешено ли такому-то субъекту такое-то действие» — в неймспейсе пода, от имени system:serviceaccount:win-demo:reports, с атрибутами: глагол use, группа windows.k8s.io, ресурс gmsacredentialspecs, имя gmsa-reports. RBAC отвечает «да» или «нет», и вебхук пропускает или отклоняет под. Поскольку проверка выполняется в неймспейсе пода, работает обычный RoleBinding, хотя сам ресурс кластерный: ClusterRole здесь — шаблон «право на эту gMSA», а RoleBinding выдаёт его в одном неймспейсе. Понадобится gMSA во втором неймспейсе — второй RoleBinding на ту же ClusterRole.

Два следствия, которые стоит понимать.

Проверяется ServiceAccount пода, а не человек, создающий под. Пользователь с правом create pods в win-demo может создать под с serviceAccountName: reports и получить в сети личность gmsa-reports, даже если сам права use не имеет. Граница — неймспейс: право создавать поды в неймспейсе, где есть ServiceAccount с правом на gMSA, равно праву действовать в домене от имени этой gMSA. Неймспейс с gMSA и права в нём выдают с этим расчётом.

Узел может получить пароли всех gMSA, разрешённых его группе. Kubernetes решает, какой под получит gMSA, но Windows на узле этого не знает: захвативший узел (например, через HostProcess-под с правами администратора — часть 36) получит любую из них. Поэтому gMSA выдают минимальные права в домене, а для чувствительных учётных записей заводят отдельную группу узлов и привязывают такие поды к этим узлам через taint и nodeSelector (часть 13).

$ kubectl apply -f gmsa-reports-credspec.yaml -f gmsa-reports-rbac.yaml
gmsacredentialspec.windows.k8s.io/gmsa-reports created
clusterrole.rbac.authorization.k8s.io/gmsa-reports-user created
serviceaccount/reports created
rolebinding.rbac.authorization.k8s.io/reports-gmsa created
$ kubectl auth can-i use gmsacredentialspecs.windows.k8s.io/gmsa-reports -n win-demo --as=system:serviceaccount:win-demo:reports
Warning: resource 'gmsacredentialspecs' is not namespace scoped in group 'windows.k8s.io'

yes
$ kubectl auth can-i use gmsacredentialspecs.windows.k8s.io/gmsa-reports -n win-demo --as=system:serviceaccount:win-demo:default
Warning: resource 'gmsacredentialspecs' is not namespace scoped in group 'windows.k8s.io'

no

Предупреждение kubectl честное — ресурс кластерный, — но проверка всё равно идёт с неймспейсом win-demo, ровно так же, как её делает вебхук.


DNS и сеть: путь от пода до контроллера

Под с gMSA сам находит контроллер домена: служба Netlogon в контейнере выполняет DC Locator — запрашивает в DNS SRV-запись _ldap._tcp.dc._msdcs.corp.example.com, получает список контроллеров и отправляет им LDAP ping (часть 3). DNS-сервер пода — CoreDNS тестового кластера (10.96.0.10), и про corp.example.com он ничего не знает: всё, что не cluster.local, он пересылает роутеру 192.168.10.1. Узел мы перенастроили, но поды спрашивают не узел, а CoreDNS. Поэтому в Corefile тестового кластера добавляем серверный блок для зоны домена — ровно тот, что в части 16 был примером stub-зоны:

corp.example.com:53 {
    errors
    cache 30
    forward . 192.168.10.53
}

kubectl -n kube-system edit configmap coredns, блок — рядом с .:53; плагин reload применит его примерно через минуту. CoreDNS выбирает блок по самой длинной совпадающей зоне, и SRV-записи домена теперь приходят от dc1. Если контроллеров несколько, в forward перечисляют их все. И ещё одно правило из части 37: частичные имена вроде hello.demo из Windows-подов не разрешаются, поэтому и сервисы кластера (<сервис>.<неймспейс>.svc.cluster.local), и ресурсы домена (srv1.corp.example.com, а не srv1) в конфигурации Windows-приложений пишут полными именами.

Сеть. Пакет из пода к 192.168.10.53 выходит за пределы пула адресов подов, и Calico на Windows-узле подменяет адрес источника на адрес узла (natOutgoing пула, часть 37). Контроллер, файловый сервер и SQL Server видят все поды узла как 192.168.10.25. Для Kerberos это не важно: Windows не записывает адрес клиента в билет. Важно для файрволов: правило «пускать только с адресов узлов» на серверах домена работает, а правило по адресу пода — нет.

Какие порты нужны поду (полная таблица портов контроллера — в части 3):

Куда Порт Зачем
dc1 88 TCP и UDP Kerberos: TGT и билеты на службы
dc1 389 TCP и UDP LDAP и LDAP ping при поиске контроллера
dc1 135 TCP и 49152–65535 TCP RPC — механизм удалённого вызова процедур Windows: на 135 Endpoint Mapper сообщает динамический порт службы; через него Netlogon строит защищённый канал
srv1 445 TCP SMB
sql1 1433 TCP SQL Server

DNS в этом списке нет: поды спрашивают CoreDNS, а к dc1 на порт 53 ходят поды CoreDNS на Linux-узлах. Если в win-demo действует запрет исходящего трафика по умолчанию (часть 20), поду с gMSA понадобится разрешение:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: reports-to-domain
  namespace: win-demo
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: reports
  policyTypes: [Egress]
  egress:
    - to:
        - ipBlock: {cidr: 192.168.10.53/32}
      ports:
        - {protocol: TCP, port: 88}
        - {protocol: UDP, port: 88}
        - {protocol: TCP, port: 389}
        - {protocol: UDP, port: 389}
        - {protocol: TCP, port: 135}
        - {protocol: TCP, port: 49152, endPort: 65535}
    - to:
        - ipBlock: {cidr: 192.168.10.54/32}   # srv1
      ports:
        - {protocol: TCP, port: 445}
    - to:
        - ipBlock: {cidr: 192.168.10.55/32}   # sql1
      ports:
        - {protocol: TCP, port: 1433}

Calico применяет политики и на Windows-узлах, через правила VFP.


Под от имени gMSA

Пример — служба отчётов reports: раз в минуту читает список файлов в общей папке \\srv1.corp.example.com\reports и спрашивает у SQL Server на sql1, под каким именем он её видит. Ни логина, ни пароля в манифестах нет.

Права на ресурсы выдаются учётной записи gMSA, как любому компьютеру домена — с долларом на конце. На srv1:

PS C:\> New-Item -ItemType Directory D:\Shares\reports
PS C:\> icacls D:\Shares\reports /grant 'CORP\gmsa-reports$:(OI)(CI)RX'
PS C:\> New-SmbShare -Name reports -Path D:\Shares\reports -ReadAccess 'CORP\gmsa-reports$' -EncryptData $true

На sql1 — вход для gMSA. Прав в базах примеру не нужно: запрос SELECT SUSER_SNAME(), DB_NAME() выполняется и в master. Настоящему приложению выдают права в его базе — CREATE USER ... FOR LOGIN и роль вроде db_datareader.

CREATE LOGIN [CORP\gmsa-reports$] FROM WINDOWS WITH DEFAULT_DATABASE = master;

В настоящем домене gMSA, как и людей, кладут в глобальную группу по роли и выдают права через доменно-локальные группы ресурса (AGDLP, часть 3); здесь права выданы напрямую, чтобы пример был короче. Одна оговорка: членство gMSA в группах, как у любого компьютера, попадает в её билеты при получении TGT, и под увидит новую группу только с новым TGT.

Приложение. Сценарий в ConfigMap и Deployment:

apiVersion: v1
kind: ConfigMap
metadata:
  name: reports-script
  namespace: win-demo
data:
  check.ps1: |
    while ($true) {
        try {
            $files = (Get-ChildItem '\\srv1.corp.example.com\reports').Count
            $conn = New-Object System.Data.SqlClient.SqlConnection `
                'Server=sql1.corp.example.com;Database=master;Integrated Security=SSPI;Encrypt=True;TrustServerCertificate=True'
            $conn.Open()
            $cmd = $conn.CreateCommand()
            $cmd.CommandText = 'SELECT SUSER_SNAME(), DB_NAME()'
            $r = $cmd.ExecuteReader(); $null = $r.Read()
            '{0:s} user={1} files={2} sql_login={3} db={4}' -f (Get-Date),
                [Security.Principal.WindowsIdentity]::GetCurrent().Name, $files, $r.GetString(0), $r.GetString(1)
            $conn.Close()
        } catch {
            '{0:s} error: {1}' -f (Get-Date), $_.Exception.Message
        }
        Start-Sleep -Seconds 60
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reports
  namespace: win-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: reports
  template:
    metadata:
      labels:
        app.kubernetes.io/name: reports
    spec:
      runtimeClassName: windows-2025
      os:
        name: windows
      serviceAccountName: reports
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        windowsOptions:
          gmsaCredentialSpecName: gmsa-reports
          runAsUserName: "NT AUTHORITY\\NETWORK SERVICE"
      containers:
        - name: reports
          image: mcr.microsoft.com/windows/servercore:ltsc2025
          command: ["powershell.exe", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass",
                    "-File", 'C:\scripts\check.ps1']
          resources:
            requests: {cpu: 100m, memory: 256Mi}
            limits: {cpu: "1", memory: 512Mi}
          volumeMounts:
            - name: scripts
              mountPath: 'C:\scripts'
              readOnly: true
      volumes:
        - name: scripts
          configMap:
            name: reports-script

Разберём то, что относится к gMSA:

  • runtimeClassName: windows-2025 — под попадает на k8s-lab-win-1 с нужной сборкой ОС и терпит taint Windows-узла (часть 36); образ servercore:ltsc2025 нужен, потому что в nanoserver нет ни PowerShell 5.1, ни .NET Framework с SqlClient;
  • gmsaCredentialSpecName на уровне пода — gMSA получают все контейнеры пода. Можно задать её и отдельному контейнеру в его securityContext.windowsOptions;
  • runAsUserName: "NT AUTHORITY\\NETWORK SERVICE" — без этого процесс работал бы от пользователя образа: у servercore это ContainerAdministrator, локальная учётная запись, которая в сеть от имени gMSA не ходит. Обратная косая черта в YAML в двойных кавычках удваивается. В своих образах то же самое задают в Dockerfile строкой USER "NT AUTHORITY\NETWORK SERVICE"; для IIS — пулу приложений ставят удостоверение NetworkService (appcmd set AppPool DefaultAppPool -processModel.identityType:NetworkService), для службы Windows — sc.exe config <служба> obj= "NT AUTHORITY\NETWORK SERVICE";
  • os.name: windows и runAsNonRoot: true — неймспейс win-demo для Windows-примеров из части 38 работает на уровне restricted (метки Pod Security — часть 30), и под reports его проходит: для подов с os.name: windows Pod Security не требует Linux-полей (allowPrivilegeEscalation, seccomp, capabilities), а runAsNonRoot проверяет kubelet — на Windows «root» означает ContainerAdministrator, и Network Service проверку проходит;
  • automountServiceAccountToken: false — токен ServiceAccount поду не нужен: ServiceAccount здесь только носитель права use.

Имя хоста и имя gMSA

У пода с gMSA два имени, и путать их не стоит.

Имя хоста — то, что вернёт hostname внутри контейнера. В Kubernetes это имя пода (reports-6c9f7d8b54-k2x9q) или значение spec.hostname, если оно задано. Это локальное имя: им контейнер не представляется домену.

Имя в сети — имя gMSA. С Windows Server 2019 контейнер всегда ходит в сеть под именем своей gMSA, какое бы имя хоста у него ни было. В Windows Server 2016 и версиях 1709 и 1803 было иначе: имя хоста обязано было совпадать с именем gMSA, иначе ломались входящая NTLM-аутентификация и преобразование имён в SID. Отсюда старые рецепты с hostname: gmsa-reports в манифесте — на Windows Server 2019 и новее так делать не нужно и вредно.

Вредно вот почему. Microsoft описывает известную проблему: если несколько контейнеров с одной gMSA работают одновременно с одинаковым именем хоста, они мешают друг другу при обращении к контроллеру — новый контейнер обрывает защищённый канал предыдущего, и аутентификация периодически сбоит, в том числе nltest /sc_verify внутри контейнера. Рецепт — уникальные имена хостов. В Kubernetes имена подов уникальны, но NetBIOS-имя компьютера в Windows ограничено 15 символами, а имена подов одного ReplicaSet различаются только последними пятью: у reports-6c9f7d8b54-k2x9q и reports-6c9f7d8b54-p7m3t первые 15 символов одинаковы. На этот случай в вебхуке есть параметр randomHostname, который мы включили: подам с gMSA, у которых spec.hostname не задан, вебхук ставит имя хоста из 15 случайных символов. Альтернатива — короткие имена Deployment, но надёжнее не зависеть от длины имён.

Входящая аутентификация. Если под принимает Windows-аутентификацию — сайт на IIS с Negotiate, служба с Kerberos, — клиенты обращаются к нему по имени Service или Ingress, например reports.corp.example.com. Клиент строит SPN из имени, которое набрал (часть 6), поэтому на gMSA нужен SPN для этого имени, иначе клиент получит KDC_ERR_S_PRINCIPAL_UNKNOWN и молча откатится на NTLM:

PS C:\> setspn -S HTTP/reports.corp.example.com gmsa-reports$

Обращение к поду или сервису по IP-адресу Kerberos не даёт никогда: SPN с адресом ни у кого не зарегистрирован, и клиент перейдёт на NTLM — протокол «вызов — ответ», при котором сервер пересылает проверку контроллеру через защищённый канал. С gMSA NTLM работает, но без взаимной аутентификации и с лишней нагрузкой на контроллер.


Узлы вне домена: CCG и плагины

Вход узла в домен — лишняя работа при каждом новом узле: учётная запись компьютера, группа, перезагрузка, а при пересоздании узла — чистка старой учётной записи. Поэтому в Windows Server 2019 появился второй режим: gMSA для узла вне домена. Узел остаётся в рабочей группе, а пароль gMSA CCG получает от имени обычного доменного пользователя, учётные данные которого хранятся в хранилище секретов. Достаёт их оттуда плагин CCG — COM-объект (компонент Windows, который загружается по идентификатору класса, CLSID), реализующий интерфейс ICcgDomainAuthCredentials с единственным методом GetPasswordCredentials: получить на вход строку из спецификации и вернуть домен, имя пользователя и пароль.

 Windows-узел вне домена

 HCS: контейнер, credential spec с разделом HostAccountConfig
  │
  ▼
 CCG ── 1. COM-класс PluginGUID ──► плагин ── 2. PluginInput ──► хранилище секретов:
  ▲                                   │                          Secret, Key Vault,
  └─── 3. CORP\svc-ccg-reports ◄──────┘                          Vault ...
          и его пароль

 CCG ── 4. Kerberos: TGT от имени svc-ccg-reports ────────────────► dc1
 CCG ── 5. LDAP: msDS-ManagedPassword gmsa-reports ───────────────► dc1
 CCG ◄─ 6. пароль gmsa-reports$ ─────────────────────────────────── dc1
  │
  ▼
 контейнер: в сети — CORP\gmsa-reports$

В спецификации появляется раздел HostAccountConfig из трёх полей:

"ActiveDirectoryConfig": {
    "GroupManagedServiceAccounts": [ ... ],
    "HostAccountConfig": {
        "PortableCcgVersion": "1",
        "PluginGUID": "{e4781092-f116-4b79-b55e-28eb6a224e26}",
        "PluginInput": "gmsa-system:ccg-reports-credentials"
    }
}
  • PortableCcgVersion — версия механизма, сейчас всегда "1";
  • PluginGUID — CLSID плагина, свой у каждой реализации; в примере — плагин Rancher;
  • PluginInput — строка, смысл которой знает только плагин: у плагина Rancher это <неймспейс>:<имя Secret>, у плагина Azure Key Vault — идентификатор управляемой учётной записи и адрес секрета.

В Active Directory меняется одно: вместо группы с компьютерами узлов в PrincipalsAllowedToRetrieveManagedPassword попадает группа с этим пользователем. Права у пользователя должны быть ровно такие: член группы, которой разрешено читать пароли нужных gMSA, длинный случайный пароль, никаких интерактивных входов. Его учётные данные дают получить пароли gMSA с любой машины, откуда виден контроллер, поэтому защищать их надо не хуже самих паролей.

Готового плагина в Windows нет. Microsoft прямо пишет, что Windows встроенного плагина не содержит и его нужно взять у поставщика или написать. Что есть:

Реализация Где хранятся учётные данные Статус
AKS (Azure) Azure Key Vault; плагин открыт: microsoft/Azure-Key-Vault-Plugin-gMSA встроен в Windows-узлы AKS
AKS на Windows Server (Arc) Secret Kubernetes встроен в дистрибутив, ставится его командлетами
Amazon EKS AWS Secrets Manager, SSM встроен в Windows AMI для EKS
rancher/Rancher-Plugin-gMSA Secret Kubernetes, который на узле раздаёт плагину HostProcess-под «account provider» экспериментальный; в требованиях — Windows Server 2019 и 2022
briantist/CcgVault HashiCorp Vault минимальная реализация

В этом режиме у CCG появляется свой журнал — Microsoft-Windows-Containers-CCG/Admin на узле, — и по нему видно, на каком шаге случился сбой:

Событие Что значит
1 плагин из спецификации загружен
2 пароль gMSA получен, указаны gMSA и плагин
3 не удалось разобрать спецификацию: неверный GUID или поля
4 плагин не загрузился: не установлен или не зарегистрирован
6 плагин загрузился, но не отдал учётные данные: неверный PluginInput или нет доступа к хранилищу
7 пароль gMSA сменился, CCG получает учётные данные заново
8 учётные данные получены, но пароль gMSA с ними не выдаётся: пользователю не разрешено его читать
PS C:\> Get-WinEvent -LogName 'Microsoft-Windows-Containers-CCG/Admin' -MaxEvents 5 | Format-Table TimeCreated, Id, Message -Wrap

Ещё одна разница: узел вне домена не получает настройки DNS и времени от домена. Разрешать имена домена и держать часы в пределах пяти минут от контроллера ему придётся помочь самому.

Что выбрано в тестовом кластере. Там узел введён в домен, и вот почему:

Узел в домене Узел вне домена с плагином
Компоненты только Windows и вебхук из kubernetes-sigs плюс плагин и его доставка учётных данных
Секрет в кластере нет: пароль узла хранит сам узел учётные данные пользователя в хранилище секретов
Новый узел вход в домен, группа, перезагрузка плагин ставится на узел автоматически
Время и DNS от домена настраивать самим
Политики домена действуют, нужна своя OU не действуют
Зрелость на kubeadm основной сценарий документации Kubernetes реализации привязаны к облакам или экспериментальны

Для одного-двух постоянных Windows-узлов вход в домен проще и обходится без стороннего кода на узле. В облаке, где Windows-узлы создаются и удаляются автомасштабированием, выигрывает режим без домена — там плагин уже встроен поставщиком.


Смотрим руками

Компоненты в кластере:

$ kubectl get crd gmsacredentialspecs.windows.k8s.io
NAME                                 CREATED AT
gmsacredentialspecs.windows.k8s.io   2026-11-07T10:15:18Z
$ kubectl api-resources --api-group=windows.k8s.io
NAME                  SHORTNAMES   APIVERSION          NAMESPACED   KIND
gmsacredentialspecs                windows.k8s.io/v1   false        GMSACredentialSpec
$ kubectl get gmsacredentialspecs
NAME           AGE
gmsa-reports   38m
$ kubectl -n kube-system get pods -o wide | grep gmsa
gmsa-5bfdf5ff88-kf4sc   1/1   Running   0   39m   172.21.172.16   k8s-lab-node-2   <none>   <none>
gmsa-5bfdf5ff88-kzcf7   1/1   Running   0   39m   172.21.211.28   k8s-lab-node-1   <none>   <none>
$ kubectl get mutatingwebhookconfigurations,validatingwebhookconfigurations | grep gmsa
mutatingwebhookconfiguration.admissionregistration.k8s.io/gmsa     1          39m
validatingwebhookconfiguration.admissionregistration.k8s.io/gmsa   1          39m
$ kubectl -n kube-system get certificates,issuers | grep gmsa
certificate.cert-manager.io/gmsa      True    gmsa-server-cert   39m
certificate.cert-manager.io/gmsa-ca   True    gmsa-root-ca       39m
issuer.cert-manager.io/gmsa   True    39m

Реплики вебхука разошлись по двум Linux-воркерам, а на control plane их не пускает taint: отказ одного узла вебхук переживёт, а PDB не даст drain выселить обе реплики сразу. Сертификат вебхука выпустил cert-manager — чарт создал для этого собственный CA (gmsa-root-ca) и Issuer. Применяем reports и смотрим, что сделал mutating-вебхук:

$ kubectl apply -f reports.yaml
configmap/reports-script created
deployment.apps/reports created
$ kubectl -n win-demo get pods -l app.kubernetes.io/name=reports -o wide
NAME                       READY   STATUS    RESTARTS   AGE     IP             NODE            NOMINATED NODE   READINESS GATES
reports-56c8c6dc7f-8ccbj   1/1     Running   0          7m32s   172.21.41.47   k8s-lab-win-1   <none>           <none>
reports-56c8c6dc7f-skpdb   1/1     Running   0          7m29s   172.21.41.48   k8s-lab-win-1   <none>           <none>
$ kubectl -n win-demo get pod reports-56c8c6dc7f-8ccbj -o jsonpath='{.spec.hostname}{"\n"}{.spec.securityContext.windowsOptions}{"\n"}'
aa0a06c0-caeb-4
{"gmsaCredentialSpec":"{\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"gmsa-reports\",\"Scope\":\"corp.example.com\"},…]},\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{…}}","gmsaCredentialSpecName":"gmsa-reports","runAsUserName":"NT AUTHORITY\\NETWORK SERVICE"}

В манифесте было только имя, а в объекте пода — весь JSON спецификации: его и получит kubelet. spec.hostname — 15 случайных символов от randomHostname. Журнал приложения:

$ kubectl -n win-demo logs reports-56c8c6dc7f-8ccbj --tail=2
2026-11-07T13:47:32 user=NT AUTHORITY\NETWORK SERVICE files=3 sql_login=CORP\gmsa-reports$ db=master
2026-11-07T13:48:32 user=NT AUTHORITY\NETWORK SERVICE files=3 sql_login=CORP\gmsa-reports$ db=master

Это и есть суть механизма в одной строке: локально процесс — NETWORK SERVICE, а SQL Server видит вход CORP\gmsa-reports$. Заглянем внутрь. kubectl exec запускает процесс от того же пользователя, что и контейнер, — то есть тоже Network Service:

$ kubectl -n win-demo exec -it reports-56c8c6dc7f-8ccbj -- powershell
PS C:\> whoami
nt authority\network service
PS C:\> hostname
aa0a06c0-caeb-4
PS C:\> nltest /dsgetdc:corp.example.com
           DC: \\dc1.corp.example.com
      Address: \\192.168.10.53
     Dom Guid: 7a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9
     Dom Name: corp.example.com
  Forest Name: corp.example.com
 Dc Site Name: Office
Our Site Name: Office
        Flags: PDC GC DS LDAP KDC TIMESERV GTIMESERV WRITABLE DNS_DC DNS_DOMAIN DNS_FOREST CLOSE_SITE FULL_SECRET WS DS_8 DS_9 DS_10 KEYLIST DS_13
The command completed successfully

DC Locator из пода нашёл контроллер — значит, работают блок corp.example.com в CoreDNS и путь до dc1. Билеты сеанса Network Service (0x3e4), общего для приложения и нашей оболочки:

PS C:\> klist

Current LogonId is 0:0x3e4

Cached Tickets: (7)

#0>     Client: gmsa-reports$ @ CORP.EXAMPLE.COM
        Server: krbtgt/CORP.EXAMPLE.COM @ CORP.EXAMPLE.COM
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x60a10000 -> forwardable forwarded renewable pre_authent name_canonicalize
        ...
#1>     Client: gmsa-reports$ @ CORP.EXAMPLE.COM
        Server: krbtgt/CORP.EXAMPLE.COM @ CORP.EXAMPLE.COM
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
        Start Time: 11/7/2026 13:47:29 (local)
        End Time:   11/7/2026 23:47:29 (local)
        Renew Time: 11/14/2026 13:47:29 (local)
        Session Key Type: AES-256-CTS-HMAC-SHA1-96
        Cache Flags: 0x1 -> PRIMARY
        Kdc Called: dc1.corp.example.com
...
#3>     Client: gmsa-reports$ @ CORP.EXAMPLE.COM
        Server: MSSQLSvc/sql1.corp.example.com:1433 @ CORP.EXAMPLE.COM
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        ...
#4>     Client: gmsa-reports$ @ CORP.EXAMPLE.COM
        Server: cifs/srv1.corp.example.com @ CORP.EXAMPLE.COM
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        ...

Client: gmsa-reports$ во всех билетах: TGT получен от имени gMSA, а по нему — билеты на SMB srv1 и на SQL Server. Остальные билеты в кеше (ldap/, GC/ и cifs/ контроллера) нужны самой Windows в контейнере — службе Netlogon и поиску в каталоге. Билет MSSQLSvc/... доказывает, что вход в SQL Server прошёл по Kerberos, а не по NTLM: при откате на NTLM билета на службу в кеше не было бы. Флаг initial у TGT — получен обменом AS, то есть контейнер аутентифицировался паролем gMSA, который передал ему CCG. klist get krbtgt запросит TGT заново и ответит A ticket to krbtgt has been retrieved successfully, если контроллер доступен.

Защищённый канал — постоянное аутентифицированное RPC-соединение службы Netlogon с контроллером, через которое «компьютер» проверяет NTLM-входы и ищет имена и SID, — проверяется nltest /sc_verify. Этой и другим управляющим командам Netlogon нужны права администратора в контейнере, а reports работает от Network Service. Для диагностики нужен временный под с той же gMSA, но от пользователя образа по умолчанию — ContainerAdministrator. В win-demo уровня restricted его не пустит Pod Security:

Error from server (Forbidden): error when creating "gmsa-debug.yaml": pods "gmsa-debug" is forbidden: violates PodSecurity "restricted:v1.37": runAsNonRoot != true (pod or container "shell" must set securityContext.runAsNonRoot=true)

Поэтому отладочный под живёт в отдельном неймспейсе уровня baseline, со своим ServiceAccount и своим RoleBinding на ту же ClusterRole — ровно тот случай «gMSA во втором неймспейсе», о котором шла речь выше:

apiVersion: v1
kind: Namespace
metadata:
  name: win-debug
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.37
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gmsa-debug
  namespace: win-debug
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gmsa-debug
  namespace: win-debug
subjects:
  - kind: ServiceAccount
    name: gmsa-debug
    namespace: win-debug
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gmsa-reports-user
---
apiVersion: v1
kind: Pod
metadata:
  name: gmsa-debug
  namespace: win-debug
spec:
  runtimeClassName: windows-2025
  os: {name: windows}
  serviceAccountName: gmsa-debug
  securityContext:
    windowsOptions:
      gmsaCredentialSpecName: gmsa-reports
  containers:
    - name: shell
      image: mcr.microsoft.com/windows/servercore:ltsc2025
      command: ["powershell.exe", "-Command", "Start-Sleep -Seconds 86400"]
$ kubectl apply -f gmsa-debug.yaml
namespace/win-debug created
serviceaccount/gmsa-debug created
rolebinding.rbac.authorization.k8s.io/gmsa-debug created
pod/gmsa-debug created
$ kubectl -n win-debug exec gmsa-debug -- whoami
user manager\containeradministrator
$ kubectl -n win-debug exec gmsa-debug -- nltest /sc_verify:corp.example.com
Flags: 800000b0 HAS_IP  HAS_TIMESERV  Authentication Service: Kerberos
Trusted DC Name \\dc1.corp.example.com
Trusted DC Connection Status Status = 0 0x0 NERR_Success
Trust Verification Status = 0 0x0 NERR_Success
The command completed successfully
$ kubectl delete namespace win-debug
namespace "win-debug" deleted

NERR_Success в обеих строках: защищённый канал с dc1 установлен, доверие к домену подтверждено, и проверка шла по Kerberos. Заодно этот неймспейс иллюстрирует сказанное о границе прав: личность gmsa-reports в сети получил любой под с ServiceAccount, которому выдан RoleBinding, — права use у того, кто создавал под, никто не спрашивал.

Со стороны файлового сервера сеанс выглядит так:

PS C:\> Get-SmbSession | Where-Object ClientUserName -like '*gmsa*' | Select-Object ClientComputerName, ClientUserName, Dialect, NumOpens

ClientComputerName ClientUserName      Dialect NumOpens
------------------ --------------      ------- --------
192.168.10.25      CORP\gmsa-reports$  3.1.1          1
192.168.10.25      CORP\gmsa-reports$  3.1.1          1

Пользователь — gMSA, адрес — узла: оба пода reports приходят с 192.168.10.25 после NAT, каждый своим сеансом. SMB 3.1.1, и шара с шифрованием.

И отказ вебхука — под в win-demo с ServiceAccount default, у которого права use нет:

$ kubectl -n win-demo apply -f gmsa-test.yaml
Error from server: error when creating "gmsa-test.yaml": admission webhook "admission-webhook.windows-gmsa.sigs.k8s.io" denied the request: service account "default" is not authorized to `use` GMSA cred spec "gmsa-reports"

Типичные ошибки

Поды не создаются, в том числе Linux-поды. Событие ReplicaSet FailedCreate с текстом failed calling webhook "admission-webhook.windows-gmsa.sigs.k8s.io" — вебхук недоступен, а failurePolicy: Fail не пропускает поды. Смотреть поды вебхука в kube-system, его Service и сертификат (cert-manager должен был выпустить Certificate и вписать CA в конфигурации вебхуков).

service account "…" is not authorized to `use` GMSA cred spec "…". Нет RoleBinding в неймспейсе пода, ServiceAccount в манифесте не тот (по умолчанию default) или в ClusterRole опечатка в resourceNames. Проверка — kubectl auth can-i use gmsacredentialspecs.windows.k8s.io/<имя> -n <неймспейс> --as=system:serviceaccount:<неймспейс>:<sa>.

admission webhook "admission-webhook.windows-gmsa.sigs.k8s.io" denied the request: cred spec gmsa-missing does not exist. Имя в gmsaCredentialSpecName не совпадает с именем объекта GMSACredentialSpec — регистр тоже считается.

The Pod "…" is invalid: spec: Forbidden: pod updates may not change fields other than spec.containers[*].image, … при попытке поменять gMSA у работающего пода. Настройки gMSA, как и почти вся спецификация пода, после создания не меняются — отказывает сам API, до вебхука дело не доходит. Их меняют в шаблоне Deployment, и поды пересоздаются.

Login failed for user 'CORP\gmsa-reports$' от SQL Server. Kerberos при этом отработал — в klist пода есть билет MSSQLSvc/sql1.corp.example.com:1433, — а на SQL Server нет входа для gMSA. Сообщение не путать с ANONYMOUS LOGON ниже: здесь сервер узнал gMSA, но не пустил.

Под висит в ContainerCreating, в событиях ошибка hcs::CreateComputeSystem. CCG не получил пароль gMSA. На узле в домене — Test-ADServiceAccount <gmsa> на самом узле: False значит, что узла нет в группе из PrincipalsAllowedToRetrieveManagedPassword, группа не та или узел ещё держит TGT со старым списком групп (klist -li 0x3e7 purge). На узле вне домена — журнал Microsoft-Windows-Containers-CCG/Admin, события 3–8 из таблицы выше.

Getting DC name failed: Status = 1355 0x54b ERROR_NO_SUCH_DOMAIN от nltest /dsgetdc в поде. Под не находит контроллер: в CoreDNS нет блока для corp.example.com, или из пода не дойти до dc1 (NetworkPolicy, файрвол контроллера, маршрут). Проверить по шагам: Resolve-DnsName -Type SRV _ldap._tcp.dc._msdcs.corp.example.com и Test-NetConnection dc1.corp.example.com -Port 389 изнутри пода.

I_NetLogonControl failed: Status = 1722 0x6ba RPC_S_SERVER_UNAVAILABLE от nltest /sc_verify или nltest /query. Если gMSA к поду вообще не применилась (нет gmsaCredentialSpecName или вебхук не дописал JSON), контейнер — обычный компьютер вне домена, и любая команда Netlogon отвечает так; первым делом смотреть .spec.securityContext.windowsOptions пода. Если применилась — не проходит RPC до контроллера: порт 135 и динамический диапазон. Документация Kubernetes предлагает при сбое защищённого канала команду nltest /sc_reset:corp.example.com внутри контейнера, а если помогает только она — разбираться с сетью, а не автоматизировать перезапуск Netlogon.

Периодические сбои аутентификации у одних реплик и не у других. Одинаковые имена хостов у подов с одной gMSA — см. раздел про имена; включить randomHostname или проверить, что в манифесте не задан spec.hostname.

Access is denied к шаре или Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON' в SQL Server. Приложение работает от ContainerUser или ContainerAdministrator, а не от Network Service или Local System, и в сеть идёт не от имени gMSA. Или права выданы без доллара — CORP\gmsa-reports вместо CORP\gmsa-reports$: такой учётной записи нет, либо, хуже, есть пользователь с таким же именем без доллара. Microsoft отдельно советует проверить, что в домене нет другого объекта с именем, совпадающим с gMSA: Get-ADObject -Filter 'sAMAccountName -like "gmsa-reports*"'.

KRB_AP_ERR_SKEW в журнале Kerberos. Часы узла разошлись с контроллером больше чем на пять минут. Поды живут по часам узла; на узле в домене — w32tm /query /source должен показывать контроллер.

Шара доступна по IP, но не по имени. Документация Kubernetes описывает этот случай для подов с gMSA и предлагает на узлах параметр реестра HNS, после которого поды нужно пересоздать:

PS C:\> reg add "HKLM\SYSTEM\CurrentControlSet\Services\hns\State" /v EnableCompartmentNamespace /t REG_DWORD /d 1

Windows-поды теперь ходят в домен от имени gMSA: пароль хранит и меняет Active Directory, получает узел, а кластер решает через RBAC, какому поду его доверить. На этом блок о гибридном кластере закончен — от устройства Windows-узла и его сети до рабочих нагрузок и доменной аутентификации. Но и GMSACredentialSpec, и RoleBinding, и Deployment reports, как и всё до них, попали в кластер командой kubectl apply с рабочей станции.


Источники


Дальше — последняя основная часть, GitOps на Argo CD: как сделать единственным источником правды о кластере репозиторий Git, из которого изменения в кластер забирает контроллер внутри него. Объекты Application, синхронизация и обнаружение ручных расхождений, волны и хуки синхронизации, ApplicationSet и проекты, что делать с секретами, которым не место в Git, и стек hello с overlays для dev и prod — снова в основном учебном кластере.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.