---
title: Kerberos with OpenLDAP Back-End
module: masson/core/krb5_server
layout: module
---

## Kerberos with OpenLDAP Back-End

robert.mroczkowski@allegrogroup.com

Usefull server commands:
*   Backup the db: `kdb5_util dump /path/to/dumpfile`
*   Initialize realm: `kdb5_ldap_util -D "cn=Manager,dc=adaltas,dc=com" -w test create -subtrees "ou=kerberos,ou=services,dc=adaltas,dc=com" -r ADALTAS.COM -s -P test`
*   Load the db: `kdb5_util load -update /path/to/dumpfile`
*   Stash password: `kdb5_ldap_util -D "cn=Manager,dc=adaltas,dc=com" -w test stashsrvpw -f /etc/krb5.d/stash.keyfile cn=krbadmin,ou=users,dc=adaltas,dc=com`

Resources:
*   [Kerberos with LDAP backend on centos](http://itdavid.blogspot.fr/2012/05/howto-centos-62-kerberos-kdc-with.html)
*   [Propagation](http://www-old.grantcohoe.com/guides/services/krb5-kdc)
*   [Replication](http://tldp.org/HOWTO/Kerberos-Infrastructure-HOWTO/server-replication.html)
*   [Kerberos with LDAP backend on ubuntu](http://labs.opinsys.com/blog/2010/02/05/setting-up-openldap-kerberos-on-ubuntu-10-04-lucid/)

    each = require 'each'
    misc = require 'mecano/lib/misc'
    module.exports = []

    module.exports.push 'masson/bootstrap/'
    module.exports.push 'masson/core/openldap_client'
    module.exports.push 'masson/core/iptables'
    module.exports.push 'masson/core/yum'

## Configuration

*   `krb5_server.{realm}.ldap_manager_dn` (string)   
    The LDAP user with read and write access to the realm dn
    defined by the `ldap_realms_dn` property. Default to the 
    `openldap_krb5.manager_dn` property if you have one OpenLDAP server with 
    kerberos support declared inside the cluster by the 
    "masson/core/openldap\_server\_krb5" module, otherwise required.      
*   `krb5_server.{realm}.ldap_manager_password` (string)   
    The password of the LDAP user with read and write access to the realm dn
    defined by the `ldap_realms_dn` property. Default to the 
    `openldap_krb5.manager_password` property if you have one OpenLDAP server with 
    kerberos support declared inside the cluster by the 
    "masson/core/openldap\_server\_krb5" module, otherwise required.      
*   `krb5_server.{realm}.ldap_realms_dn` (string)   
    The location where to store the realms inside the LDAP tree. Default to the 
    `openldap_krb5.realms_dn` property if you have one OpenLDAP server with 
    kerberos support declared inside the cluster by the 
    "masson/core/openldap\_server\_krb5" module, otherwise required.   

Example:

```json
{
  "krb5": {
    "ADALTAS.COM": {
      "kdc": "master3.hadoop",
      "kadmin_server": "master3.hadoop",
      "kadmin_principal": "wdavidw/admin@ADALTAS.COM",
      "kadmin_password": "test",
      "ldap\_kerberos\_container_dn": "ou=kerberos,dc=adaltas,dc=com",
      "ldap\_kdc\_dn": "cn=krbadmin,ou=users,dc=adaltas,dc=com",
      "ldap\_kadmind\_dn": "cn=krbadmin,ou=users,dc=adaltas,dc=com",
      "ldap_servers": [
        "ldaps://master3.hadoop",
      ],
      "principals": [
        "principal": "wdavidw@ADALTAS.COM",
        "password": "test"
      ]
    }
  }
}
```

    safe_etc_krb5_conf = module.exports.safe_etc_krb5_conf = (etc_krb5_conf) ->
      etc_krb5_conf = misc.merge {}, etc_krb5_conf
      for realm, config of etc_krb5_conf.realms
        delete config.kadmin_principal
        delete config.kadmin_password
        delete config.principals
      for name, config of etc_krb5_conf.dbmodules
        delete config.kdc_master_key
        delete config.manager_dn
        delete config.manager_password
      etc_krb5_conf

    module.exports.push module.exports.configure = (ctx) ->
      require('./krb5_client').configure ctx
      require('./iptables').configure ctx
      {etc_krb5_conf} = ctx.config.krb5
      openldap_hosts = ctx.hosts_with_module 'masson/core/openldap_server_krb5'
      throw new Error "Expect at least one server with action \"masson/core/openldap_server_krb5\"" if openldap_hosts.length is 0
      # Prepare configuration for "kdc.conf"
      kdc_conf = ctx.config.krb5.kdc_conf ?= {}
      misc.merge kdc_conf,
        'kdcdefaults':
          'kdc_ports': '88'
          'kdc_tcp_ports': '88'
        'realms': {}
        'logging':
            'kdc': 'FILE:/var/log/kdc.log'
      , kdc_conf
      # Multiple kerberos servers accross the cluster are defined in server
      # specific configuration
      realms = ctx.config.servers[ctx.config.host].krb5?.etc_krb5_conf?.realms
      realms = etc_krb5_conf.realms if not realms or realms.length is 0
      for realm, i of realms
        kdc_conf.realms[realm] = {}
      # Set default values each realm
      for realm, config of kdc_conf.realms
        kdc_conf.realms[realm] = misc.merge
          # 'kadmind_port': 749
          # 'kpasswd_port': 464 # http://www.opensource.apple.com/source/Kerberos/Kerberos-47/KerberosFramework/Kerberos5/Documentation/kadmin/kpasswd.protocol
          'max_life': '10h 0m 0s'
          'max_renewable_life': '7d 0h 0m 0s'
          'master_key_type': 'aes256-cts'
          'default_principal_flags': '+preauth'
          'acl_file': '/var/kerberos/krb5kdc/kadm5.acl'
          'dict_file': '/usr/share/dict/words'
          'admin_keytab': '/var/kerberos/krb5kdc/kadm5.keytab'
          'supported_enctypes': 'aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal'
        , config

## IPTables

| Service                    | Port   | Protocol | Parameter         |
|----------------------------|--------|--------|-------------------- |
| NameNode WebUI             | 50070  | http   | `dfs.http.address`  |
|                            | 50470  | https  | `dfs.https.address` |
| NameNode metadata service  | 8020   | ipc    | `fs.default.name`   |

IPTables rules are only inserted if the parameter "iptables.action" is set to 
"start" (default value).

    module.exports.push name: 'Krb5 Server # IPTables', callback: (ctx, next) ->
      {etc_krb5_conf, kdc_conf} = ctx.config.krb5
      rules = []
      add_default_kadmind_port = false
      add_default_kdc_ports = false
      add_default_kdc_tcp_ports = false
      for realm, config of kdc_conf.realms
        if config.kadmind_port
          rules.push chain: 'INPUT', target: 'ACCEPT', dport: kadmind_port, protocol: 'tcp', state: 'NEW', comment: "Kerberos administration server (kadmind daemon)"
        else add_default_kadmind_port = true
        if config.kdc_ports
          for port in config.kdc_ports.split /\s,/
            rules.push chain: 'INPUT', target: 'ACCEPT', dport: port, protocol: 'udp', state: 'NEW', comment: "Kerberos Authentication Service and Key Distribution Center (krb5kdc daemon)"
        else add_default_kdc_ports = true
        if config.kdc_tcp_ports
          kdc_tcp_ports = true
          for port in config.kdc_ports.split /\s,/
            rules.push chain: 'INPUT', target: 'ACCEPT', dport: port, protocol: 'tcp', state: 'NEW', comment: "Kerberos Authentication Service and Key Distribution Center (krb5kdc daemon)"
        else add_default_kdc_tcp_ports = true
      if add_default_kadmind_port
        port = kdc_conf.kdcdefaults.kadmind_port or '749'
        rules.push chain: 'INPUT', target: 'ACCEPT', dport: port, protocol: 'tcp', state: 'NEW', comment: "Kerberos administration server (kadmind daemon)"
      if add_default_kdc_ports
        for port in (kdc_conf.kdcdefaults.kdc_ports or '88').split /\s,/
          rules.push chain: 'INPUT', target: 'ACCEPT', dport: port, protocol: 'udp', state: 'NEW', comment: "Kerberos Authentication Service and Key Distribution Center (krb5kdc daemon)"
      if add_default_kdc_tcp_ports
        for port in (kdc_conf.kdcdefaults.kdc_tcp_ports or '88').split /\s,/
          rules.push chain: 'INPUT', target: 'ACCEPT', dport: port, protocol: 'tcp', state: 'NEW', comment: "Kerberos Authentication Service and Key Distribution Center (krb5kdc daemon)"
      ctx.iptables
        rules: rules
        if: ctx.config.iptables.action is 'start'
      , (err, configured) ->
        next err, if configured then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # LDAP Install', timeout: -1, callback: (ctx, next) ->
      ctx.service
        name: 'krb5-server-ldap'
      , (err, installed) ->
        next err, if installed then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # LDAP Configuration', timeout: 100000, callback: (ctx, next) ->
      {etc_krb5_conf} = ctx.config.krb5
      ctx.ini
        content: safe_etc_krb5_conf etc_krb5_conf
        destination: '/etc/krb5.conf'
        stringify: misc.ini.stringify_square_then_curly
        backup: true
      , (err, written) ->
        return next err if err
        next err, if written then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # LDAP Insert Entries', timeout: 100000, callback: (ctx, next) ->
      {etc_krb5_conf, kdc_conf} = ctx.config.krb5
      modified = false
      each(etc_krb5_conf.realms)
      .on 'item', (realm, config, next) ->
        return next() unless config.database_module
        {kdc_master_key, ldap_kerberos_container_dn, manager_dn, manager_password, ldap_servers} = etc_krb5_conf.dbmodules[config.database_module]
        ldap_server = ldap_servers.split(' ')[0]
        do_wait = ->
          ctx.waitForExecution 
            cmd: "ldapsearch -x -LLL -H #{ldap_server} -D \"#{manager_dn}\" -w #{manager_password} -b \"#{ldap_kerberos_container_dn}\""
            code_skipped: 32
          , (err) ->
            return next err if err
            do_exists()
        do_exists = ->
          searchbase = "cn=#{realm},#{ldap_kerberos_container_dn}"
          ctx.execute 
            cmd: "ldapsearch -x -H #{ldap_server} -D \"#{manager_dn}\" -w #{manager_password} -b \"#{searchbase}\""
            code_skipped: 32
          , (err, exists) ->
            return next err if err
            if exists then next() else do_subtrees()
        do_subtrees = ->
          # Note, kdb5_ldap_util is using /etc/krb5.conf (server version)
          ctx.execute
            cmd: "kdb5_ldap_util -D \"#{manager_dn}\" -w #{manager_password} create -subtrees \"#{ldap_kerberos_container_dn}\" -r #{realm} -s -P #{kdc_master_key}"
          , (err, executed, stdout, stderr) ->
            return next err if err
            modified = true if executed
            next()
        do_wait()
      .on 'both', (err) ->
        next err, if modified then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # LDAP Stash password', callback: (ctx, next) ->
      {etc_krb5_conf} = ctx.config.krb5
      modified = false
      each(etc_krb5_conf.dbmodules)
      .on 'item', (name, dbmodule, next) ->
        {kdc_master_key, manager_dn, manager_password, ldap_service_password_file, ldap_kadmind_dn} = dbmodule
        ctx.log "Stash key file is: #{dbmodule.ldap_service_password_file}"
        keyfileContent = null
        do_read = ->
          ctx.log 'Read current keyfile if it exists'
          ctx.fs.readFile "#{ldap_service_password_file}", 'utf8', (err, content) ->
            return do_mkdir() if err and err.code is 'ENOENT'
            return next err if err
            keyfileContent = content
            do_stash()
        do_mkdir = ->
          ctx.log 'Create directory "/etc/krb5.d"'
          ctx.mkdir '/etc/krb5.d', (err, created) ->
            return next err if err
            do_stash()
        do_stash = ->
          ctx.log 'Stash password into local file'
          ctx.ssh.shell (err, stream) ->
            return next err if err
            cmd = "kdb5_ldap_util -D \"#{manager_dn}\" -w #{manager_password} stashsrvpw -f #{ldap_service_password_file} #{ldap_kadmind_dn}"
            ctx.log "Run #{cmd}"
            reentered = false
            stream.write "#{cmd}\n"
            stream.on 'data', (data, stderr) ->
              ctx.log[if stderr then 'err' else 'out'].write data
              data = data.toString()
              if /Password for/.test data
                stream.write "#{kdc_master_key}\n"
              else if /Re-enter password for/.test data
                stream.write "#{kdc_master_key}\n\n"
                reentered = true
              else if reentered
                stream.end()
            stream.on 'close', ->
              do_compare()
        do_compare = ->
          unless keyfileContent
            modified = true
            return next()
          ctx.fs.readFile "#{ldap_service_password_file}", 'utf8', (err, content) ->
            return next err if err
            modified = if keyfileContent is content then false else true
            next()
        do_read()
      .on 'both', (err) ->
        next err, if modified then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # Install', timeout: -1, callback: (ctx, next) ->
      ctx.log 'Install krb5kdc and kadmin services'
      ctx.service [
        name: 'krb5-pkinit-openssl'
      ,
        name: 'krb5-server-ldap'
        startup: true
        chk_name: 'krb5kdc'
        srv_name: 'krb5kdc'
      ,
        name: 'krb5-server-ldap'
        startup: true
        chk_name: 'kadmin'
        srv_name: 'kadmin'
      ,
        name: 'words'
      ,
        name: 'krb5-workstation'
      ], (err, serviced) ->
        next err, if serviced then ctx.OK else ctx.PASS

    module.exports.push name: 'Krb5 Server # Configure', timeout: 100000, callback: (ctx, next) ->
      {realm, etc_krb5_conf, kdc_conf} = ctx.config.krb5
      modified = false
      exists = false
      do_exists = ->
        ctx.fs.exists '/etc/krb5.conf', (err, e) ->
          exists = e
          do_krb5()
      do_krb5 = ->
        ctx.log 'Update /etc/krb5.conf'
        # Clone etc_krb5_conf
        etc_krb5_conf = misc.merge {}, etc_krb5_conf
        ctx.ini
          content: safe_etc_krb5_conf etc_krb5_conf
          destination: '/etc/krb5.conf'
          stringify: misc.ini.stringify_square_then_curly
          backup: true
        , (err, written) ->
          return next err if err
          modified = true if written
          do_kadm5()
      do_kadm5 = ->
        ctx.log 'Update /var/kerberos/krb5kdc/kadm5.acl'
        writes = for realm of etc_krb5_conf.realms
          match: ///^\*/\w+@#{misc.regexp.escape realm}\s+\*///mg
          replace: "*/admin@#{realm}     *"
          append: true
        ctx.write 
          write: writes
          destination: '/var/kerberos/krb5kdc/kadm5.acl'
          backup: true
        , (err, written) ->
          return next err if err
          modified = true if written
          do_kdc()
      do_kdc = ->
        ctx.log 'Update /var/kerberos/krb5kdc/kdc.conf'
        ctx.ini
          content: kdc_conf
          destination: '/var/kerberos/krb5kdc/kdc.conf'
          stringify: misc.ini.stringify_square_then_curly
          backup: true
        , (err, written) ->
          return next err if err
          modified = true if written
          do_end()
      do_end = (err) ->
        return next err if err
        return next null, ctx.PASS unless modified
        # The first time, we dont restart because ldap conf is 
        # not there yet
        return next null, ctx.OK unless exists
        ctx.log '(Re)start krb5kdc and kadmin services'
        ctx.service [
          name: 'krb5-server'
          action: 'restart'
          srv_name: 'krb5kdc'
        ,
          name: 'krb5-server'
          action: 'restart'
          srv_name: 'kadmin'
        ], (err, serviced) ->
          next err, ctx.OK
      do_exists()

    module.exports.push name: 'Krb5 Server # Log', timeout: 100000, callback: (ctx, next) ->
      modified = false
      touch = ->
        ctx.log 'Touch "/etc/logrotate.d/krb5kdc" and "/etc/logrotate.d/kadmind"'
        ctx.write [
          content: ''
          destination: '/var/log/krb5kdc.log'
          not_if_exists: true
        ,
          content: ''
          destination: '/var/log/kadmind.log'
          not_if_exists: true
        ], (err, written) ->
          return done err if err
          modified = true if written
          rsyslog()
      rsyslog = ->
        ctx.log 'Update /etc/rsyslog.conf'
        ctx.write
          destination: '/etc/rsyslog.conf'
          write: [
            match: /.*krb5kdc.*/mg
            replace: 'if $programname == \'krb5kdc\' then /var/log/krb5kdc.log'
            append: '### RULES ###'
          ,
            match: /.*kadmind.*/mg
            replace: 'if $programname == \'kadmind\' then /var/log/kadmind.log'
            append: '### RULES ###'
          ]
        , (err, written) ->
          return done err if err
          modified = true if written
          if written then restart() else done()
      restart = ->
        ctx.log 'Restart krb5kdc and kadmin'
        ctx.service [
          name: 'krb5-server'
          action: 'start'
          srv_name: 'krb5kdc'
        ,
          name: 'krb5-server'
          action: 'start'
          srv_name: 'kadmin'
        ], (err, restarted) ->
          return done err if err
          ctx.log 'Restart rsyslog'
          ctx.service
            name: 'rsyslog'
            action: 'restart'
          , (err, restarted) ->
            done err
      done = (err) ->
        next err, if modified then ctx.OK else ctx.PASS
      touch()

    module.exports.push name: 'Krb5 Server # Admin principal', timeout: -1, callback: (ctx, next) ->
      {etc_krb5_conf} = ctx.config.krb5
      modified = false
      each(etc_krb5_conf.realms)
      .on 'item', (realm, config, next) ->
        {database_module, kadmin_principal, kadmin_password} = config
        return next() unless database_module
        ctx.log "Create principal #{kadmin_principal}"
        ctx.krb5_addprinc
          # We dont provide an "kadmin_server". Instead, we need
          # to use "kadmin.local" because the principal used
          # to login with "kadmin" isnt created yet
          principal: kadmin_principal
          password: kadmin_password
        , (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

    module.exports.push name: 'Krb5 Server # Start', timeout: 100000, callback: (ctx, next) ->
      ctx.service [
        name: 'krb5-server-ldap'
        action: 'start'
        srv_name: 'krb5kdc'
      ,
        name: 'krb5-server-ldap'
        action: 'start'
        srv_name: 'kadmin'
      ], (err, serviced) ->
        next err, if serviced then ctx.OK else ctx.PASS

## Krb5 Client

Call the "masson/core/krb5_client" dependency which will register this host to
each Kerberos servers.

    module.exports.push '!masson/core/krb5_client'

## Notes

Renewable tickets is per default disallowed in the most linux distributions. This can be done per:

```bash
kadmin.local: modprinc -maxrenewlife 7day krbtgt/YOUR_REALM
kadmin.local: modprinc -maxrenewlife 7day +allow_renewable hue/FQRN
```





