Security Briefs
Exploring S4U Kerberos Extensions in Windows Server 2003
Keith Brown
Contents
Discovering Authorization Information
S4U2Self
The Problem of Delegation
Constrained Delegation or S4U2Proxy
Protocol Transition
Controls and Limitations
Conclusion
Building Web sites that provide services external to the corporate firewall is tricky. Usually it's not desirable to grant corporate domain accounts to external clients, and from a purely practical standpoint Kerberos does not work well over the Internet due to the typical configuration of client-side firewalls. This means that the site has to provide some form of authentication and authorization that is different from the methods built into Windows®.
To help deal with this and the other challenges I'll describe, Microsoft has implemented two extensions to Kerberos designed for use on servers. These extensions are collectively called Service-for-User (S4U). The goal is to allow server developers to program against the built-in security model that Windows provides, making use of the group-based authorization framework that administrators are already familiar with.
This column assumes a basic familiarity with Kerberos. For an online primer, see "Exploring Kerberos, the Protocol for Distributed Security in Windows 2000" in the August 1999 issue of Microsoft Systems Journal. For a more detailed explanation, see the book Programming Windows Security (Addison-Wesley, 2000). For the spec, see RFC 1510 at the IETF site (https://www.ietf.org/rfc/rfc1510.txt).
Discovering Authorization Information
The first extension I'll introduce helps a server discover the groups that a domain user belongs to. The authorization framework in Windows has become so complicated that it's virtually impossible for a server developer to manually discover the groups for a user. Global groups come from the user's home domain and may be nested. Universal groups can come from all over the user's home forest, are stored in the global catalog, and may be nested as well. Domain local groups are stored in the server's domain and may be nested. Local groups are stored on the server machine and are the easiest of the bunch to deal with. The SIDHistory feature in Windows 2000 means the resulting group expansion may need to be done multiple times for some users. Another feature called domain quarantine might mean removing some groups. Simply put, trying to manually figure out the group membership of a domain user is a developer's worst nightmare, and should be avoided.
The best way to get an authoritative list of groups for a user is to establish a logon for that user and look at the resulting token. During Kerberos authentication, the domain controllers perform all the heavy lifting. The only problem is, to perform Kerberos authentication on a client's behalf, the server needs to have the client's primary credentials, which basically means having access to the client's password or the client's ticket-granting ticket (TGT) and the corresponding session key. If the server knew the client's password, here's how simple it would be to get a token for a client named Alice, in the domain sales:
HANDLE hToken; if (LogonUser("alice", "sales", alicesPassword, LOGON32_LOGON_NETWORK, 0, &hToken)) { // hToken holds a token with all of Alice's groups }
The obvious problem here is that the server must know the client's password, which is a severe breach of trust.
S4U2Self
The S4U solution in this case is for the server to go through the motions of Kerberos authentication and obtain a logon for the client, but without providing the client's credentials. Thus, you're not really authenticating the client in this case, only making the rounds to collect the group security identifiers (SIDs) for the client. To allow this to occur, Windows Server 2003 domain controllers accept a new type of Kerberos request, where the service requests a ticket from the client to itself, presenting its own credentials instead of the client's. This extension is called Service-for-User-to-Self (S4U2Self).
If the client and the service are in separate domains, this requires a bidirectional trust path between them because the service, acting on the client's behalf, must request tickets from the client's domain.
While the wire-level details are all rather complicated, the service developer need only call one function to start the ball rolling: LsaLogonUser. In spirit, this is similar to calling LogonUser as I have shown earlier, but without needing to provide the client's password. The result is a token that the service can use with functions like AccessCheck and CheckTokenMembership, as well as the new AuthZ family of authorization functions. This allows the service to perform access checks against security descriptors for objects that it manages.
To protect the client, LsaLogonUser normally returns a token with a special restriction. The token will have an impersonation level of Identify, which means that the service will not be able to open kernel objects while impersonating the client using this token. However, for services that are part of the trusted computing base (TCB)—for example, a service running as SYSTEM—LsaLogonUser will return a token with an impersonation level of Impersonate, allowing access to local kernel objects using the client's identity. This prevents an untrusted service from using an S4U2Self ticket to elevate its own local privileges.
Developers who want to use this feature need only call LsaLogonUser. However, LsaLogonUser is a difficult function to use. It expects 14 arguments, some of which are pointers to variable-length structures or handles to things that need opening first. Version 1.1 of the Microsoft® .NET Framework (in beta as of this writing) includes a new constructor on the WindowsIdentity class that tremendously simplifies this:
public WindowsIdentity(string userPrincipalName);
Figure 1 shows some sample C++ code that calls LsaLogonUser directly, requesting an S4U2Self logon.
Figure 1 Calling LsaLogonUser
#define UNICODE #define _UNICODE #include <windows.h> #include <ntsecapi.h> #include <stdio.h> #pragma comment(lib, "secur32.lib") // some simple error handling functions void _err(const wchar_t* fcn, DWORD err = GetLastError()) { wchar_t msg[256]; if (!FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, err, 0, msg, sizeof msg / sizeof *msg, 0)) { wsprintf(msg, L"hr = 0x%08X", err); } wprintf(L"%s failed: %s\n", fcn, msg); exit(1); } void _nterr(const wchar_t* fcn, NTSTATUS s) { _err(fcn, LsaNtStatusToWinError(s)); } void _checknterr(const wchar_t* fcn, NTSTATUS s) { if (s) { _nterr(fcn, s); } else { wprintf(L"%s succeeded.\n", fcn); } } // helper for initializing Kernel type ANSI strings // from string literals void _si(LSA_STRING* a, const char* b) { a->Length = (USHORT)(strlen(b) * sizeof(*b)); a->MaximumLength = (USHORT)(a->Length + sizeof(*b)); a->Buffer = const_cast<char*>(b); } HANDLE _s4uLogon(const wchar_t* pszUserPrincipalName) { // connect to the Local Security Authority // this will allow us to get a token via S4U2Self // (note we aren't in the TCB so we won't be able // to use the resulting token to open Kernel objects) HANDLE hlsa; _checknterr(L"LsaConnectUntrusted", LsaConnectUntrusted(&hlsa)); // look up the Kerb authentication provider's index LSA_STRING pkgName; _si(&pkgName, MICROSOFT_KERBEROS_NAME_A); ULONG authnPkg; _checknterr(L"LsaLookupAuthenticationPackage", LsaLookupAuthenticationPackage(hlsa, &pkgName, &authnPkg)); const DWORD cchUPN = lstrlen(pszUserPrincipalName); const DWORD cbUPN = cchUPN * sizeof(wchar_t); // KERB_S4U_LOGON must be passed as a single // contiguous buffer that includes all strings, // otherwise LsaLogonUser will complain const DWORD cbLogon = sizeof(KERB_S4U_LOGON) + cbUPN; KERB_S4U_LOGON* s4uLogon = (KERB_S4U_LOGON*)calloc(cbLogon, 1); s4uLogon->MessageType = KerbS4ULogon; s4uLogon->ClientUpn.Buffer = (wchar_t*)((char*)s4uLogon + sizeof *s4uLogon); CopyMemory(s4uLogon->ClientUpn.Buffer, pszUserPrincipalName, cbUPN); s4uLogon->ClientUpn.Length = (USHORT)cbUPN; s4uLogon->ClientUpn.MaximumLength = (USHORT)cbUPN; // this information is copied into the resulting token // note that SourceName is an 8 character ASCII buffer TOKEN_SOURCE tokenSource; AllocateLocallyUniqueId(&tokenSource.SourceIdentifier); strcpy(tokenSource.SourceName, "test"); LSA_STRING originName; _si(&originName, "MSDN S4U Logon Sample"); // finally, the call to LsaLogonUser, // asking for a Network style logon // using the S4U2Self Kerb extension void* profile = 0; DWORD cbProfile = 0; LUID logonId; HANDLE htok; QUOTA_LIMITS quotaLimits; NTSTATUS subStatus; _checknterr(L"LsaLogonUser", LsaLogonUser(hlsa, &originName, Network, authnPkg, s4uLogon, cbLogon, 0, &tokenSource, &profile, &cbProfile, &logonId, &htok, "aLimits, &subStatus)); // clean up free(s4uLogon); LsaFreeReturnBuffer(profile); LsaClose(hlsa); return htok; } void main() { HANDLE htok = _s4uLogon(L"alice@esec.com"); }
The Problem of Delegation
Delegation is another problem that has plagued developers. Even when a client is able to use Kerberos to authenticate with a service, the client's security context given to the service does not normally contain the client's network credentials. Imagine what could happen without this protection: a service, which may or may not be trusted in its own domain, could use a client's credentials from another domain to access resources it would normally not be able to access on its own. No client would ever want to use such a service, unless that service was highly trusted.
Windows 2000 introduced a feature called unconstrained delegation that allowed clients to delegate credentials to certain services that were designated as "trusted for delegation." In this case, unconstrained means that the service could use these delegated credentials anywhere on the network, all day long. Very few organizations actually used this feature, however, for two reasons. First, compromise of a service trusted for delegation means compromise of all the delegated credentials it holds, making it a virtual honeypot for attackers. Second, the local administrator of such a service must be highly trusted, lest he elevate his own privileges by misusing client credentials on the network. It was clear that this feature would be used only in a handful of rare situations.
Constrained Delegation or S4U2Proxy
The Active Directory® implementation in Windows Server 2003 has matured significantly since Windows 2000, and it supports a more reasonable delegation model known as constrained delegation. The idea is that a service may be allowed to delegate client credentials, but domain controllers will be much more selective about issuing such credentials. Thus, when service A requests Kerberos tickets for service B using delegated client credentials, the domain will consult a list to see if service A is allowed to delegate credentials to service B. In other words, the network administrator can control the scope of delegation.
Figure 2** Constrained Delegation **
An example of this would be a COM+ service hosting business logic. The service might need to access a remote database using the client's credentials. The network administrator can grant limited delegation privileges to the COM+ service, allowing it to delegate only to the database service. Figure 2 shows how this can be configured with the Active Directory Users and Computers tool. In this case, the COM+ service runs under the built-in Network Service account on a machine called AppServer, and the remote database is on a machine called DataServer. Contrast this to Windows 2000, where the only choice for delegation was a checkbox that turned unconstrained delegation on or off, as shown in Figure 3.
Figure 3** Unconstrained Delegation **
Kerberos, as defined by RFC 1510, does provide a constrained delegation feature known as a proxy ticket, but the client must request these tickets on the service's behalf. The Microsoft extension to Kerberos, known as S4U2Proxy (Service-for-User-to-Proxy), allows the service to obtain tickets on the client's behalf based on configuration settings in Active Directory. With S4U2Proxy, a service can obtain tickets to some other service (the proxy) for a user. This means the client must trust the directory (and its administrators) because the client no longer has control over whether her credentials may be delegated.
Technically, the biggest difference between normal Kerberos proxy delegation and S4U2Proxy is that in standard Kerberos the client's TGT is required to obtain proxy tickets. In S4U2Proxy, since the service is requesting the tickets on the client's behalf and the service does not have the client's TGT, the service submits a service ticket instead, which will be either the ticket the client presented to the service during normal Kerberos authentication or a ticket obtained on the client's behalf via S4U2Self. In the latter case, the client may not even have been authenticated using Kerberos, which may seem a bit odd. This particular case is known as protocol transition.
Protocol Transition
A common problem with externally facing services in Windows 2000 is client authentication and authorization. Since Kerberos authentication isn't usually an option over the Internet, these services must often use other authentication techniques such as Secure Sockets Layer (SSL) with client certificates, hand-rolled forms-based solutions, Microsoft Passport, or other proprietary authentication protocols such as SecurID from RSA Security. Once the service determines the client's identity, the challenge is to convert this knowledge into real Windows credentials that can be used to access local resources and remote services on the client's behalf.
Once again, there are two solutions to this problem in Windows 2000: either the service knows the password for an account representing the client and uses this password to obtain a Kerberos logon, or the service must scrape through Active Directory manually to figure out the group memberships for the user. Because the service couldn't use Kerberos to authenticate its client, it is effectively penalized by not being able to easily make authorization decisions based on settings in Active Directory.
Protocol Transition uses both of the S4U Kerberos extensions I've described to make it possible for a service in this position to transition from a proprietary authentication protocol to the Microsoft Kerberos implementation on the back end. Figure 4 shows what protocol transition looks like.
Figure 4** Protocol Transitioning **
Developers can use this technique by authenticating the client using whatever technique makes sense, then calling LsaLogonUser to obtain a token for the user via the S4U2Self extension that I discussed earlier. The developer writes code to impersonate the resulting token and makes authenticated requests to any of the back-end services to which it is allowed to delegate. The back-end services will think the client is accessing them—not the front-end service—and will receive a security context for the client, along with all groups and privileges that the client would have if she had authenticated directly with the back-end service. Thus the front-end server acts as a trusted point of authentication protocol transition. The network administrator controls which back-end services trust the front-end service via the constrained delegation settings in Active Directory.
Note that the network administrator must explicitly allow the server to use protocol transition by granting constrained delegation using "any authentication protocol" on the front end.
Controls and Limitations
Impersonation and delegation of a client's credentials is a sensitive matter, and several controls are maintained in Active Directory to prevent abuse of these extensions. A service that wants to use S4U2Self to obtain a token for a client must be granted permission to enumerate the groups for that client. The network administrator controls this permission via the access control list on the Active Directory user object attribute known as Token-Groups-Global-And-Universal, which isn't accessible via the Active Directory Users and Computers snap-in, but can be accessed interactively using ADSIEdit or programmatically via ADSI.
When a service requests an S4U2Self ticket for a client, if protocol transition has been enabled for that service in the directory, the resulting ticket will have the forwardable flag set. This controls whether the service can use this ticket to further obtain S4U2Proxy tickets to delegate the client's credentials to other services. If the client's account has been marked "Sensitive and cannot be delegated," the forwardable flag will not be set, and delegation will not be allowed. Even if the forwardable flag is set, S4U2Proxy tickets will only be issued for services that the requesting service is allowed to delegate to (refer back to Figure 2 to take a look at the list in Active Directory).
S4U2Proxy tickets will not work if the path from the client's domain to the target resource domain spans more than two forests. The reason for this has to do with the way cross-forest security identifier filtering works.
As I mentioned earlier, LsaLogonUser behaves differently depending on whether the caller has the TCB privilege. If the caller enables the TCB privilege before calling LsaLogonUser, it will return a token that can be used to access local resources on the machine; otherwise, this will be disallowed via the impersonation level on the token. This is orthogonal to whether the forwardable flag is set, so technically you can end up with a token that you can impersonate and use for delegation to remote services, but while impersonating you will be denied access to all local kernel objects. An example of this would be a service running as NETWORK SERVICE (as opposed to SYSTEM) that has been granted the right to use constrained delegation.
Note that for brevity I've mentioned permissions that a service must be granted. What I mean by this is that the account the service is running under must be granted the permissions. For example, if the service is running as Bob, Bob must be granted the permissions. More subtly, if the service is running as NETWORK SERVICE or SYSTEM, it is running on behalf of the machine itself, so the machine account must be granted the permissions.
Conclusion
Tightly coupling authorization with authentication has been both a blessing and a curse for Microsoft. The benefits are clear. As the client obtains Kerberos tickets, domain controllers inject group SIDs into the tickets, which eliminates the network round-trips that would be needed to get to a separate authorization service. The administration model is also simplified. The downside is that the only way to get authoritative authorization information is via Kerberos authentication, and extensions like these don't make it any easier to interoperate with other implementations of Kerberos. In addition, the bidirectional trust that must be established between forests for these features to work across forest boundaries is another potential stumbling block.
Send your questions and comments for Keith to briefs@microsoft.com.
Keith Brownis a member of the technical staff at DevelopMentor, where he spends his time researching, writing, teaching, and spreading the word about security. He is the author of Programming Windows Security (Addison Wesley, 2000). Reach Keith at https://www.develop.com/kbrown.