Windows Server для самых маленьких. Часть 30: Как работает единый вход через Kerberos?

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

В части 29 alice вошла на lnx1 с паролем, и после входа у неё появился TGT. В части 6 было описано, что с TGT можно получить билет на любую службу, не предъявляя пароль ещё раз. На Windows это происходит само: \\srv1\share, RDP, WinRM, LDAP — пароль не спрашивают никогда. На Linux и между платформами то же самое возможно, но не включено по умолчанию и требует понимания двух вещей: как приложение добирается до Kerberos и как служба получает свой ключ. Эта часть — о едином входе с Linux-стороны: SSH, веб, базы данных и всё, что говорит GSSAPI.


GSSAPI и Negotiate

Приложения не работают с Kerberos напрямую. Между ними — GSSAPI (Generic Security Services API, RFC 2743): стандартный интерфейс, за которым может стоять Kerberos (механизм описан в RFC 4121), NTLM или что-то ещё. Приложение просит «установить контекст безопасности с host/lnx1.corp.example.com», библиотека сама получает билет, собирает AP-REQ и возвращает токен, который приложение отправляет серверу по своему протоколу; сервер отдаёт токен своей библиотеке, та проверяет его ключом из keytab и возвращает имя клиента. Приложению всё равно, что внутри токена: оно только переносит байты туда и обратно.

Поверх GSSAPI — SPNEGO (RFC 4178), он же Negotiate: согласование механизма, когда стороны могут знать разные. Первый токен клиента перечисляет механизмы, которые он умеет, и сразу несёт токен предпочтительного (обычно Kerberos), сервер отвечает, какой выбран. Windows использует SPNEGO везде, Linux — там, где нужно совместимо с Windows. Где это встроено:

Протокол Механизм Как выглядит
SSH gssapi-with-mic (RFC 4462) GSSAPIAuthentication yes с обеих сторон
HTTP Negotiate (RFC 4559) заголовок WWW-Authenticate: Negotiate, токен в Authorization
LDAP SASL GSSAPI или GSS-SPNEGO часть 5
SMB SPNEGO в Session Setup Samba с security = ads, smbclient --use-kerberos=required
PostgreSQL gss в pg_hba.conf SPN postgres/host
NFSv4 sec=krb5, krb5i, krb5p SPN nfs/host, rpc.gssd
RPC и DCOM на Windows Negotiate всё из части 15

SASL в строке LDAP — рамка подключаемых механизмов аутентификации внутри протокола: клиент называет механизм (GSSAPI — Kerberos через GSSAPI, GSS-SPNEGO — то же через SPNEGO) и обменивается его токенами в операции Bind. SMB — протокол файловых ресурсов Windows, его реализация на Linux — Samba. PostgreSQL — сервер баз данных со своим протоколом поверх TCP 5432. NFS — сетевая файловая система Unix, в версии 4 с аутентификацией через RPCSEC_GSS; об этих трёх подробнее ниже. RPC и DCOM — удалённый вызов процедур Windows, на котором держатся оснастки и WMI.

Библиотека на Linux — MIT Kerberos (libkrb5, libgssapi_krb5), на Windows — SSPI, и они совместимы на уровне протокола, потому что Kerberos один.


SSH

Linux → Linux. У lnx1 в keytab есть host/lnx1.corp.example.com с момента вступления; sshd нужно разрешить механизм:

$ sudo tee /etc/ssh/sshd_config.d/60-gssapi.conf <<'EOF'
GSSAPIAuthentication yes
GSSAPICleanupCredentials yes
EOF
$ sudo systemctl reload ssh

GSSAPICleanupCredentials yes (это и умолчание) удаляет пересланный TGT пользователя, когда сеанс закрывается. Ещё одно умолчание, GSSAPIStrictAcceptorCheck yes, требует, чтобы билет был выписан именно на host/<имя этого сервера>: билет на другое имя из того же keytab sshd не примет.

На клиенте, где у alice есть TGT (вход с паролем через sssd или kinit alice), вход выглядит как обычный ssh:

alice@lnx2:~$ ssh -K lnx1.corp.example.com hostname

-K включает GSSAPI и делегирование на один вызов; постоянно — в ~/.ssh/config или /etc/ssh/ssh_config.d/:

