---
title: Kerberos Client
module: masson/core/krb5_client
layout: module
---

# Kerberos Client

Kerberos is a network authentication protocol. It is designed 
to provide strong authentication for client/server applications 
by using secret-key cryptography.

This module install the client tools written by the [Massachusetts 
Institute of Technology](http://web.mit.edu).

    each = require 'each'
    misc = require 'mecano/lib/misc'
    krb5_server = require './krb5_server'
    module.exports = []
    module.exports.push 'masson/bootstrap/'
    module.exports.push 'masson/bootstrap/utils'
    module.exports.push 'masson/core/yum'
    module.exports.push 'masson/core/ssh'
    module.exports.push 'masson/core/ntp'
    module.exports.push 'masson/core/openldap_client'

## Configuration

*   `krb5.kadmin_principal` (string, required)
*   `krb5.kadmin_password` (string, required)
*   `krb5.kadmin_server` (string, required)
*   `krb5.realm` (string, required)
*   `krb5.etc_krb5_conf` (object)
    Object representing the full ini file in "/etc/krb5.conf". It is
    generated by default.
*   `krb5.sshd` (object)
    Properties inserted in the "/etc/ssh/sshd_config" file.

Example:
```json
{
  "krb5": {
    "realm": "ADALTAS.COM",
    "kdc": "krb5.hadoop",
    "kadmin_server": "krb5.hadoop",
    "kadmin_principal": "wdavidw/admin@ADALTAS.COM",
    "kadmin_password": "test",
    "sshd": {
      "ChallengeResponseAuthentication: "yes",
      "KerberosAuthentication: "yes",
      "KerberosOrLocalPasswd: "yes",
      "KerberosTicketCleanup: "yes",
      "GSSAPIAuthentication: "yes",
      "GSSAPICleanupCredentials: "yes"
    }
  }
}
```

    safe_etc_krb5_conf = module.exports.safe_etc_krb5_conf = (etc_krb5_conf) ->
      etc_krb5_conf = krb5_server.safe_etc_krb5_conf etc_krb5_conf
      for realm, config of etc_krb5_conf.realms
        delete config.database_module
      delete etc_krb5_conf.dbmodules
      etc_krb5_conf

    module.exports.etc_krb5_conf =
      'logging':
        'default': 'SYSLOG:INFO:LOCAL1'
        'kdc': 'SYSLOG:NOTICE:LOCAL1'
        'admin_server': 'SYSLOG:WARNING:LOCAL1'
      'libdefaults': 
        # 'default_realm': "#{REALM}"
        'dns_lookup_realm': false
        'dns_lookup_kdc': false
        'ticket_lifetime': '24h'
        'renew_lifetime': '7d'
        'forwardable': true
      'realms': {}
      'domain_realm': {}
      'appdefaults':
        'pam':
          'debug': false
          'ticket_lifetime': 36000
          'renew_lifetime': 36000
          'forwardable': true
          'krb4_convert': false
      'dbmodules': {}

    module.exports.push module.exports.configure = (ctx) ->
      ctx.config.krb5.sshd ?= {}
      ctx.config.krb5 ?= {}
      etc_krb5_conf = misc.merge {}, module.exports.etc_krb5_conf, ctx.config.krb5.etc_krb5_conf
      ctx.config.krb5.etc_krb5_conf = etc_krb5_conf
      openldap_hosts = ctx.hosts_with_module 'masson/core/openldap_server_krb5'
      # Generate dynamic "krb5.dbmodules" object
      for host in openldap_hosts
        {kerberos_container_dn, users_container_dn, manager_dn, manager_password} = ctx.hosts[host].config.openldap_krb5
        name = "openldap_#{host.split('.')[0]}"
        scheme = if ctx.hosts[host].has_module 'masson/core/openldap_server_tls' then "ldap://" else "ldaps://"
        ldap_server =  "#{scheme}#{host}"
        etc_krb5_conf.dbmodules[name] = misc.merge
          'db_library': 'kldap'
          'ldap_kerberos_container_dn': kerberos_container_dn
          'ldap_kdc_dn': users_container_dn
           # this object needs to have read rights on
           # the realm container, principal container and realm sub-trees
          'ldap_kadmind_dn': users_container_dn
           # this object needs to have read and write rights on
           # the realm container, principal container and realm sub-trees
          'ldap_service_password_file': "/etc/krb5.d/#{name}.stash.keyfile"
          # 'ldap_servers': 'ldapi:///'
          'ldap_servers': ldap_server
          'ldap_conns_per_server': 5
          'manager_dn': manager_dn
          'manager_password': manager_password
        , etc_krb5_conf.dbmodules[name]
        ldapservers = etc_krb5_conf.dbmodules[name].ldap_servers
        etc_krb5_conf.dbmodules[name].ldap_servers = ldapservers.join ' ' if Array.isArray ldapservers
      # Merge global with server-based configuration
      krb5_server_hosts = ctx.hosts_with_module "masson/core/krb5_server"
      for krb5_server_host in krb5_server_hosts
        {realms} = misc.merge {}, ctx.hosts[krb5_server_host].config.krb5.etc_krb5_conf
        for realm, config of realms
          delete config.database_module
          realms[realm].kdc ?= krb5_server_host
          realms[realm].kdc = [realms[realm].kdc] unless Array.isArray realms[realm].kdc
          realms[realm].admin_server ?= krb5_server_host
          realms[realm].default_domain ?= realm.toLowerCase()
        misc.merge etc_krb5_conf.realms, realms
      for realm, config of etc_krb5_conf.realms
        # Check if realm point to a database_module
        if config.database_module
          # Make sure this db module is registered
          dbmodules = Object.keys(etc_krb5_conf.dbmodules).join ','
          valid = etc_krb5_conf.dbmodules[config.database_module]?
          throw new Error "Property database_module \"#{config.database_module}\" not in list: \"#{dbmodules}\"" unless valid
        # Set a database module if we manage the realm locally
        if config.admin_server is ctx.config.host
          # Valid if
          # *   only one OpenLDAP server accross the cluster or
          # *   an OpenLDAP server in this host
          openldap_index = openldap_hosts.indexOf ctx.config.host
          openldap_host = if openldap_hosts.length is 1 then openldap_hosts[0] else if openldap_index isnt -1 then openldap_hosts[openldap_index]
          throw new Error "Could not find a suitable OpenLDAP server" unless openldap_host
          config.database_module = "openldap_#{openldap_host.split('.')[0]}"
        config.principals ?= []
      # Now that we have db_modules and realms, filter and validate the used db_modules
      database_modules = for realm, config of etc_krb5_conf.realms
        config.database_module
      for name, config of etc_krb5_conf.dbmodules
        # Filter
        if database_modules.indexOf(name) is -1
          delete etc_krb5_conf.dbmodules[name]
          continue
        # Validate
        throw new Error "Kerberos property `krb5.dbmodules.#{name}.kdc_master_key` is required" unless config.kdc_master_key
        throw new Error "Kerberos property `krb5.dbmodules.#{name}.ldap_kerberos_container_dn` is required" unless config.ldap_kerberos_container_dn
        throw new Error "Kerberos property `krb5.dbmodules.#{name}.ldap_kdc_dn` is required" unless config.ldap_kdc_dn
        throw new Error "Kerberos property `krb5.dbmodules.#{name}.ldap_kadmind_dn` is required" unless config.ldap_kadmind_dn
      # Generate the "domain_realm" property
      for realm of etc_krb5_conf.realms
        etc_krb5_conf.domain_realm[".#{realm.toLowerCase()}"] = realm
        etc_krb5_conf.domain_realm["#{realm.toLowerCase()}"] = realm

## Install

The package "krb5-workstation" is installed.

    module.exports.push name: 'Krb5 Client # Install', timeout: -1, callback: (ctx, next) ->
      ctx.service
        name: 'krb5-workstation'
      , (err, serviced) ->
        next err, if serviced then ctx.OK else ctx.PASS

## Configure

Modify the Kerberos configuration file in "/etc/krb5.conf". Note, 
this action wont be run if the server host a Kerberos server. 
This is to avoid any conflict where both modules would try to write 
their own configuration one. We give the priority to the server module 
which create a Kerberos file with complementary information.

    module.exports.push name: 'Krb5 Client # Configure', timeout: -1, callback: (ctx, next) ->
      # Kerberos config is also managed by the kerberos server action.
      ctx.log 'Check who manage /etc/krb5.conf'
      return next null, ctx.INAPPLICABLE if ctx.has_module 'masson/core/krb5_server'
      {etc_krb5_conf} = ctx.config.krb5
      ctx.log 'Update /etc/krb5.conf'
      ctx.ini
        content: safe_etc_krb5_conf etc_krb5_conf
        destination: '/etc/krb5.conf'
        stringify: misc.ini.stringify_square_then_curly
      , (err, written) ->
        return next err, if written then ctx.OK else ctx.PASS

## Host Principal

Create a user principal for this host. The principal is named like "host/{hostname}@{realm}".

    module.exports.push name: 'Krb5 Client # Host Principal', timeout: -1, callback: (ctx, next) ->
      {etc_krb5_conf} = ctx.config.krb5
      modified = false
      each(etc_krb5_conf.realms)
      .on 'item', (realm, config, next) ->
        {kadmin_principal, kadmin_password, admin_server} = config
        cmd = misc.kadmin
          realm: realm
          kadmin_principal: kadmin_principal
          kadmin_password: kadmin_password
          kadmin_server: admin_server
        , 'listprincs'
        ctx.waitForExecution cmd, (err) ->
          return next err if err
          ctx.krb5_addprinc
            principal: "host/#{ctx.config.host}@#{realm}"
            randkey: true
            # kadmin_principal: kadmin_principal if admin_server isnt ctx.config.host
            # kadmin_password: kadmin_password if admin_server isnt ctx.config.host
            # kadmin_server: admin_server if admin_server isnt ctx.config.host
            kadmin_principal: kadmin_principal
            kadmin_password: kadmin_password
            kadmin_server: admin_server
          , (err, created) ->
            return next err if err
            modified = true if created
            next()
      .on 'both', (err) ->
        next err, if modified then ctx.OK else ctx.PASS

## principals

Populate the Kerberos database with new principals.

    module.exports.push name: 'Krb5 Client # Principals', callback: (ctx, next) ->
      {etc_krb5_conf} = ctx.config.krb5
      modified = false
      utils = require 'util'
      each(etc_krb5_conf.realms)
      .on 'item', (realm, config, next) ->
        {kadmin_principal, kadmin_password, admin_server, principals} = config
        return next() if principals.length is 0
        principals = for principal in principals
          misc.merge
            kadmin_principal: kadmin_principal
            kadmin_password: kadmin_password
            kadmin_server: admin_server
          , principal
        ctx.log "Create principal #{principal.principal}"
        ctx.krb5_addprinc principals, (err, created) ->
          return next err if err
          modified = true if created
          next()
      .on 'both', (err) ->
        next err, if modified then ctx.OK else ctx.PASS

## Configure SSHD

Updated the "/etc/ssh/sshd\_config" file with properties provided by the "krb5.sshd" 
configuration object. By default, we set the following properties to "yes": "ChallengeResponseAuthentication",
"KerberosAuthentication", "KerberosOrLocalPasswd", "KerberosTicketCleanup", "GSSAPIAuthentication", 
"GSSAPICleanupCredentials". The "sshd" service will be restarted if a change to the configuration is detected.

    module.exports.push name: 'Krb5 Client # Configure SSHD', timeout: -1, callback: (ctx, next) ->
      {sshd} = ctx.config.krb5
      return next null, ctx.DISABLED unless sshd
      write = for k, v of sshd
        match: new RegExp "^#{k}.*$", 'mg'
        replace: "#{k} #{v}"
        append: true
      return next null, ctx.DISABLED if write.length is 0
      ctx.log 'Write /etc/ssh/sshd_config'
      ctx.write
        write: write
        destination: '/etc/ssh/sshd_config'
      , (err, written) ->
        return next err if err
        return next null, ctx.PASS unless written
        ctx.log 'Restart openssh'
        ctx.service
          name: 'openssh'
          srv_name: 'sshd'
          action: 'restart'
        , (err, restarted) ->
          next err, ctx.OK

## Usefull client commands

*   List all the current principals in the realm: `getprincs`
*   Login to a local kadmin: `kadmin.local`
*   Login to a remote kadmin: `kadmin -p wdavidw/admin@ADALTAS.COM -s krb5.hadoop`
*   Print details on a principal: `getprinc host/hadoop1.hadoop@ADALTAS.COM`
*   Examine the content of the /etc/krb5.keytab: `klist -etk /etc/krb5.keytab`
*   Destroy our own tickets: `kdestroy`
*   Get a user ticket: `kinit -p wdavidw@ADALTAS.COM`
*   Confirm that we do indeed have the new ticket: `klist`
*   Check krb5kdc is listening: `netstat -nap | grep :750` and `netstat -nap | grep :88`

## Todo

*   Enable sshd(8) Kerberos authentication.
*   Enable PAM Kerberos authentication.
*   SASL GSSAPI OpenLDAP authentication.
*   Use SASL GSSAPI Authentication with AutoFS.

## Notes

Kerberos clients require connectivity to the KDC's TCP ports 88 and 749.


