xrealmauthz, a KDC policy plugin upstreamed into MIT Kerberos, authorizes cross-realm clients per realm or per principal at ticket issuance.

~/blog $ cat kerberos-cross-realm-authorization-xrealmauthz.md Dax Kelson 6 min read

A KDC policy plugin for cross-realm authorization in MIT Kerberos

Last year I helped a Fortune 50 client modernize the largest MIT Kerberos deployment I've worked on: tens of thousands of Windows, Linux, and UNIX hosts across several realms. Their security policy required authorization checks at the KDC itself, which they had built by patching the MIT KDC source and running a modified KDC in-house. That patch pinned them to an old MIT Kerberos release and made version upgrades and security fixes hard to apply. I wrote the authorization logic as a kdcpolicy plugin, xrealmauthz, so they could run stock MIT Kerberos and retire the in-house fork, and submitted it upstream; it merged in June 2025 (krb5 PR #1431).

Kerberos authenticates but does not authorize: the KDC proves who you are and issues tickets, and each service decides what you may do with one. At the ticket level, trusting another realm trusts every principal in it, and every principal in the realms it transitively trusts. With xrealmauthz, a stock MIT Kerberos KDC can authorize cross-realm clients per realm or per principal at the moment it issues the ticket. For the first time, the KDC itself makes that decision about principals from other realms, instead of leaving it to each service.

How cross-realm trust works

A cross-realm trust is a shared key, stored as a krbtgt/REALM1@REALM2 principal in both databases. Once it exists, any principal that authenticated to REALM2 can present its TGT to REALM1's KDC and receive a REALM1 service ticket. [capaths] constrains the transit path and permits transitive trust (REALM3 via REALM2), but nothing constrains which foreign principal is asking.

The kdcpolicy hook

The kdcpolicy plugin interface (krb5 1.16) exposes two entry points, check_as and check_tgs, that the KDC calls before issuing a ticket. A module returns success to allow, KRB5KDC_ERR_POLICY to deny, and may shorten ticket lifetimes. Until this plugin the only in-tree module was a test stub; nothing shipped used the hook for a real decision.

Cross-realm requests always arrive as a TGS-REQ carrying a cross-realm TGT, so xrealmauthz implements check_tgs only. It runs after the KDC has validated the cross-realm TGT and the transited path, as the last gate before issuance:

%%{init: {'theme':'base','fontFamily':'Open Sans, sans-serif','themeVariables':{'fontFamily':'Open Sans, sans-serif','fontSize':'16px','lineColor':'#5CC8FF','edgeLabelBackground':'transparent','clusterBkg':'transparent','clusterBorder':'#33485c','titleColor':'#CFE4FA'},'flowchart':{'htmlLabels':true,'useMaxWidth':true,'nodeSpacing':48,'rankSpacing':52,'curve':'basis'}}}%%
flowchart LR
    subgraph B["Existing KDC cross-realm flow"]
        direction TB
        bC["Client (REALM2)"] --> bK["KDC (REALM1)"]
        bK --> bT["KDC issues service ticket"]
        bT --> bS["Service (REALM1)"]
    end
    subgraph A["KDC using my xrealmauthz plugin"]
        direction TB
        aC["Client (REALM2)"] --> aK["KDC (REALM1)"]
        aK -->|"check_tgs"| aX["<u><b>xrealmauthz</b></u><div style='text-align:left;white-space:nowrap;font-weight:400;margin-top:7px;line-height:1.5'>1. client realm pre-approved?<br>2. xr:@CLIENTREALM on krbtgt entry?<br>3. xr:CLIENTPRINC on krbtgt entry?</div>"]
        aX -->|"a rule matches"| aT["KDC issues service ticket"]
        aX -->|"no rule matches"| aD["<b>Policy error</b><div style='font-weight:400;margin-top:3px'>KRB5KDC_ERR_POLICY</div>"]
        aT --> aS["Service (REALM1)"]
    end

    bC ~~~ aC

    classDef infra fill:#0B3A5E,stroke:#5CC8FF,stroke-width:2px,color:#FFFFFF
    classDef plugin fill:#FBE3B3,stroke:#ED8C0C,stroke-width:2.5px,color:#3A2A08
    classDef deny fill:#3A1212,stroke:#E06A6A,stroke-width:1.6px,color:#FFD9D9
    class bC,bK,bT,bS,aC,aK,aT,aS infra
    class aX plugin
    class aD deny
    linkStyle default stroke:#5CC8FF,color:#CFE4FA

Cross-realm TGS request flow, without and with xrealmauthz. In the existing flow (left), REALM1's KDC issues a service ticket to any cross-realm client. With the plugin (right), the amber check_tgs gate runs its rule checks and either issues the ticket or returns a policy error.

Authorization rules live on the krbtgt entry