Host *.corp.example.com
    GSSAPIAuthentication yes
    GSSAPIDelegateCredentials yes

Клиент OpenSSH пробует GSSAPI первым (PreferredAuthentications по умолчанию начинается с gssapi-with-mic), а ключ и пароль остаются запасными методами там, где билета нет. Как выглядит обмен в ssh -v, показано в «Смотрим руками».

Делегирование здесь — то же, что в части 16: вместе с билетом на сервер клиент пересылает TGT, он уезжает на lnx1, и там klist -f покажет у TGT флаг f (forwarded), после чего с lnx1 можно идти дальше — на \\srv1, на lnx3, в LDAP — от имени alice. И тот же риск: TGT пользователя на сервере, и любой, кто там root, может им воспользоваться. Клиент MIT Kerberos пересылает TGT, когда его просят, независимо от того, что думает о сервере каталог (enforce_ok_as_delegate = false по умолчанию в krb5.conf). Клиент Windows — нет: по спецификации MS-KILE он пересылает TGT только серверу, чей билет несёт флаг ok_as_delegate, а KDC ставит его учётным записям с доверием для неограниченного делегирования (часть 6). Контроллеры домена такое доверие имеют, LNX1$ — нет.

Windows → Linux. Встроенный клиент Windows из части 16 с GSSAPIAuthentication yes в ~\.ssh\config входит на lnx1 билетом из сеанса Windows: SPN host/lnx1.corp.example.com у LNX1$ есть, keytab совпадает, и Windows-клиенту всё равно, что за sshd на той стороне. Имя пользователя — -l alice или User alice в той же секции: без него клиент подставит corp\alice, а Linux-сервер такого пользователя не знает. Делегирование (-K) с Windows на lnx1 не произойдёт по причине выше: клиент честно попросит, получит отказ своей же библиотеки, и в сеансе на lnx1 билета не будет. Включать LNX1$ неограниченное делегирование ради этого не стоит — тогда TGT каждого, кто входит на сервер, остаётся на нём; если на lnx1 нужен билет, проще kinit в сеансе.

Linux → Windows. ssh -K dc1.corp.example.com к Windows OpenSSH с включённым GSSAPI работает так же: SPN host/dc1.corp.example.com у контроллера есть всегда, а sshd Windows проверяет билет через SSPI.


Кеши и работа без пароля

На Linux TGT лежит в кеше учётных данных, и его тип задаётся в krb5.conf (default_ccache_name). Три варианта: FILE:/tmp/krb5cc_%{uid} — файл, классика, один на пользователя; KEYRING:persistent:%{uid} — в ключах ядра, не виден по файловой системе; KCM: — демон (у sssd это sssd-kcm), который хранит кеши для всех пользователей и умеет несколько кешей на пользователя. RHEL и Fedora с sssd используют KCM: пакет sssd-kcm кладёт default_ccache_name = KCM: в /etc/krb5.conf.d/. Ubuntu по умолчанию остаётся на файлах: при входе с паролем pam_sss кладёт TGT в /tmp/krb5cc_<uid>_<случайный суффикс> и выставляет в сеансе KRB5CCNAME, а без него библиотека ищет /tmp/krb5cc_<uid>. klist показывает текущий кеш, klist -A — все, kdestroy — удалить, KRB5CCNAME — переключить.

Билет появляется при входе с паролем через pam_sss и не появляется при входе по SSH-ключу или по GSSAPI без делегирования: пароля у sssd в этот момент нет, и TGT взять неоткуда. Это первая причина «я вошёл, а klist пуст» — как это выглядит, тоже в «Смотрим руками». Выход — kinit руками или вход с делегированием.

Службам, которым нужны свои билеты для планового обращения к каталогу или к SMB-ресурсу, пароль не нужен: ключ из keytab заменяет его:

$ sudo kinit -k -t /etc/krb5.keytab 'LNX1$@CORP.EXAMPLE.COM'
$ sudo klist

klist после этого покажет основным принципалом LNX1$@CORP.EXAMPLE.COM и один билет krbtgt/CORP.EXAMPLE.COM, выданный на срок по политике домена (по умолчанию 10 часов). Так резервное копирование по SMB или скрипт, читающий LDAP, работает от имени машины; в cron перед командой — kinit -k, или k5start для долгоживущих процессов, или gssproxy, который отдаёт службам билеты, не давая им keytab.


