When using referrals, the TGS client starts the referrals chasing process at the client principal's realm. That is, the desired service principal's realm is replaced with the client principal's and a TGS-REQ is sent to that realm's TGS.
There are two cases where this choice of "start" realm are inappropriate:
- when the client principal's realm is a WELLKNOWN realm (e.g., the anonymous realm) - when the client has a "root TGT" (krbtgt/REALM@REALM) but not for its own realm
The latter is used at some sites as a form of constrained credential delegation: a client at realm A forwards a ticket for krbtgt/B@B to a service in realm B, thus that service cannot reach realm A services (because of loop detection at A's TGSes), but it can still authenticate as the client @A. (The services in realm B can probably also not reach other realms that are reachable from A.)
The TGS client really needs a way to record the "start" realm where the TGT for that realm is stored: in the ccache.
Recording the start realm in the ccache
Searching for a root TGT in the ccache is not a good way to find a start realm: there is no guarantee that there will be only one such TGT, and there is no guarantee that the ccache preserves insertion order, therefore such a search could yield non-deterministic results.
The proposed solution is: record the start realm in a "cc config" ccache entry. Any process that initializes a ccache should add this cc config entry along with the TGT that it stores in the ccache. When trying referrals, the client should search for this cc config entry, and if it finds it, it should use the realm recorded in it as the "start realm".
Heimdal (as of May 2015) automatically sets start_realm at the ccache dispatch layer using the following heuristic: when a cache is initialized, a flag is set on the in-memory cache handle. When a local TGT is stored, add a start_realm config entry and clear the flag. Also clear the flag if a start_realm config entry is explicitly stored. This method can create duplicate entries during ccache copies.
The new cc config name should be "start_realm", and the value stored there should be the start realm's name.
An alternative: starting ticket
https://github.com/heimdal/heimdal/pull/577 raised a similar issue: anonymous tickets cannot be renewed with the current implementation of krb5_get_renewed_creds(), because it assumes that the ticket to be renewed has the same service realm as the client principal. At first, this seemed like another application of start_realm, but it was noted that if a ccache is obtained with a cross TGT as the starting ticket, the renewal realm differs from the realm that should be consulted for referrals or to begin cross-realm ticket acquisition.
Essentially all credential caches are created with one starting ticket that cannot be trivially replaced. Ephemeral tickets may be added which can easily be replaced via TGS requests. The starting ticket is commonly an initial ticket (obtained via kinit), but may not be--the initial ticket might be removed, or a cache might be created containing only a service ticket obtained via TGS request from another cache.
The current ccache API does not identify starting tickets in either direction. Ephemeral tickets are obtained through krb5_get_credentials(), which could be changed to identify the cache entries as ephemeral. Credential cache operations might lose that information if the cache format does not identify the starting ticket. (Note that the current MIT krb5 MEMORY ccache implementation reverses the order of credential entries when iterating, so the first non-config entry during iteration is not necessarily the starting ticket.)
Distinguishing between the starting ticket and ephemeral tickets can be valuable to a ccache implementation; for instance, a file-based cache type could store ephemeral tickets in a hash table and simply overwrite entries on hash collisions.
If a ccache consumer could identify the starting ticket, it would solve the start_realm problem and could simplify the krb5_get_renewed_creds() API by correctly identifying the ticket to be renewed without any information from the caller.