Kerberos SSO

Build Status Contributors Jenkins Plugin GitHub release Jenkins Plugin Installs

This plugin authenticates a user in Jenkins based on Kerberos ticket negotiation.

Summary

The Kerberos SSO plugin reads user's Kerberos ticket and logs the user into Jenkins based on that information. It is designed to work well with the Active Directory or an LDAP plugin. It can also redirect users that omit specifying a domain in their request.

The authentication can be bypassed for a specific request by setting a Bypass-Kerberos header in the request. It doesn't matter which value it has, the user will be authenticated as anonymous.

The Plugin can be configured to permit unauthenticated requests and authenticated only if requested. Otherwise, authentication is performed for every request.

Prerequisites

  • A Kerberos environment
  • krb5.conf and login.conf, both readable by Jenkins
  • A user database provided by Security Realm. The plugin will not log in a user if it's not found in a database.

Configuration as a Code

Enable Kerberos SSO:

security:
  kerberosSso:
    enabled: true
    krb5Location: '/etc/krb5.conf'
    loginLocation: '/etc/login.conf'
    loginServerModule: 'spnego-server'
    loginClientModule: 'spnego-client'
    anonymousAccess: false
    machinePrincipalPatterns:
      - 'host/*-laptop-*.remote.example.com@EXAMPLE.COM -> laptop-callbacks'
      - 'host/ci*.example.com@EXAMPLE.COM               -> ci-servers, production'
      - '*$@EXAMPLE.COM'
      - '!decommissioned$@EXAMPLE.COM'
    allowLocalhost: false
    allowBasic: true
    allowDelegation: false
    allowUnsecureBasic: false
    promptNtlm: false

Machine principals

Domain-joined hosts already hold Kerberos credentials of their own: host/fqdn@REALM from a Unix keytab, and NAME$@REALM for a Windows computer account. machinePrincipalPatterns lets those hosts call the Jenkins API as themselves, instead of being issued long-lived API tokens.

security:
  kerberosSso:
    anonymousAccess: false
    machinePrincipalPatterns:
      - 'host/*-laptop-*.example.com@EXAMPLE.COM -> laptop-callbacks'
      - 'host/ci*.example.com@EXAMPLE.COM -> ci-servers, Production'
      - '!host/retired-laptop-1.example.com@EXAMPLE.COM'

Patterns match case-insensitively against the whole principal, realm included. * is the only wildcard and is allowed only before @; every pattern must name one nonempty, literal realm. A pattern beginning with ! denies, and denial always wins, regardless of ordering. A new deny entry takes effect on the machine's next negotiated request, including requests carrying a cookie. It does not cancel builds or requests already running.

An admitted machine authenticates as its lowercased principal, for example host/agent01.example.com@example.com. A pattern may grant groups after ->, separated by commas. A machine matching several allow patterns receives the union of their groups, plus kerberos-machines. Group names keep their case. Deny patterns cannot grant groups, and authenticated is reserved and cannot be granted. In the UI, enter one pattern per line; commas separate groups within that line.

Authorization and compatibility

Use an authorization strategy with explicit user/group grants, such as Matrix Authorization. Grant only the permissions each machine needs. Existing grants to the machine's name or any assigned group apply immediately; anonymous access granted by the strategy may also apply.

Do not enable machine access with "Logged-in users can do anything" or another strategy that trusts every non-anonymous authentication. Machines are authenticated identities even though they do not carry the authenticated group authority. Omitting that authority does not constrain such strategies. Matrix Authorization is covered by the automated tests; other strategies need separate validation.

The allowlist decides, not the shape of the principal. A principal whose local part contains / or ends in $ is eligible to be matched, which covers host/..., services such as HTTP/server@REALM, and Windows computer accounts. Eligibility alone admits nothing: a principal that matches no pattern takes the security realm's user lookup exactly as it did before this feature existed. So configuring no patterns changes nothing, and a Kerberos instance name such as alice/admin@REALM keeps resolving as the person it belongs to.

A denied principal is the exception. It stays anonymous rather than falling back to the realm, otherwise revoking a computer account would restore it as an ordinary user. That also makes the stricter posture available as configuration: !*$@EXAMPLE.COM stops every Windows computer account authenticating at all, including through a realm that would otherwise resolve it.

The plugin does not create or save a Jenkins user record during machine authentication. Other Jenkins features or plugins may create records when an identity is used. Machine names and groups may not appear in authorization autocompletion; enter their exact names and select the appropriate user or group entry type.

Treat these identities as low trust: a local administrator on a domain-joined host can use that host's credentials. In particular, granting build permission on a job allows a machine to run the job's configured automation and any credentials that automation uses.

Using it

Set anonymousAccess: false so protected API requests negotiate. With anonymousAccess: true, only /login negotiates; a machine cannot log in there and then authenticate API calls using only a cookie. When machine patterns are configured, unauthenticated POSTs negotiate before Jenkins validates their crumbs. This also applies to human principals on those POSTs; normal CSRF validation still runs after authentication. Paths configured for bypass and unprotected paths such as /whoAmI skip negotiation and do not establish a machine identity. Check a protected API endpoint instead.

For a laptop callback, select project-based Matrix Authorization and configure:

  • Global Overall/Read for the group laptop-callbacks.
  • Job/Read and Job/Build for that group on the callback job only.
  • No broader grants through another matching group or machine name.

The following example requires curl with Negotiate support and jq. It acquires the host ticket, fetches a crumb, and triggers the job. Use the principal actually present in your keytab.

set -eu
kinit -k -t /etc/krb5.keytab "host/$(hostname -f)@EXAMPLE.COM"

jenkins_url='https://jenkins.example.com'
cookie_jar=$(mktemp)
trap 'rm -f "$cookie_jar"' EXIT