SPN и keytab для служб

Служба на Linux, в которую будут входить по Kerberos, должна иметь принципал класс/имя@REALM в каталоге и его ключ в keytab, доступном службе.

Для имени самой машины многие классы уже есть, хотя в servicePrincipalName их не видно. Атрибут sPNMappings объекта CN=Directory Service,CN=Windows NT,CN=Services в разделе конфигурации объявляет десятки классов синонимами host, и http среди них: на запрос HTTP/lnx1.corp.example.com KDC выдаст билет на ключ LNX1$, хотя у учётной записи записан только host/lnx1.corp.example.com. Для сайта под другим именем (intranet.corp.example.com) SPN нужен явный. Два способа.

На учётной записи машины. SPN добавляется к LNX1$, ключ — тот же, что у host/, и adcli заодно дописывает записи с новым именем в keytab:

$ sudo adcli update --add-service-principal=HTTP/intranet.corp.example.com --verbose
$ sudo klist -k | grep HTTP

klist -k после этого покажет HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM с тем же KVNO, что у host/, по строке на каждый тип шифрования. SPN можно добавить и с Windows-стороны — setspn -S HTTP/intranet.corp.example.com lnx1, — но это меняет только каталог, записи в keytab так не появится; adcli делает и то, и другое.

Keytab машины принадлежит root; веб-серверу нужен отдельный файл с нужными ключами: ktutil с rkt /etc/krb5.keytab, delent лишних, wkt /etc/apache2/http.keytab, и права chown www-data, chmod 0400. У такой копии есть срок годности: sssd меняет пароль машины раз в 30 дней (ad_maximum_machine_account_password_age), KVNO в каталоге растёт, /etc/krb5.keytab обновляется, а копия — нет, и через месяц веб-сервер перестаёт расшифровывать билеты. Копию нужно пересобирать после каждой смены пароля; надёжнее отдать ключ службе через gssproxy или завести для неё отдельную учётную запись.

На отдельной учётной записи. Для служб, которые живут на нескольких узлах за балансировщиком (один SPN должен быть у одного принципала, а ключ — на всех узлах), или которым нужны собственные права, — учётная запись домена svc-intranet, и keytab для неё делается на Windows:

PS C:\> New-ADUser -Name svc-intranet -SamAccountName svc-intranet -UserPrincipalName HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM -Enabled $true -AccountPassword (Read-Host -AsSecureString) -PasswordNeverExpires $true -KerberosEncryptionType AES128, AES256
PS C:\> ktpass -princ HTTP/intranet.corp.example.com@CORP.EXAMPLE.COM -mapuser CORP\svc-intranet -crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL -pass * -out C:\Temp\intranet.keytab

ktpass регистрирует SPN, ставит UPN, меняет пароль учётной записи на тот, что указан в -pass (звёздочка — спросить), и пишет ключ в файл; номер версии ключа vno после этого увеличивается, и старые keytab с этой учётной записи перестают работать. Файл везут на узлы с теми же правами, что выше. На Linux то же умеет msktutil, создавая учётную запись и keytab сразу с нужной стороны.

Групповые управляемые учётные записи из части 13 sssd и adcli не поддерживают: для Linux-служб остаются машинный keytab и обычная учётная запись с длинным паролем и плановой сменой через ktpass или msktutil --auto-update.


Веб-приложение

Apache с модулем mod_auth_gssapi превращает любое приложение в приложение с единым входом: браузер пользователя домена предъявляет билет, Apache проверяет его keytab и передаёт приложению имя в REMOTE_USER.

$ sudo apt install libapache2-mod-auth-gssapi
$ sudo tee /etc/apache2/conf-available/intranet-auth.conf <<'EOF'
<Location />
    AuthType GSSAPI
    AuthName "Corp intranet"
    GssapiCredStore keytab:/etc/apache2/http.keytab
    GssapiBasicAuth Off
    GssapiNegotiateOnce On
    GssapiLocalName On
    GssapiAllowedMech krb5
    Require valid-user
