AD, Kerberos и ADFS
Как устроен корпоративный вход в Windows-сети: чем Kerberos отличается от NTLM, как ASP.NET Core узнаёт пользователя и его группы AD, что делает ADFS и почему вызов от имени пользователя ломается на втором сервере. Эти вопросы задают везде, где приложения живут во внутренней сети с Active Directory.
Перед этой темой: OAuth 2.0 и OpenID Connect.
Что такое Windows-аутентификация: чем NTLM отличается от Kerberos и что такое Negotiate?
Заголовок раздела «Что такое Windows-аутентификация: чем NTLM отличается от Kerberos и что такое Negotiate?»Что это
Windows-аутентификация — браузер входит в приложение под той учётной записью, с которой пользователь вошёл в Windows. Учётки хранит Active Directory (AD), а пароли проверяет контроллер домена (DC).
Чем отличаются протоколы
| Признак | NTLM | Kerberos |
|---|---|---|
| Как доказывает | отвечает на вызов сервера | предъявляет билет от DC |
| Кто проверяет | DC при каждом входе | сервер сам |
| Проверка сервера | нет | есть |
Что такое Negotiate
Схема HTTP, которая пробует Kerberos, а при неудаче берёт NTLM. Сервер отвечает 401 с заголовком WWW-Authenticate: Negotiate, браузер повторяет запрос с токеном в Authorization.
Частая ошибка
Открывать сайт по IP-адресу. Kerberos требует DNS-имя, поэтому вход молча пойдёт через NTLM.
Как устроено
NTLM — три сообщения в двух кругах 401:
- Клиент сообщает, что хочет NTLM.
- Сервер присылает вызов — случайное число.
- Клиент возвращает вызов, обработанный ключом из хеша пароля. Сервер пароля не знает и пересылает ответ на DC.
В Kerberos клиент заранее получил у DC билет к этой службе. Билет зашифрован ключом службы, поэтому сервер проверяет его сам, без DC.
Когда что сработает
| Ситуация | Протокол |
|---|---|
| Доменный клиент, DNS-имя службы зарегистрировано в AD | Kerberos |
| IP-адрес, имя не зарегистрировано, клиент вне домена | NTLM |
| Нужен вызов дальше от имени пользователя | только Kerberos |
Ловушки
- Тихий откат. Negotiate не сообщает, что Kerberos не получился. Протокол видно по токену в
Authorization: Negotiate: NTLM в base64 начинается сTlRMTVNTUA, Kerberos — обычно сYII. - Привязка к соединению. Вход живёт на TCP-соединении. Прокси без закрепления соединений ломает аутентификацию.
- HTTP/2. Windows-аутентификация поверх HTTP/2 не работает: для рукопожатия клиент переходит на HTTP/1.1.
Правило
Цель — Kerberos для всех доменных клиентов, а каждый вход через NTLM — повод искать причину.
Под капотом
- Negotiate — механизм SPNEGO (RFC 4178), перенесённый в HTTP (RFC 4559).
- Ответ NTLMv2 — HMAC-MD5 на ключе из NT-хеша пароля. Хеш равносилен паролю: его кража даёт pass-the-hash.
- NTLM не проверяет сервер, поэтому ответ можно переслать на другой сервер (NTLM relay). Защита для HTTPS — Extended Protection: привязка входа к TLS-каналу.
Цена
- Каждый вход NTLM — запрос сервера к DC через Netlogon. На тысячах пользователей это заметная нагрузка на DC.
- NTLM — два круга
401против одного у Kerberos.
Статус NTLM (октябрь 2026)
- Июнь 2024 — все версии NTLM объявлены deprecated.
- Windows 11 24H2 и Windows Server 2025 — NTLMv1 удалён.
- Январь 2026 — план Microsoft из трёх фаз:
- аудит NTLM, уже доступен;
- во второй половине 2026 года — IAKerb и локальный центр выдачи билетов Kerberos для сценариев, которые сейчас откатываются на NTLM;
- в следующей версии Windows Server и клиентской Windows сетевой NTLM выключен по умолчанию, вернуть его можно только политикой.
Как проверить в проде
- Событие 4624 в журнале Security сервера: поле Authentication Package —
KerberosилиNTLM. - Аудит NTLM — политики «Network security: Restrict NTLM» в режиме аудита.
- Частые причины NTLM: IP-адрес или имя без SPN, клиент вне домена, сайт не в зоне «Местная интрасеть» браузера.
Итог: NTLM — запасной протокол без проверки сервера и делегирования, который Microsoft выключает; задача сеньора — найти и устранить каждый откат на него.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication.Negotiate;
var builder = WebApplication.CreateBuilder(args);
// Negotiate: сначала Kerberos, при неудаче — NTLMbuilder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) .AddNegotiate();
// Без [AllowAnonymous] каждый эндпоинт требует входаbuilder.Services.AddAuthorization(options => options.FallbackPolicy = options.DefaultPolicy);
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
app.MapGet("/whoami", (ClaimsPrincipal user) => user.Identity?.Name); // CONTOSO\alice
app.Run();На IIS тот же код работает поверх Windows Authentication самого IIS: обработчик Negotiate видит, что сервер умеет её сам, и передаёт работу ему. В IIS включают Windows Authentication и выключают анонимный доступ.
- Почему NTLM-вход нельзя передать дальше, на другой сервер? — Сервер получает только ответ на свой вызов, а не билет или ключ пользователя.
- Как узнать, что сработал NTLM, а не Kerberos? — По токену в
Authorization(TlRMTVNTUA— NTLM) или по событию 4624 на сервере. - Почему по IP-адресу Kerberos не работает? — Билет выдаётся на имя службы; для IP его по умолчанию не запрашивают, и Negotiate откатывается на NTLM.
- Зачем HTTPS, если пароль не передаётся? — Без TLS читаются данные и cookie сессии, а NTLM-ответ можно переслать; Extended Protection привязывает вход к TLS-каналу.
- Что Microsoft делает с NTLM? — Deprecated с 2024 года, NTLMv1 удалён, сетевой NTLM будет выключен по умолчанию в следующей версии Windows Server.
Как работает Kerberos и как включить его в ASP.NET Core?
Заголовок раздела «Как работает Kerberos и как включить его в ASP.NET Core?»Что это
Kerberos — протокол, в котором пароль проверяется один раз, а дальше пользователь предъявляет службам билеты. Билеты выдаёт KDC — служба на каждом контроллере домена.
Как это выглядит
- Пользователь входит в Windows и получает TGT — «билет на получение билетов».
- Открывает портал — Windows по TGT получает у KDC билет именно к этому порталу.
- Браузер отправляет билет в заголовке
Authorization: Negotiate .... - Портал проверяет билет своим ключом и узнаёт пользователя.
Как пользоваться
- Посмотреть свои билеты — команда
klist. - Портал открывают по DNS-имени, для которого администратор зарегистрировал SPN, например
HTTP/portal.contoso.com.
Частая ошибка
Не следить за временем сервера. Если часы расходятся с контроллером домена больше чем на 5 минут, Kerberos отклоняет билеты.
Как устроено
- AS-REQ и AS-REP. Клиент шлёт KDC метку времени, зашифрованную ключом из пароля. KDC отвечает TGT, зашифрованным ключом учётки
krbtgt, и сеансовым ключом. - TGS-REQ и TGS-REP. Клиент предъявляет TGT и SPN службы. KDC находит учётку с этим SPN и выдаёт билет, зашифрованный её ключом.
- AP-REQ. Клиент передаёт серверу билет и свежую метку времени на сеансовом ключе. Сервер расшифровывает билет своим ключом и сверяет время.
- AP-REP. По запросу клиента сервер доказывает, что тоже знает ключ, — это взаимная аутентификация.
Ловушки
- SPN не на той учётке. Приложение под другой учёткой не расшифрует билет:
KRB_AP_ERR_MODIFIEDи откат на NTLM. - Дубликат SPN. У SPN два владельца — KDC не выдаст билет.
- Часы. Допуск по умолчанию — 5 минут, иначе
KRB_AP_ERR_SKEW. - CNAME. Браузер запрашивает билет на каноническое имя из DNS, поэтому для алиаса SPN нужен на имени A-записи.
Кто проверяет билет в ASP.NET Core
| Хостинг | Чьим ключом |
|---|---|
| IIS, Windows Authentication | учётки компьютера; учётки пула — с useAppPoolCredentials |
| Kestrel на Windows | учётки, под которой работает процесс |
| Kestrel на Linux | из keytab, путь в KRB5_KTNAME |
Под капотом
- Сервер не ходит к KDC: ключ службы знают только они двое. Поэтому кража ключа службы — keytab или пароля сервисной учётки — позволяет подделать билеты к ней (silver ticket), а ключа
krbtgt— любые TGT (golden ticket). - PAC перечисляет группы пользователя, включая вложенные, и сервер строит из него Windows-токен без запроса к AD. Новое членство появится только с новым TGT, а он по умолчанию живёт 10 часов.
- Любой пользователь домена может запросить билет к любому SPN и подбирать пароль сервисной учётки офлайн (Kerberoasting). Защита — длинные случайные пароли и AES.
- RC4 в Kerberos объявлен deprecated в Windows Server 2025. С обновлений 2026 года (CVE-2026-20833) контроллеры домена считают, что учётка без явного
msDS-SupportedEncryptionTypesподдерживает только AES.
Компромиссы
- Kestrel на Linux. Групп из PAC в принципале нет — их читают из AD по LDAP (
EnableLdap): лишние запросы и кеш. - Ферма за балансировщиком. Общее имя — один SPN на доменной учётке, под которой работают все узлы. SPN на учётке одного сервера сломает вход на остальных.
- Keytab. Это ключ в файле. При смене пароля учётки меняется номер версии ключа (kvno), и старый keytab перестаёт подходить.
Как проверить в проде
klist get HTTP/portal.contoso.comс клиента: выдаётся ли билет и с каким шифрованием.- На DC события 4768 (TGT) и 4769 (сервисный билет) с кодом ошибки:
0x7— SPN не найден. - Время —
w32tm /query /statusна сервере и DC.
Итог: Kerberos держится на правильном SPN, общем ключе и синхронном времени; почти любая авария в проде — одно из трёх.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication.Negotiate;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) .AddNegotiate(options => { // На Linux ключ службы берётся из keytab (переменная KRB5_KTNAME), // а групп в принципале нет: читаем их из AD по LDAP if (OperatingSystem.IsLinux()) options.EnableLdap("contoso.com"); });builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
app.MapGet("/", (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}") .RequireAuthorization();
app.Run();# SPN на учётке, под которой работает приложение; -S сначала ищет дубликатыsetspn -S HTTP/portal.contoso.com CONTOSO\svc-portal
# Все SPN учётки и поиск дубликатов по всему лесуsetspn -L CONTOSO\svc-portalsetspn -X -F
# Запросить сервисный билет и посмотреть кеш билетовklist get HTTP/portal.contoso.comklist
# Сбросить билеты, чтобы получить новые, например после добавления в группуklist purge- Что будет, если часы сервера ушли на 10 минут? —
KRB_AP_ERR_SKEW: билеты отклоняются, Negotiate откатится на NTLM или вернёт401. - Почему сервер не обращается к DC при проверке билета? — Билет зашифрован ключом службы, который знают только она и KDC.
- Где регистрировать SPN, если пул IIS работает под доменной учёткой? — На этой учётке и включить
useAppPoolCredentials; иначе IIS расшифровывает билет ключом компьютера. - Почему новая группа пользователя не видна в приложении? — Группы берутся из PAC в TGT; нужен новый TGT: перелогин или
klist purge. - Что такое Kerberoasting? — Офлайн-подбор пароля сервисной учётки по её билету; защита — длинные случайные пароли и AES.
Что такое ADFS и федерация?
Заголовок раздела «Что такое ADFS и федерация?»Что это
ADFS — роль Windows Server для единого входа (SSO) через браузер. Приложение не проверяет пароли: оно перенаправляет пользователя на ADFS и получает от него токен.
Как это выглядит
- Пользователь открывает приложение, оно перенаправляет его на ADFS.
- ADFS проверяет пользователя: внутри сети — Windows-аутентификацией, снаружи — формой входа.
- ADFS выдаёт подписанный токен с claims: имя, почта, группы.
- Приложение проверяет подпись и создаёт сессию в cookie.
Зачем
- Приложению не нужен доступ к AD, и ему не обязательно быть в домене.
- Пользователь входит один раз во все подключённые приложения.
Частая ошибка
Путать ADFS с Active Directory. AD хранит учётки, а ADFS выдаёт на их основе токены.
Как устроено
- Claims provider trust — откуда ADFS берёт пользователей. По умолчанию это AD, но может быть и ADFS партнёра — так строится федерация между организациями.
- Relying party trust — приложение, которому ADFS выдаёт токены: идентификатор, адрес возврата, протокол.
- Claim rules — что положить в токен: атрибуты LDAP, группы, преобразования. Правило пишется как условие,
=>и действиеissue.
Когда что выбирать
| Протокол | Где уместен |
|---|---|
| WS-Federation | старые приложения .NET на WIF, SharePoint |
| SAML 2.0 | готовые продукты и SaaS |
| OpenID Connect (ADFS 2016+) | новые веб-приложения и API |
Ловушки
- Смена сертификата подписи. ADFS сам обновляет сертификат подписи токенов. Приложение, которое читает метаданные (
MetadataAddress), подхватит новый ключ, а с захардкоженным отпечатком — сломается. - SAML в ASP.NET Core. Встроенного обработчика SAML 2.0 нет, нужна сторонняя библиотека. WS-Federation и OIDC есть в виде пакетов Microsoft.
- Шифрование токенов.
Microsoft.AspNetCore.Authentication.WsFederationего не поддерживает, поэтому в relying party trust сертификат шифрования не задают.
Правило
Новое приложение — OIDC; WS-Federation — для совместимости со старыми.
Под капотом
- ADFS — ферма серверов с общей конфигурацией в Windows Internal Database или SQL Server. Из интернета её публикует Web Application Proxy в DMZ.
- Движок claims выполняет правила по порядку:
addкладёт claim только во входной набор для следующих правил,issue— ещё и в токен. - Группы обычно выдаёт правило, которое запрашивает у хранилища Active Directory атрибут
tokenGroups: это все группы пользователя, включая вложенные.
Статус ADFS (октябрь 2026)
- ADFS есть в Windows Server 2025 и поддерживается, но Microsoft прямо советует вместо обновления переходить на Entra ID.
- Windows Internal Database, база конфигурации ADFS по умолчанию, в Windows Server 2025 объявлена deprecated.
Путь миграции на Entra ID
- Entra Connect синхронизирует пользователей и группы, чтобы Entra выдавала группы в формате AD, например
sAMAccountName. - Отчёт AD FS application migration (нужны агенты Entra Connect Health) показывает активные приложения и правила, которые не переносятся как есть.
- Приложения переключают по одному: тестовый экземпляр, затем прод. Мастер переносит SAML-приложения, WS-Federation и OIDC настраивают вручную.
- Домен переводят с федерации на Password Hash Sync или Pass-through Authentication и выводят ADFS из эксплуатации.
Внутренние приложения с Windows-аутентификацией публикуют через Microsoft Entra application proxy: его коннектор сам получает для пользователя билет Kerberos к приложению.
Как проверить в проде
Ошибки выдачи токенов — журнал AD FS/Admin на серверах фермы; состав токена — служебная страница приложения со списком claims.
Итог: ADFS работает и поддерживается, но это тупиковая ветка: сопровождая его, планируют перенос claims и групп в Entra ID.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication.Cookies;using Microsoft.AspNetCore.Authentication.WsFederation;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options => { // Сессия — в cookie, вход — перенаправлением на ADFS options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme; }) .AddCookie() .AddWsFederation(options => { // Метаданные: адрес входа и сертификат подписи токенов options.MetadataAddress = "https://adfs.contoso.com/FederationMetadata/2007-06/FederationMetadata.xml"; // Идентификатор relying party trust в ADFS options.Wtrealm = "https://portal.contoso.com/"; });builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
// Claims, которые выдали правила ADFSapp.MapGet("/claims", (ClaimsPrincipal user) => user.Claims.Select(c => $"{c.Type}: {c.Value}")) .RequireAuthorization();
app.Run();Для OIDC вместо AddWsFederation берут AddOpenIdConnect с Authority = "https://adfs.contoso.com/adfs"; сам протокол разобран в теме про OAuth 2.0 и OpenID Connect.
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"] => issue(store = "Active Directory", types = ("http://schemas.microsoft.com/ws/2008/06/identity/claims/role"), query = ";tokenGroups;{0}", param = c.Value);Правило берёт имя Windows-учётки пользователя, запрашивает у AD все его группы, включая вложенные, и выдаёт каждую как claim роли.
- Чем relying party trust отличается от claims provider trust? — Первый описывает, кому ADFS выдаёт токены, второй — кому ADFS верит при проверке пользователя.
- Как ADFS выбирает способ входа? — По политике аутентификации: из интрасети — Windows-аутентификация, через WAP из интернета — форма и MFA.
- Что сломается при смене сертификата подписи токенов? — Приложения с захардкоженным отпечатком; те, что читают метаданные, подхватят новый ключ.
- Почему обработчик WS-Federation по умолчанию отвергает незапрошенный вход? — Вход без запроса приложения уязвим к CSRF; разрешает его только
AllowUnsolicitedLogins. - Что делать с внутренним приложением на Kerberos при уходе с ADFS? — Опубликовать через Microsoft Entra application proxy или перевести на OIDC.
Как использовать группы AD для авторизации в ASP.NET Core?
Заголовок раздела «Как использовать группы AD для авторизации в ASP.NET Core?»Что это
Группа AD — набор учёток, которому администратор выдаёт права. Приложение проверяет не имя пользователя, а членство в группе: новый сотрудник получает доступ, когда его добавят в группу.
Как пользоваться
- Включите Windows-аутентификацию (
AddNegotiate()) или вход через ADFS. - Опишите политику:
options.AddPolicy("Admins", p => p.RequireRole(@"CONTOSO\Portal-Admins")). - Повесьте её на эндпоинт:
.RequireAuthorization("Admins").
Как это работает
При Windows-аутентификации User.IsInRole(@"CONTOSO\Portal-Admins") переводит имя группы в SID — постоянный идентификатор — и ищет его в Windows-токене пользователя. Вложенные группы там уже учтены.
Частая ошибка
Ждать доступа сразу после добавления в группу. Группы попадают в билет при входе в Windows, поэтому пользователю нужно перелогиниться.
Как устроено
| Вход | Как приходят группы |
|---|---|
| Windows: Negotiate, IIS | SID в claims ClaimTypes.GroupSid, включая вложенные |
| ADFS | claims по правилу, обычно tokenGroups, тоже с вложенными |
| Entra ID | ID объектов или sAMAccountName синхронизированных групп |
IClaimsTransformation
Сервис, который ASP.NET Core вызывает после аутентификации, чтобы дополнить ClaimsPrincipal. В нём группы переводят в роли приложения: SID группы → approver. Бизнес-код знает только роли, а соответствие групп ролям лежит в одном месте.
Ловушки
- Повторные вызовы.
TransformAsyncвызывается на каждыйAuthenticateAsync, иногда несколько раз за запрос. Проверяйте, не добавлены ли роли уже. - IIS без схемы. Если полагаться только на автоматический вход IIS,
AuthenticateAsyncне вызывается и трансформация молчит. НуженAddAuthenticationсо схемой:AddNegotiate()илиIISServerDefaults.AuthenticationScheme. - Имя вместо SID. Группу переименовали — проверка по имени сломалась, а SID остался прежним.
- Свежесть. Права обновляются с новым билетом или токеном, а кеш добавляет свою задержку.
Правило
Группы — на входе, роли приложения — в политиках, соответствие — в IClaimsTransformation.
Под капотом
WindowsPrincipal.IsInRole(string)на каждый вызов переводит имя в SID через LSA и только потом смотрит обычные claims. Проверка по SID или по своим claims ролей этого запроса не делает.- Kerberos-билет ограничен
MaxTokenSize— 48 000 байт по умолчанию. В HTTP он едет в base64, то есть на треть длиннее, и упирается в лимиты заголовков HTTP.sys (MaxFieldLength,MaxRequestBytes) и Kestrel (MaxRequestHeadersTotalSize, 32 КБ). Симптом —400 Request Header too long. - Entra ID кладёт не больше 200 групп в JWT и 150 в SAML. Сверх лимита claim
groupsпропадает, вместо него приходит_claim_namesсо ссылкой на Graph.
Компромиссы
| Решение | Плюс | Минус |
|---|---|---|
| Группы в токене | без запросов к AD | размер токена, устаревание |
| LDAP при входе | свежие данные | нагрузка на DC, нужен кеш |
| Группы, назначенные приложению | маленький токен | без вложенных групп |
Linux
На Kestrel под Linux EnableLdap читает memberOf через System.DirectoryServices.Protocols (нужен libldap) и кеширует результат: 10 минут скользящий срок, 60 минут абсолютный. memberOf не содержит основную группу, обычно Domain Users. System.DirectoryServices и AccountManagement работают только на Windows.
Как проверить в проде
Служебная страница для администраторов показывает claims текущего пользователя. О больших билетах KDC предупреждает событием 31, если включена политика «Warning for large Kerberos tickets».
Итог: группы — это размер токена против свежести прав; в больших доменах в токен кладут только нужное приложению, остальное читают по LDAP с кешем.
using System.Security.Claims;using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Authentication.Negotiate;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();builder.Services.AddTransient<IClaimsTransformation, AdGroupsToAppRoles>();// RequireClaim, а не RequireRole: WindowsPrincipal.IsInRole сначала ищет имя через LSAbuilder.Services.AddAuthorization(options => options.AddPolicy("Approvers", p => p.RequireClaim("app_role", "approver")));
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
app.MapPost("/requests/{id:int}/approve", (int id) => $"Request {id} approved") .RequireAuthorization("Approvers");
app.Run();
public sealed class AdGroupsToAppRoles : IClaimsTransformation{ // SID группы → роль приложения: SID не меняется при переименовании группы static readonly Dictionary<string, string> RolesBySid = new() { ["S-1-5-21-1004336348-1177238915-682003330-1105"] = "approver", };
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { // Вызывается на каждый AuthenticateAsync: роли добавляем один раз if (principal.Identities.Any(i => i.Label == "app-roles")) return Task.FromResult(principal);
var roles = principal.FindAll(ClaimTypes.GroupSid) .Where(c => RolesBySid.ContainsKey(c.Value)) .Select(c => new Claim("app_role", RolesBySid[c.Value]));
principal.AddIdentity(new ClaimsIdentity(roles) { Label = "app-roles" }); return Task.FromResult(principal); }}- Почему пользователь не получил доступ сразу после добавления в группу? — Группы берутся из TGT; нужен новый билет: перелогин или
klist purge. - Почему лучше SID, а не имя группы? — Имя меняется при переименовании и может совпасть в разных доменах, а SID постоянен.
- Что делать при overage в Entra ID? — Выдавать только группы, назначенные приложению, или запрашивать членство через Microsoft Graph.
- Почему
TransformAsyncдолжен быть идемпотентным? — Он вызывается на каждыйAuthenticateAsync, иногда несколько раз за запрос. - Как получить группы на Kestrel под Linux? —
EnableLdapвAddNegotiate: запросmemberOfпо LDAP с кешем.
Что такое проблема double hop и как работает делегирование Kerberos?
Заголовок раздела «Что такое проблема double hop и как работает делегирование Kerberos?»Что это
Первый прыжок — браузер → веб-приложение, второй — приложение → база или файловый сервер. На втором прыжке приложение не может представиться пользователем.
Почему так
Браузер отдал приложению билет Kerberos только к этому приложению. Пароля и TGT пользователя у него нет, поэтому билет к базе от имени пользователя не получить. База видит NT AUTHORITY\ANONYMOUS LOGON или отказывает в доступе.
Как решают
- Проще всего ходить ко второму серверу от учётки приложения, а права пользователя проверять в самом приложении.
- Если второй сервер должен видеть именно пользователя, администратор настраивает делегирование Kerberos.
Частая ошибка
Проверять в браузере на самом веб-сервере: там прыжок один и всё работает, а с рабочих мест — нет.
Как устроено
- Пользователь входит по Kerberos во фронтенд, тот получает его Windows-идентичность.
- Фронтенд имперсонирует пользователя (
WindowsIdentity.RunImpersonatedAsync) и обращается к бэкенду. - Windows просит у KDC билет к бэкенду от имени пользователя. KDC выдаёт его, только если учётке фронтенда разрешено делегирование к SPN бэкенда.
Когда что выбирать
| Вид | Где настраивается | Что может фронтенд |
|---|---|---|
| Неограниченное | на учётке фронтенда | с TGT пользователя — что угодно |
| KCD | на учётке фронтенда, список SPN | только перечисленные службы |
| RBCD | на учётке бэкенда, кому можно | то, что разрешил владелец бэкенда |
Ловушки
- Protocol transition. Режим KCD «Use any authentication protocol» (S4U2Self) выдаёт билет даже за пользователя, вошедшего без Kerberos — через NTLM или форму. Удобно, но тогда фронтенд может выдать себя за любого пользователя перед этими службами.
- Пул IIS. Делегирование настраивают на учётке, которая владеет SPN фронтенда: учётке компьютера или доменной учётке пула.
- Linux. Имперсонация в ASP.NET Core есть только на Windows.
Правило
Сначала выяснить, нужен ли бэкенду именно пользователь. Если да — RBCD или KCD «Kerberos only» к конкретным SPN.
Под капотом
- Неограниченное. Клиент пересылает серверу свой TGT, и тот лежит в памяти LSASS. Захват сервера даёт TGT всех, кто к нему заходил. Credential Guard, включённый по умолчанию на подходящих Windows 11 22H2+ и Windows Server 2025, такое делегирование блокирует.
- KCD — S4U2Proxy. Фронтенд предъявляет KDC билет пользователя к себе и получает билет к бэкенду. KDC сверяет SPN со списком
msDS-AllowedToDelegateToфронтенда. - RBCD. KDC проверяет атрибут
msDS-AllowedToActOnBehalfOfOtherIdentityна учётке бэкенда; в PowerShell это параметрPrincipalsAllowedToDelegateToAccount. - Не делегируются члены Protected Users и учётки с флагом «Account is sensitive and cannot be delegated».
Контейнеры и Linux
- Windows-контейнер в домен не вводят: доменную идентичность ему даёт gMSA через credential spec.
- В Linux-контейнер монтируют keytab и
krb5.conf, путь к keytab задаётKRB5_KTNAME. Имперсонации там нет, поэтому бэкенд вызывают от учётки сервиса или с OAuth-токеном.
Как проверить в проде
setspn -Qиsetspn -X -F— у каждого SPN ровно один владелец.- События 4769 на DC: поле Transited Services показывает делегирование, Failure Code — причину отказа.
- Дубликат SPN KDC отмечает в журнале System событием 11.
Итог: делегирование — доверие KDC к фронтенду; выбирают самый узкий вид, а ломается оно чаще в каталоге, чем в коде.
using System.Security.Principal;using Microsoft.AspNetCore.Authentication.Negotiate;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();builder.Services.AddAuthorization(options => options.FallbackPolicy = options.DefaultPolicy);
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();
app.MapGet("/report", async (HttpContext context) =>{ // Только Windows: на Linux имперсонации нет var user = (WindowsIdentity)context.User.Identity!;
// Второй прыжок: файловый сервер увидит пользователя, только если учётке // приложения разрешено делегирование к cifs/files01.contoso.com return await WindowsIdentity.RunImpersonatedAsync(user.AccessToken, () => File.ReadAllTextAsync(@"\\files01.contoso.com\reports\daily.txt"));});
app.Run();Без делегирования файловый сервер получает анонимный вход и отвечает отказом в доступе, хотя у самого пользователя права на файл есть.
# У SPN фронтенда ровно один владелец; дубликаты по всему лесуsetspn -Q HTTP/portal.contoso.comsetspn -X -F
# RBCD: файловый сервер разрешает делегирование учётке портала (gMSA)Set-ADComputer files01 -PrincipalsAllowedToDelegateToAccount (Get-ADServiceAccount gmsa-portal)Get-ADComputer files01 -Properties PrincipalsAllowedToDelegateToAccount
# Выдаёт ли KDC билет к файловому серверу и с каким шифрованиемklist get cifs/files01.contoso.com- Почему на самом сервере всё работает, а у пользователей — нет? — Браузер на сервере даёт один прыжок; с рабочего места их два.
- Чем KCD отличается от RBCD? — KCD задают на фронтенде списком SPN, RBCD — на бэкенде, его владелец решает сам, и оно работает между доменами.
- Что такое protocol transition? — S4U2Self: фронтенд получает билет за пользователя, вошедшего не по Kerberos; удобно и опасно.
- Почему не включить неограниченное делегирование? — TGT каждого пользователя оседает в памяти сервера; захват сервера даёт доступ от их имени куда угодно.
- Как обойтись без делегирования? — Вызывать бэкенд от учётки приложения с проверкой прав у себя или передавать OAuth-токен пользователя.