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) │
└────────────────────────────────────────────────────────────────────────────┘
- Mutating-вебхук. В манифесте пода указано только имя спецификации — поле
securityContext.windowsOptions.gmsaCredentialSpecName. Admission-вебхук (внешний HTTPS-сервис, которому kube-apiserver отправляет объект на изменение или проверку перед записью; механизм разобран в части 28) находит объектGMSACredentialSpecс этим именем и дописывает его содержимое — JSON спецификации — в соседнее полеgmsaCredentialSpec. Поле с именем остаётся. - Validating-вебхук. Тот же сервис проверяет, что ServiceAccount пода имеет право
useна эту спецификацию, и что JSON вgmsaCredentialSpecсовпадает с содержимым объекта — подсунуть свою спецификацию в обход имени нельзя. - kubelet и containerd. kubelet передаёт JSON в CRI-запросе на создание контейнера (поле
credential_specв контексте безопасности Windows-контейнера), containerd кладёт его в OCI-спецификацию, hcsshim — прослойка containerd для Windows (часть 36) — просит HCS (Host Compute Service, служба Windows, которая создаёт контейнеры) создать контейнер вместе с экземпляром Container Credential Guard (CCG). - CCG — компонент Windows (процесс
ccg.exe), который получает для контейнера пароль gMSA. Если узел в домене, он читает его от имени учётной записи компьютера узла; если не в домене — от имени доменного пользователя, учётные данные которого ему отдаёт плагин (об этом ниже). Пароль передаётся подсистеме безопасности контейнера; ни в образе, ни в API кластера, ни в файлах пода его нет. - Внутри контейнера 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: windowsPod 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 с рабочей станции.
Источники
- Kubernetes: Configure GMSA for Windows Pods and containers, Configure RunAsUserName for Windows pods and containers, Pod Security Standards — раздел про
spec.os.name. - kubernetes-sigs/windows-gmsa: CRD, вебхук, Helm-чарт
gmsaи егоvalues.yaml. - Microsoft: Create gMSAs for Windows containers, Configure your app to use a gMSA, Troubleshoot gMSAs for Windows containers, Group Managed Service Accounts overview, ICcgDomainAuthCredentials, gMSA в AKS на Windows Server.
- Плагины CCG: Azure Key Vault Plugin, Rancher-Plugin-gMSA, CcgVault, домен без входа узлов в Amazon EKS.
Дальше — последняя основная часть, GitOps на Argo CD: как сделать единственным источником правды о кластере репозиторий Git, из которого изменения в кластер забирает контроллер внутри него. Объекты Application, синхронизация и обнаружение ручных расхождений, волны и хуки синхронизации, ApplicationSet и проекты, что делать с секретами, которым не место в Git, и стек hello с overlays для dev и prod — снова в основном учебном кластере.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.