</Location>
EOF
$ sudo a2enmod auth_gssapi && sudo a2enconf intranet-auth && sudo systemctl reload apache2

GssapiLocalName On — передавать alice, а не alice@CORP.EXAMPLE.COM; GssapiBasicAuth Off — не предлагать ввод пароля тем, у кого нет билета (с On Apache принял бы пароль и сам сходил бы за билетом — запасной путь для машин не из домена, но пароль тогда идёт через веб-сервер); GssapiNegotiateOnce On — не повторять вызов Negotiate, если первый токен не подошёл. Обмен по RFC 4559:

GET / HTTP/1.1
Host: intranet.corp.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Negotiate

GET / HTTP/1.1
Host: intranet.corp.example.com
Authorization: Negotiate <base64: токен SPNEGO с AP-REQ на HTTP/intranet.corp.example.com>

HTTP/1.1 200 OK
WWW-Authenticate: Negotiate <base64: ответ SPNEGO с AP-REP>

Первый ответ — вызов; второй запрос несёт SPNEGO-токен с AP-REQ на HTTP/intranet.corp.example.com; в ответе 200 — AP-REP для взаимной аутентификации. Пароля нет, формы входа нет. Реальный обмен с веб-сервером домена — в «Смотрим руками».

Браузер отправляет билет не любому сайту, а только тем, которым разрешено. Chrome и Edge на Windows без политики спрашивают зоны Windows: по умолчанию это сайты зоны «Местная интрасеть» — имена без точек и то, что добавлено политикой «Site to Zone Assignment List»; явный список задаёт политика AuthServerAllowlist (раньше AuthServerWhitelist). Chrome на Linux и macOS — та же политика или ключ --auth-server-allowlist=*.corp.example.com; Firefox — network.negotiate-auth.trusted-uris. Пока сайт не в списке, вместо единого входа пользователь видит окно ввода пароля или, если сервер предлагает только Negotiate, страницу 401. Делегирование из браузера (чтобы веб-приложение ходило дальше от имени пользователя) — отдельный список AuthNegotiateDelegateAllowlist, и по умолчанию оно выключено.

Имя, на которое браузер просит билет, — HTTP/<имя хоста>, класс всегда HTTP, независимо от TLS. Какое имя хоста, решает клиент: Chrome и Edge по умолчанию разворачивают CNAME и просят билет на каноническое имя (CNAME intranet → lnx1 даст запрос HTTP/lnx1.corp.example.com; политика DisableAuthNegotiateCnameLookup это выключает), а curl и многие другие клиенты — на имя из URL. Если запрошенного SPN нет ни у одной учётной записи, KDC отвечает KDC_ERR_S_PRINCIPAL_UNKNOWN (часть 6), клиент откатывается на NTLM, которого у Apache нет, — то есть просто 401. Надёжнее всего, когда сайт открывают по имени, на которое есть SPN, а keytab на сервере содержит ключ этой учётной записи. У nginx встроенного модуля нет; обычно ставят spnego-http-auth-nginx-module или выносят аутентификацию в прокси перед nginx.


Делегирование с Linux-стороны

Веб-приложение на lnx1 должно обратиться к SQL Server или к SMB-ресурсу от имени вошедшего пользователя. Неограниченное делегирование из браузера почти никогда не включают; правильный путь — ограниченное делегирование, и MIT Kerberos его умеет с версии 1.8 через расширения S4U2Self и S4U2Proxy из части 6. На стороне каталога — как для Windows:

PS C:\> Set-ADUser svc-intranet -Add @{ 'msDS-AllowedToDelegateTo' = 'MSSQLSvc/sql1.corp.example.com:1433', 'cifs/fs1.corp.example.com' }
PS C:\> Set-ADAccountControl svc-intranet -TrustedToAuthForDelegation $true     # с переходом протокола, если пользователь пришёл не по Kerberos

На Linux у mod_auth_gssapi есть GssapiDelegCcacheDir — складывать делегированные (если браузер прислал) или полученные через S4U2Proxy (GssapiUseS4U2Proxy On) билеты в каталог, откуда приложение берёт их через KRB5CCNAME. Приложение на Python, Java или .NET на Linux получает билет на MSSQLSvc/sql1 от имени alice без её пароля и без её TGT — через собственный keytab svc-intranet. Делегирование на основе ресурса (msDS-AllowedToActOnBehalfOfOtherIdentity на sql1) работает так же и настраивается владельцем ресурса, как в части 14.