crumb_json=$(curl --fail --silent --show-error --negotiate -u : \
  --cookie-jar "$cookie_jar" "$jenkins_url/crumbIssuer/api/json")
crumb_field=$(printf '%s' "$crumb_json" | jq -er '.crumbRequestField')
crumb_value=$(printf '%s' "$crumb_json" | jq -er '.crumb')

curl --fail --silent --show-error --negotiate -u : \
  --cookie "$cookie_jar" --cookie-jar "$cookie_jar" \
  --header "$crumb_field: $crumb_value" \
  --request POST --dump-header - --output /dev/null \
  "$jenkins_url/job/callback/build"

A successful trigger returns 201 with a queue location. Every protected request must authenticate with Kerberos; the cookie retains the HTTP session used by Jenkins' default CSRF crumb issuer, not the machine's authentication. Fetch a new crumb if the session expires. The job's build cause records the lowercased machine principal. A job without Job/Read is hidden with 404.

User guide

The configuration page for the Kerberos Single Sign-on plugin under Configure Global Security.

The logout button is still visible for practical reasons. It refreshes the session and authentication when the user presses it, but the user is immediately logged in again so will not notice the effect.

Setup guide

This is a short list of steps and hints for setting up this plugin. Some of these tips can also be found in the help links in the configuration page. This guide is for a server using Linux.

  • The service account is only used when a keytab is not present on the server. Keytabs are probably used either everywhere or nowhere on the intranet. Although, if there are Service accounts at hand, it can only benefit the stability of the solution to provide it. Some Kerberos information sources, such as Spnego may call the service account "pre-auth username and password"

  • Location of krb5.conf defaults to "/etc/krb5.conf". Also, if the user enters other information in that field, and the file is not found at that location, it looks for the file in "/etc/krb5.conf" look for the file. If you have a Kerberos environment, probably all users have this file. The libdefaults section tells Spnego which encryption types are used in the realm and realms tells Spnego where the Key Distribution Center is located. The requests to authenticate provided Kerberos tickets are sent to that server. Jenkins needs read permissions to this file!
    The following is a complete file where sensitive data have been replaced:

    [libdefaults]
      default_realm = INTERNALDOMAIN.NET
      default_tgs_enctypes = RC4-HMAC DES-CBC-CRC DES-CBC-MD5
      default_tkt_enctypes = RC4-HMAC DES-CBC-CRC DES-CBC-MD5
      preferred_enctypes = RC4-HMAC DES-CBC-CRC DES-CBC-MD5
    [realms]
      INTERNALDOMAIN.NET = {
        kdc = 59.169.100.36
        kdc = 7b99:a413:0:ac1b::00:01
        kdc = 7b99:a413:0:ac1b::00:02
        kdc = 7b99:a413:0:ac1b::00:03
      }
    
  • Location of Login.conf is the most important part of the setup and by far the most complex. If this is wrongly specified, Jenkins will tell you immediately with a Servlet Exception. It has to point to a file on the server named "login.conf" or, commonly in documentation etc, "jaas.conf". As with the "krb5.conf" file, it's very important that Jenkins has read permissions to this file!
    The following is a complete file where sensitive data have been replaced:

    Kerberos {
      com.sun.security.auth.module.Krb5LoginModule required
      principal="HTTP/hostname01@INTERNALDOMAIN.NET"
      doNotPrompt="false"
      useTicketCache="false"
      useKeyTab="true"
      keyTab="/etc/krb5.keytab";
    };
    spnego-client {
      com.sun.security.auth.module.Krb5LoginModule required;
    };
    spnego-server {
      com.sun.security.auth.module.Krb5LoginModule required
      isInitiator="false"
      useKeyTab="true"
      keyTab="/etc/krb5.keytab"
      principal="HTTP/hostname01@INTERNALDOMAIN.NET"
      tryFirstPass="true"
      storePass="true"
      storeKey="true";
    };
    com.sun.security.jgss.initiate {
      com.sun.security.auth.module.Krb5LoginModule required
      principal="HTTP/hostname01@INTERNALDOMAIN.NET"
      useKeyTab="true"
      keyTab="/etc/krb5.keytab";
    };
    com.sun.security.jgss.accept {
      com.sun.security.auth.module.Krb5LoginModule required
      principal="HTTP/hostname01@INTERNALDOMAIN.NET"
      useKeyTab="true"
      keyTab="/etc/krb5.keytab";
    };
    

    This file will be subject to a lot of internal settings. The Krb5LoginModule is listed in every section and what it means is basically that the login module is required for each of the entries and a list of parameters follows. The important parameters we have found are: useKeyTab="true", otherwise pre-auth details will be required. A link to where a keytab is stored is also needed through the keyTab parameter.
    When setting up the server, it may be required to modify this keytab file on the server:

    # Write the following into a terminal:
    sudo net ads keytab list
    # If the server is correctly set up, the keytab will contain rows looking something like this: HTTP/HOSTNAME01@INTERNALDOMAIN.NET
    # The important part here is the HTTP principal. If it does not contain such a row, issue the following command to add it:
    sudo net ads keytab add \-P HTTP
    # Run the *list* agaom command to verify that the keytab now contains HTTP entries.

    Another important parameter is the isInitiator="false" parameter to spnego-server. In this context, the client initiates the authorization by requesting a web page.
    We have left the parameter list for the client empty. This is because Keytabs are specifically server side solutions and the principal is the client itself.
    The parameters left to experiment with are storeKey, storePass,tryFirstPass, doNotPrompt and useTicketCache. They may or may not make any difference.

  • An important thing to notice is that your servlet container, for example Tomcat, needs to have a sufficient header size. This varies between environments and is not always easy to spot as the problem when something seems wrong.