The rules are string attributes on the krbtgt/MYREALM@OTHERREALM entry, read with krb5_dbe_get_string (set with kadmin setstr) and namespaced with an xr: prefix. This needs no new config file and no schema change: every KDB backend (DB2, LMDB, LDAP) supports string attributes, and the rules replicate with the database. They also attach to the exact trust edge a request crosses, which is what makes transitive authorization work.

Enable the module in kdc.conf:

[plugins]
    kdcpolicy = {
        module = xrealmauthz:/usr/lib/krb5/plugins/kdcpolicy/xrealmauthz.so
    }

Realm and principal rules

Two rule forms live under xr:. xr:@CLIENTREALM authorizes every principal from a realm; xr:PRINC authorizes one client. For a principal rule, omit the realm when the client is in the krbtgt's far-end realm, and include it otherwise:

# Authorize every principal in REALM2.COM
kadmin.local -r REALM1.COM setstr krbtgt/[email protected] xr:@REALM2.COM ""

# Authorize one principal arriving directly from REALM2.COM
kadmin.local -r REALM1.COM setstr krbtgt/[email protected] xr:dkelson ""

# Authorize one principal arriving transitively from REALM3.COM via REALM2.COM
kadmin.local -r REALM1.COM setstr krbtgt/[email protected] xr:[email protected] ""

The third command is the transitive case. A REALM3 client reaching REALM1 still presents a krbtgt/REALM1@REALM2 ticket, because REALM2 is the realm it transited, so the rule attaches to the REALM1-to-REALM2 krbtgt and carries an explicit @REALM3.COM. A direct REALM2 client on the same entry needs no realm part, so xr:dkelson is unambiguous.

%%{init: {'theme':'base','fontFamily':'Open Sans, sans-serif','themeVariables':{'fontFamily':'Open Sans, sans-serif','fontSize':'16px','lineColor':'#5CC8FF','edgeLabelBackground':'transparent'},'flowchart':{'htmlLabels':true,'useMaxWidth':true,'nodeSpacing':55,'rankSpacing':70,'curve':'basis'}}}%%
flowchart LR
    R3["<b>REALM3</b><div style='font-weight:400;margin-top:3px'>jkelson authenticates here</div>"] -->|"cross-realm TGT"| R2["<b>REALM2</b><div style='font-weight:400;margin-top:3px'>transited</div>"]
    R2 -->|"krbtgt/REALM1@REALM2"| R1["<b>REALM1</b><div style='font-weight:400;margin-top:3px'>target KDC</div>"]
    R1 -.->|"reads the rule from"| E["krbtgt/REALM1@REALM2 entry<div style='font-weight:400;margin-top:3px'>xr:[email protected]</div>"]

    classDef infra fill:#0B3A5E,stroke:#5CC8FF,stroke-width:2px,color:#FFFFFF
    classDef accent fill:#FBE3B3,stroke:#ED8C0C,stroke-width:2.5px,color:#3A2A08
    class R3,R2,R1 infra
    class E accent
    linkStyle default stroke:#5CC8FF,color:#CFE4FA

The transitive case. jkelson authenticates in REALM3 and transits REALM2, so the ticket reaching REALM1 is krbtgt/REALM1@REALM2. The amber entry is where the rule lives: it attaches to the transited krbtgt and names the origin principal explicitly with @REALM3.COM.

Pre-approved realms in kdc.conf

A realm you trust on any path can be named in [kdcdefaults] instead of tagged on each krbtgt entry. xrealmauthz_allowed_realms lists client realms that bypass the per-entry checks: when the client's realm matches, the request is allowed before the krbtgt string attributes are read. Repeat the key once per realm:

[kdcdefaults]
    xrealmauthz_enforcing = true
    xrealmauthz_allowed_realms = REALM2.COM
    xrealmauthz_allowed_realms = REALM3.COM

This is coarser than the xr: rules: it authorizes an entire client realm regardless of trust path, so reserve it for realms you would otherwise have to tag on every krbtgt. The [capaths] transited check still applies; pre-approval only skips this module's own per-entry lookup.

Enforcing and monitoring

Enforcement is on by default. Unauthorized cross-realm clients are denied, and a database error during the attribute lookup returns non-zero, which the KDC treats as a denial, so the lookup fails closed. Set xrealmauthz_enforcing = false for monitoring mode, which permits every cross-realm request but logs the ones it would deny:

xrealmauthz module would deny [email protected] for host/[email protected] from REALM2.COM

Each line names the client, the requested service, and the transited realm: enough to author the setstr rules and xrealmauthz_allowed_realms entries before switching enforcement on. The module is not built or loaded by default. Build it, point kdcpolicy at the .so, and start in monitoring mode.

Kerberos is one of the topics we teach in depth in GL550, our Enterprise Linux Security Administration course, woven through the material wherever it is relevant: deploying Kerberos 5, GSSAPI, and Kerberizing services, alongside PAM and password auditing, SELinux, the kernel audit framework, and TLS hardening for Apache and PostgreSQL. The labs run on RHEL or SLES.