Другие службы

PostgreSQL. SPN postgres/db1.corp.example.com на учётной записи машины (adcli update --add-service-principal=postgres/db1.corp.example.com), keytab для пользователя postgres, в postgresql.conf — krb_server_keyfile = '/etc/postgresql/krb5.keytab', в pg_hba.conf — hostgssenc all all 192.168.0.0/16 gss include_realm=0 krb_realm=CORP.EXAMPLE.COM. Пользователь alice с TGT подключается psql -h db1.corp.example.com без пароля, и соединение ещё и зашифровано GSS (hostgssenc принимает только такие соединения). Имя роли в базе должно совпадать с именем пользователя без realm. С Windows — тот же psql или любой клиент с libpq, билет из сеанса.

NFS. SPN nfs/nfs1.corp.example.com на сервере, rpc.gssd на клиентах, экспорт с sec=krb5p: каждый пользователь обращается к файлам со своим билетом, сервер проверяет его и подставляет UID из sssd — в отличие от NFS с sec=sys, где сервер верит UID, который назвал клиент. krb5 — только аутентификация, krb5i — плюс целостность каждого запроса, krb5p — плюс шифрование. Windows Server и сам может быть таким сервером: Server for NFS принимает krb5, krb5i и krb5p и сопоставляет пользователей Windows с UID и GID через атрибуты каталога (uidNumber, gidNumber).

Samba. Файловый сервер на Linux с security = ads вступает в домен как член (через net ads join или совместно с sssd), получает SPN cifs/, и клиенты Windows открывают его папки с билетом, как папки srv1: для них разницы нет.


Когда снова спрашивают пароль

Главный инструмент на Linux-стороне — трассировка библиотеки MIT Kerberos:

$ KRB5_TRACE=/dev/stderr ssh -K lnx1.corp.example.com true

Переменная KRB5_TRACE заставляет любую программу, собранную с libkrb5, писать в указанный файл каждый шаг: какой кеш выбран и с каким принципалом, есть ли в нём TGT, какой SPN запрошен, к какому KDC (имя из SRV-записи и адрес) ушёл TGS-REQ, что вернулось, с каким типом шифрования сессионный ключ и сохранён ли билет в кеш. Ошибки, которые здесь видны, и что они значат:

В трассировке или ответе Причина
Server not found in Kerberos database у учётной записи нет SPN с таким именем; часто библиотека канонизировала имя через DNS и запросила host/lnx1-old.corp… — лечится rdns = false и dns_canonicalize_hostname = fallback в [libdefaults] krb5.conf
Wrong principal in request, KRB_AP_ERR_MODIFIED, Decrypt integrity check failed служба не может расшифровать билет: kvno в keytab отстал от каталога (пароль машины сменился, а keytab или его копия нет — adcli update), SPN на другой учётной записи, дубликат SPN (setspn -X)
No credentials cache found, No Kerberos credentials available у пользователя нет TGT: вошёл по ключу или по GSSAPI без делегирования, KRB5CCNAME указывает не туда, или билет истёк
Clock skew too great время (часть 12)
KDC has no support for encryption type в keytab ключ RC4, а учётная запись допускает только AES, или наоборот: klist -k -e и msDS-SupportedEncryptionTypes
Cannot find KDC for realm DNS: нет SRV _kerberos._tcp или dns_lookup_kdc = false без [realms]
Permission denied (gssapi-with-mic) при живом билете sshd отверг: нет GSSAPIAuthentication yes, keytab недоступен (права), или принципал не сопоставился с локальным пользователем; в журнале успешного входа это строка Authorized to alice, krb5 principal alice@CORP.EXAMPLE.COM (krb5_kuserok), сопоставление делает модуль localauth sssd, для чужого realm нужен .k5login или auth_to_local

Проверка SPN и keytab без приложения — kvno: он просит у KDC билет на указанный SPN и печатает его номер версии ключа, по строке host/lnx1.corp.example.com@CORP.EXAMPLE.COM: kvno = 2 на каждое имя. Сравнить с keytab службы — sudo klist -k /etc/apache2/http.keytab: если kvno в билете больше, чем в keytab, keytab устарел. Без TGT kvno сразу отказывает той же ошибкой, что и klist:

alice@lnx1:~$ kvno host/lnx3.corp.example.com
kvno: No credentials cache found (filename: /tmp/krb5cc_1838401104) while getting client principal name

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

Вход с рабочей станции ws1 на lnx1 встроенным клиентом Windows, в ~\.ssh\config для *.corp.example.com — GSSAPIAuthentication yes:

PS C:\> ssh -v -l alice lnx1.corp.example.com true 2>&1 | Select-String 'Authenticat|gssapi|delegat'

debug1: Authenticating to lnx1.corp.example.com:22 as 'alice'
debug1: get_agent_identities: ssh_get_authentication_socket: No such file or directory
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: gssapi-with-mic
Authenticated to lnx1.corp.example.com ([192.168.1.60]:22) using "gssapi-with-mic".

Сервер предложил четыре метода, клиент сразу взял gssapi-with-mic и вошёл с первой попытки; строка про агента значит лишь, что ssh-agent на рабочей станции не запущен, — ключи не понадобились. gssapi-keyex — обмен ключами через GSSAPI из патча, который Ubuntu добавляет к OpenSSH; на сервере он выключен. В кеше Windows после входа — билет на host/lnx1, рядом для сравнения билет на LDAP контроллера:

PS C:\> klist | Select-String -Context 0,3 'host/lnx1|ldap/dc1\.corp\.example\.com @'

> 	Server: host/lnx1.corp.example.com @ CORP.EXAMPLE.COM
  	KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
  	Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
  	Start Time: 10/30/2026 12:39:53 (local)
> 	Server: ldap/dc1.corp.example.com @ CORP.EXAMPLE.COM
  	KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
  	Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
  	Start Time: 10/30/2026 10:25:26 (local)

У билета контроллера есть ok_as_delegate, у lnx1 — нет. Поэтому с -K клиент Windows просит делегирование и не получает его:

PS C:\> ssh -v -K -l alice lnx1.corp.example.com true 2>&1 | Select-String 'Authenticat|gssapi|delegat'

debug1: Authenticating to lnx1.corp.example.com:22 as 'alice'
debug1: get_agent_identities: ssh_get_authentication_socket: No such file or directory
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: gssapi-with-mic
debug1: Delegating credentials
debug1: sspi delegation was requested but not fulfilled
debug1: Delegating credentials
debug1: sspi delegation was requested but not fulfilled
Authenticated to lnx1.corp.example.com ([192.168.1.60]:22) using "gssapi-with-mic".

Вход состоялся, TGT остался на ws1. В сеансе на lnx1 кеша нет вовсе:

PS C:\> ssh -l alice lnx1.corp.example.com klist
klist: No credentials cache found (filename: /tmp/krb5cc_1838401104)

То же самое после входа по ключу; после входа с паролем там был бы файл /tmp/krb5cc_1838401104_<суффикс> от pam_sss.

На lnx1 — действующие настройки sshd (а не то, что написано в файлах) и журнал:

alice@lnx1:~$ sudo sshd -T | grep -i gssapi
gssapiauthentication yes
gssapicleanupcredentials yes
gssapistrictacceptorcheck yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
gssapikexalgorithms gss-group14-sha256-,gss-group16-sha512-,gss-nistp256-sha256-,gss-curve25519-sha256-
alice@lnx1:~$ sudo journalctl -u ssh --since -15min -o short-iso --no-pager | grep 'sshd-session\[537477\]' | grep -vi disconnect
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: Authorized to alice, krb5 principal alice@CORP.EXAMPLE.COM (krb5_kuserok)
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: Accepted gssapi-with-mic for alice from 192.168.1.50 port 62513 ssh2: alice@CORP.EXAMPLE.COM
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: pam_unix(sshd:session): session opened for user alice(uid=1838401104) by alice(uid=0)
2026-10-30T09:40:31+00:00 lnx1.corp.example.com sshd-session[537477]: pam_unix(sshd:session): session closed for user alice

Сначала sshd сопоставил принципал alice@CORP.EXAMPLE.COM с локальным alice (krb5_kuserok), потом принял вход и дописал принципал в конец строки Accepted. За месяц на этом сервере в журнале 101 вход gssapi-with-mic, 12 по ключу и 4 по паролю:

alice@lnx1:~$ sudo journalctl -u ssh --since -30days --no-pager | grep -oE 'Accepted [a-z-]+' | sort | uniq -c
    101 Accepted gssapi-with-mic
      4 Accepted password
     12 Accepted publickey

Keytab, которым sshd расшифровал билет (-e — типы шифрования; сами ключи klist не печатает):

alice@lnx1:~$ sudo klist -k -e /etc/krb5.keytab
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
   2 LNX1$@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 LNX1$@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 host/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 host/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)

И та же учётная запись в каталоге — SPN, допустимые типы шифрования (24 = AES128 + AES256), номер версии ключа и доверие для делегирования:

PS C:\> Get-ADComputer lnx1 -Properties servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, TrustedForDelegation |
    Format-List Name, servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, TrustedForDelegation

Name                          : LNX1
servicePrincipalName          : {RestrictedKrbHost/lnx1.corp.example.com, RestrictedKrbHost/LNX1, host/lnx1.corp.example.com, host/LNX1}
msDS-SupportedEncryptionTypes : 24
msDS-KeyVersionNumber         : 2
TrustedForDelegation          : False

KVNO в каталоге и в keytab совпадают — 2, TrustedForDelegation — False, отсюда отказ в делегировании выше. Выдачу билета видно на контроллере в событии 4769; поле ServiceName в нём — учётная запись службы, а не SPN:

PS C:\> Invoke-Command -ComputerName dc1 -ScriptBlock {
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4769; StartTime=(Get-Date).AddHours(-2)} |
        Where-Object { $_.Properties[2].Value -eq 'LNX1$' } | Select-Object -First 1 | ForEach-Object {
            ([xml]$_.ToXml()).Event.EventData.Data |
                Where-Object Name -in 'ServiceName','TicketOptions','TicketEncryptionType','Status','TransmittedServices','ServiceSupportedEncryptionTypes','ServiceAvailableKeys' |
                ForEach-Object { '{0} = {1}' -f $_.Name, $_.'#text' }
        }
}
ServiceName = LNX1$
TicketOptions = 0x40810000
TicketEncryptionType = 0x12
Status = 0x0
TransmittedServices = -
ServiceSupportedEncryptionTypes = 0x18 (AES128-SHA96, AES256-SHA96)
ServiceAvailableKeys = RC4, AES128-SHA96, AES256-SHA96, AES128-SHA256, AES256-SHA384

Билет зашифрован AES256 (0x12), TransmittedServices пусто — это прямой запрос, не S4U. Если бы пользователь ушёл с lnx1 дальше по делегированному билету, в событии 4769 на cifs/fs1 адресом клиента был бы lnx1, а не её рабочая станция: по этому признаку делегирование видно в аудите.

Последнее — HTTP с Negotiate. Веб-приложение web1 на IIS с Windows-аутентификацией; запрос без учётных данных получает список методов:

PS C:\> curl.exe -s -v -o NUL https://web1.corp.example.com/ 2>&1 | Select-String '^[<>] (GET|HTTP|Authorization|WWW-Authenticate|Location)'

> GET / HTTP/1.1
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Bearer
< WWW-Authenticate: Basic realm="https://web1.corp.example.com/"
< WWW-Authenticate: Negotiate
< WWW-Authenticate: NTLM

С --negotiate -u : (пустые учётные данные — сигнал брать билет из сеанса) curl, которому назван единственный метод, отправляет токен сразу, не дожидаясь 401; токены ниже обрезаны:

PS C:\> curl.exe -s -v -o NUL --negotiate -u : https://web1.corp.example.com/ 2>&1 | Select-String '^[<>] (GET|HTTP|Authorization|WWW-Authenticate|Location)'

> GET / HTTP/1.1
> Authorization: Negotiate YIIHuwYGKwYBBQUCoIIHrzCC…
< HTTP/1.1 302 Found
< Location: https://web1.corp.example.com/home/
< WWW-Authenticate: Negotiate oYG2MIGzoAMKAQChCwYJKoZI…

YII… — начало токена SPNEGO NegTokenInit (за ним OID SPNEGO 1.3.6.1.5.5.2 и AP-REQ внутри), около двух килобайт вместе с PAC; ответ oYG… — NegTokenResp с состоянием «принято» и AP-REP. Приложение ответило перенаправлением, а не 401: пользователь опознан. В кеше ws1 появился билет на HTTP/web1:

PS C:\> klist | Select-String -Context 0,3 'HTTP/'

> 	Server: HTTP/web1.corp.example.com @ CORP.EXAMPLE.COM
  	KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
  	Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
  	Start Time: 10/30/2026 12:43:59 (local)

хотя SPN HTTP/… у web1 в каталоге нет:

PS C:\> (Get-ADComputer web1 -Properties servicePrincipalName).servicePrincipalName
TERMSRV/web1.corp.example.com
WSMAN/web1.corp.example.com
RestrictedKrbHost/web1.corp.example.com
HOST/web1.corp.example.com
TERMSRV/WEB1
WSMAN/WEB1
RestrictedKrbHost/WEB1
HOST/WEB1

Это и есть sPNMappings: http — синоним host, и KDC нашёл учётную запись по HOST/web1.corp.example.com.


Источники

GSSAPI, SPNEGO и протоколы:

  • RFC 2743 — GSSAPI: контексты безопасности и токены, которые приложение переносит само;
  • RFC 4121 — механизм Kerberos V5 для GSSAPI;
  • RFC 4178 — SPNEGO: согласование механизма, NegTokenInit и NegTokenResp;
  • RFC 4462 — аутентификация SSH через GSSAPI, метод gssapi-with-mic и обмен ключами gssapi-keyex;
  • RFC 4559 — HTTP Negotiate: WWW-Authenticate и Authorization с токеном SPNEGO;
  • RFC 4120 — Kerberos V5: флаги билетов forwardable, forwarded и ok-as-delegate;
  • [MS-KILE]: Request Flags Details и Ticket Flag Details — клиент Windows пересылает TGT только серверу с OK-AS-DELEGATE, KDC ставит флаг учётным записям с доверием для делегирования.

MIT Kerberos:

  • krb5.conf — default_ccache_name, rdns, dns_canonicalize_hostname, enforce_ok_as_delegate, dns_lookup_kdc, модули localauth;
  • Credential cache — типы кешей FILE, DIR, KEYRING, KCM и переменная KRB5CCNAME;
  • keytab — что хранит keytab и как с ним работает ktutil;
  • kinit, klist и kvno — kinit -k, флаги билетов в klist -f, klist -k -e, номер версии ключа;
  • .k5login — какие принципалы могут входить под локальным пользователем;
  • Troubleshooting — трассировка KRB5_TRACE;
  • krb5-1.8 — S4U2Self и S4U2Proxy в MIT Kerberos.

OpenSSH:

SSSD и учётная запись машины:

  • sssd-krb5 — krb5_ccachedir, krb5_ccname_template, кеш по умолчанию из krb5.conf;
  • sssd-ad — ad_maximum_machine_account_password_age: смена пароля машины раз в 30 дней;
  • sssd-kcm — демон KCM и default_ccache_name = KCM:;
  • adcli — adcli update, --add-service-principal, keytab машины;
  • msktutil — учётные записи служб и keytab с Linux-стороны, --auto-update;
  • gssproxy — доступ служб к ключам без чтения keytab;
  • kstart — k5start для долгоживущих процессов.

Каталог:

Веб:

Другие службы:


Один каталог, одна аутентификация для Windows, Linux, Wi-Fi, VPN и веб-приложений. Остался последний рубеж, который у большинства организаций давно существует рядом с доменом и живёт по своим правилам: облако.

Дальше — часть 31: Entra Connect. Что такое Entra ID рядом с Active Directory и зачем их связывать; что именно синхронизируется и что нет; три способа аутентификации — синхронизация хешей паролей, сквозная аутентификация и федерация; UPN и подтверждённые домены; фильтрация по OU и атрибутам; установка Entra Connect и учётная запись синхронизации; и Seamless SSO — Kerberos для облака.

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

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