86baa9
From baf93c9bdfcb761ab469efdb2a1ba4ae8485f165 Mon Sep 17 00:00:00 2001
86baa9
From: Alexander Bokovoy <abokovoy@redhat.com>
86baa9
Date: Fri, 22 Mar 2019 19:02:16 +0200
86baa9
Subject: [PATCH] upgrade: upgrade existing trust agreements to new layout
86baa9
86baa9
Existing trust agreements will lack required Kerberos principals and
86baa9
POSIX attributes expected to allow Active Directory domain controllers
86baa9
to query IPA master over LSA and NETLOGON RPC pipes.
86baa9
86baa9
Upgrade code is split into two parts:
86baa9
 - upgrade trusted domain object to have proper POSIX attributes
86baa9
 - generate required Kerberos principals for AD DC communication
86baa9
86baa9
Fixes: https://pagure.io/freeipa/issue/6077
86baa9
(cherry picked from commit 090e09df130f27fb30a986529ef0944383e668e1)
86baa9
86baa9
Reviewed-By: Christian Heimes <cheimes@redhat.com>
86baa9
---
86baa9
 ipaserver/install/plugins/adtrust.py | 371 ++++++++++++++++++++++++---
86baa9
 1 file changed, 329 insertions(+), 42 deletions(-)
86baa9
86baa9
diff --git a/ipaserver/install/plugins/adtrust.py b/ipaserver/install/plugins/adtrust.py
86baa9
index 1f50bef891770c53a9086c7aa36d0ee1f088fbe6..1461e000dc855a21665eb5ea0cfe4a47df419344 100644
86baa9
--- a/ipaserver/install/plugins/adtrust.py
86baa9
+++ b/ipaserver/install/plugins/adtrust.py
86baa9
@@ -1,30 +1,27 @@
86baa9
-# Authors:
86baa9
-#   Martin Kosek <mkosek@redhat.com>
86baa9
-#
86baa9
-# Copyright (C) 2012  Red Hat
86baa9
-# see file 'COPYING' for use and warranty information
86baa9
-#
86baa9
-# This program is free software; you can redistribute it and/or modify
86baa9
-# it under the terms of the GNU General Public License as published by
86baa9
-# the Free Software Foundation, either version 3 of the License, or
86baa9
-# (at your option) any later version.
86baa9
-#
86baa9
-# This program is distributed in the hope that it will be useful,
86baa9
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
86baa9
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
86baa9
-# GNU General Public License for more details.
86baa9
-#
86baa9
-# You should have received a copy of the GNU General Public License
86baa9
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
86baa9
+# Copyright (C) 2012-2019  FreeIPA Contributors see COPYING for license
86baa9
 
86baa9
+from __future__ import absolute_import
86baa9
 import logging
86baa9
 
86baa9
 from ipalib import Registry, errors
86baa9
 from ipalib import Updater
86baa9
 from ipapython.dn import DN
86baa9
+from ipapython import ipautil
86baa9
+from ipaplatform.paths import paths
86baa9
 from ipaserver.install import sysupgrade
86baa9
 from ipaserver.install.adtrustinstance import (
86baa9
     ADTRUSTInstance, map_Guests_to_nobody)
86baa9
+from ipaserver.dcerpc_common import TRUST_BIDIRECTIONAL
86baa9
+
86baa9
+try:
86baa9
+    from samba.ndr import ndr_unpack
86baa9
+    from samba.dcerpc import lsa, drsblobs
86baa9
+except ImportError:
86baa9
+    # If samba.ndr is not available, this machine is not provisioned
86baa9
+    # for serving a trust to Active Directory. As result, it does
86baa9
+    # not matter what ndr_unpack does but we save on pylint checks
86baa9
+    def ndr_unpack(x):
86baa9
+        raise NotImplementedError
86baa9
 
86baa9
 logger = logging.getLogger(__name__)
86baa9
 
86baa9
@@ -325,6 +322,28 @@ class update_sids(Updater):
86baa9
         return False, ()
86baa9
 
86baa9
 
86baa9
+def get_gidNumber(ldap, env):
86baa9
+    # Read the gidnumber of the fallback group and returns a list with it
86baa9
+    dn = DN(('cn', ADTRUSTInstance.FALLBACK_GROUP_NAME),
86baa9
+            env.container_group,
86baa9
+            env.basedn)
86baa9
+
86baa9
+    try:
86baa9
+        entry = ldap.get_entry(dn, ['gidnumber'])
86baa9
+        gidNumber = entry.get('gidnumber')
86baa9
+    except errors.NotFound:
86baa9
+        logger.error("%s not found",
86baa9
+                     ADTRUSTInstance.FALLBACK_GROUP_NAME)
86baa9
+        return None
86baa9
+
86baa9
+    if gidNumber is None:
86baa9
+        logger.error("%s does not have a gidnumber",
86baa9
+                     ADTRUSTInstance.FALLBACK_GROUP_NAME)
86baa9
+        return None
86baa9
+
86baa9
+    return gidNumber
86baa9
+
86baa9
+
86baa9
 @register()
86baa9
 class update_tdo_gidnumber(Updater):
86baa9
     """
86baa9
@@ -340,43 +359,55 @@ class update_tdo_gidnumber(Updater):
86baa9
             logger.debug('AD Trusts are not enabled on this server')
86baa9
             return False, []
86baa9
 
86baa9
-        # Read the gidnumber of the fallback group
86baa9
-        dn = DN(('cn', ADTRUSTInstance.FALLBACK_GROUP_NAME),
86baa9
-                self.api.env.container_group,
86baa9
-                self.api.env.basedn)
86baa9
-
86baa9
-        try:
86baa9
-            entry = ldap.get_entry(dn, ['gidnumber'])
86baa9
-            gidNumber = entry.get('gidnumber')
86baa9
-        except errors.NotFound:
86baa9
-            logger.error("%s not found",
86baa9
-                         ADTRUSTInstance.FALLBACK_GROUP_NAME)
86baa9
-            return False, ()
86baa9
-
86baa9
+        gidNumber = get_gidNumber(ldap, self.api.env)
86baa9
         if not gidNumber:
86baa9
             logger.error("%s does not have a gidnumber",
86baa9
                          ADTRUSTInstance.FALLBACK_GROUP_NAME)
86baa9
             return False, ()
86baa9
 
86baa9
-        # For each trusted domain object, add gidNumber
86baa9
+        # For each trusted domain object, add posix attributes
86baa9
+        # to allow use of a trusted domain account by AD DCs
86baa9
+        # to authenticate against our Samba instance
86baa9
         try:
86baa9
             tdos = ldap.get_entries(
86baa9
                 DN(self.api.env.container_adtrusts, self.api.env.basedn),
86baa9
                 scope=ldap.SCOPE_ONELEVEL,
86baa9
-                filter="(objectclass=ipaNTTrustedDomain)",
86baa9
-                attrs_list=['gidnumber'])
86baa9
+                filter="(&(objectclass=ipaNTTrustedDomain)"
86baa9
+                       "(objectclass=ipaIDObject))",
86baa9
+                attrs_list=['gidnumber', 'uidnumber', 'objectclass',
86baa9
+                            'ipantsecurityidentifier',
86baa9
+                            'ipaNTTrustDirection'
86baa9
+                            'uid', 'cn', 'ipantflatname'])
86baa9
             for tdo in tdos:
86baa9
                 # if the trusted domain object does not contain gidnumber,
86baa9
                 # add the default fallback group gidnumber
86baa9
                 if not tdo.get('gidnumber'):
86baa9
-                    try:
86baa9
-                        tdo['gidnumber'] = gidNumber
86baa9
-                        ldap.update_entry(tdo)
86baa9
-                        logger.debug("Added gidnumber %s to %s",
86baa9
-                                     gidNumber, tdo.dn)
86baa9
-                    except Exception:
86baa9
-                        logger.warning(
86baa9
-                            "Failed to add gidnumber to %s", tdo.dn)
86baa9
+                    tdo['gidnumber'] = gidNumber
86baa9
+
86baa9
+                # Generate uidNumber and ipaNTSecurityIdentifier if
86baa9
+                # uidNumber is missing. We rely on sidgen plugin here
86baa9
+                # to generate ipaNTSecurityIdentifier.
86baa9
+                if not tdo.get('uidnumber'):
86baa9
+                    tdo['uidnumber'] = ['-1']
86baa9
+
86baa9
+                if 'posixAccount' not in tdo.get('objectclass'):
86baa9
+                    tdo['objectclass'].extend(['posixAccount'])
86baa9
+                # Based on the flat name of a TDO,
86baa9
+                # add user name FLATNAME$ (note dollar sign)
86baa9
+                # to allow SSSD to map this TDO to a POSIX account
86baa9
+                if not tdo.get('uid'):
86baa9
+                    tdo['uid'] = ["{flatname}$".format(
86baa9
+                                  flatname=tdo.single_value['ipantflatname'])]
86baa9
+                if not tdo.get('homedirectory'):
86baa9
+                    tdo['homedirectory'] = ['/dev/null']
86baa9
+
86baa9
+                # Store resulted entry
86baa9
+                try:
86baa9
+                    ldap.update_entry(tdo)
86baa9
+                except errors.ExecutionError as e:
86baa9
+                    logger.warning(
86baa9
+                        "Failed to update trusted domain object %s", tdo.dn)
86baa9
+                    logger.debug("Exception during TDO update: %s", str(e))
86baa9
 
86baa9
         except errors.NotFound:
86baa9
             logger.debug("No trusted domain object to update")
86baa9
@@ -400,3 +431,259 @@ class update_mapping_Guests_to_nobody(Updater):
86baa9
 
86baa9
         map_Guests_to_nobody()
86baa9
         return False, []
86baa9
+
86baa9
+
86baa9
+@register()
86baa9
+class update_tdo_to_new_layout(Updater):
86baa9
+    """
86baa9
+    Transform trusted domain objects into a new layout
86baa9
+
86baa9
+    There are now two Kerberos principals per direction of trust:
86baa9
+
86baa9
+    INBOUND:
86baa9
+     - krbtgt/<OUR REALM>@<REMOTE REALM>, enabled by default
86baa9
+
86baa9
+     - <OUR FLATNAME$>@<REMOTE REALM>, disabled by default on our side
86baa9
+       as it is only used by SSSD to retrieve TDO creds when operating
86baa9
+       as an AD Trust agent across IPA topology
86baa9
+
86baa9
+    OUTBOUND:
86baa9
+     - krbtgt/<REMOTE REALM>@<OUR REALM>, enabled by default
86baa9
+
86baa9
+     - <REMOTE FLATNAME$>@<OUR REALM>, enabled by default and
86baa9
+       used by remote trusted DCs to authenticate against us
86baa9
+
86baa9
+       This principal also has krbtgt/<REMOTE FLATNAME>@<OUR REALM> defined
86baa9
+       as a Kerberos principal alias. This is due to how Kerberos
86baa9
+       key salt is derived for cross-realm principals on AD side
86baa9
+
86baa9
+    Finally, Samba requires <REMOTE FLATNAME$> account to also possess POSIX
86baa9
+    and SMB identities. We ensure this by making the trusted domain object to
86baa9
+    be this account with 'uid' and 'cn' attributes being '<REMOTE FLATNAME$>'
86baa9
+    and uidNumber/gidNumber generated automatically. Also, we ensure the
86baa9
+    trusted domain object is given a SID.
86baa9
+
86baa9
+    The update to <REMOTE FLATNAME$> POSIX/SMB identities is done through
86baa9
+    the update plugin update_tdo_gidnumber.
86baa9
+    """
86baa9
+    tgt_principal_template = "krbtgt/{remote}@{local}"
86baa9
+    nbt_principal_template = "{nbt}$@{realm}"
86baa9
+    trust_filter = \
86baa9
+        "(&(objectClass=ipaNTTrustedDomain)(objectClass=ipaIDObject))"
86baa9
+    trust_attrs = ("ipaNTFlatName", "ipaNTTrustPartner", "ipaNTTrustDirection",
86baa9
+                   "cn", "ipaNTTrustAttributes", "ipaNTAdditionalSuffixes",
86baa9
+                   "ipaNTTrustedDomainSID", "ipaNTTrustType",
86baa9
+                   "ipaNTTrustAuthIncoming", "ipaNTTrustAuthOutgoing")
86baa9
+    change_password_template = \
86baa9
+        "change_password -pw {password} " \
86baa9
+        "-e aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 " \
86baa9
+        "{principal}"
86baa9
+
86baa9
+    KRB_PRINC_CREATE_DEFAULT = 0x00000000
86baa9
+    KRB_PRINC_CREATE_DISABLED = 0x00000001
86baa9
+    KRB_PRINC_CREATE_AGENT_PERMISSION = 0x00000002
86baa9
+    KRB_PRINC_CREATE_IDENTITY = 0x00000004
86baa9
+    KRB_PRINC_MUST_EXIST = 0x00000008
86baa9
+
86baa9
+    # This is a flag for krbTicketFlags attribute
86baa9
+    # to disallow creating any tickets using this principal
86baa9
+    KRB_DISALLOW_ALL_TIX = 0x00000040
86baa9
+
86baa9
+    def retrieve_trust_password(self, packed):
86baa9
+        # The structure of the trust secret is described at
86baa9
+        # https://github.com/samba-team/samba/blob/master/
86baa9
+        # librpc/idl/drsblobs.idl#L516-L569
86baa9
+        # In our case in LDAP TDO object stores
86baa9
+        # `struct trustAuthInOutBlob` that has `count` and
86baa9
+        # the `current` of `AuthenticationInformationArray` struct
86baa9
+        # which has own `count` and `array` of `AuthenticationInformation`
86baa9
+        # structs that have `AuthType` field which should be equal to
86baa9
+        # `LSA_TRUST_AUTH_TYPE_CLEAR`.
86baa9
+        # Then AuthInfo field would contain a password as an array of bytes
86baa9
+        assert(packed.count != 0)
86baa9
+        assert(packed.current.count != 0)
86baa9
+        assert(packed.current.array[0].AuthType == lsa.TRUST_AUTH_TYPE_CLEAR)
86baa9
+        clear_value = packed.current.array[0].AuthInfo.password
86baa9
+
86baa9
+        return ''.join(map(chr, clear_value))
86baa9
+
86baa9
+    def set_krb_principal(self, principals, password, trustdn, flags=None):
86baa9
+
86baa9
+        ldap = self.api.Backend.ldap2
86baa9
+
86baa9
+        if isinstance(principals, (list, tuple)):
86baa9
+            trust_principal = principals[0]
86baa9
+            aliases = principals[1:]
86baa9
+        else:
86baa9
+            trust_principal = principals
86baa9
+            aliases = []
86baa9
+
86baa9
+        try:
86baa9
+            entry = ldap.get_entry(
86baa9
+                DN(('krbprincipalname', trust_principal), trustdn))
86baa9
+            dn = entry.dn
86baa9
+            action = ldap.update_entry
86baa9
+            logger.debug("Updating Kerberos principal entry for %s",
86baa9
+                         trust_principal)
86baa9
+        except errors.NotFound:
86baa9
+            # For a principal that must exist, we re-raise the exception
86baa9
+            # to let the caller to handle this situation
86baa9
+            if flags & self.KRB_PRINC_MUST_EXIST:
86baa9
+                raise
86baa9
+
86baa9
+            dn = DN(('krbprincipalname', trust_principal), trustdn)
86baa9
+            entry = ldap.make_entry(dn)
86baa9
+            logger.debug("Adding Kerberos principal entry for %s",
86baa9
+                         trust_principal)
86baa9
+            action = ldap.add_entry
86baa9
+
86baa9
+        entry_data = {
86baa9
+            'objectclass':
86baa9
+                ['krbPrincipal', 'krbPrincipalAux',
86baa9
+                 'krbTicketPolicyAux', 'top'],
86baa9
+            'krbcanonicalname': [trust_principal],
86baa9
+            'krbprincipalname': [trust_principal],
86baa9
+        }
86baa9
+
86baa9
+        entry_data['krbprincipalname'].extend(aliases)
86baa9
+
86baa9
+        if flags & self.KRB_PRINC_CREATE_DISABLED:
86baa9
+            flg = int(entry.single_value.get('krbticketflags', 0))
86baa9
+            entry_data['krbticketflags'] = flg | self.KRB_DISALLOW_ALL_TIX
86baa9
+
86baa9
+        if flags & self.KRB_PRINC_CREATE_AGENT_PERMISSION:
86baa9
+            entry_data['objectclass'].extend(['ipaAllowedOperations'])
86baa9
+
86baa9
+        entry.update(entry_data)
86baa9
+        try:
86baa9
+            action(entry)
86baa9
+        except errors.EmptyModlist:
86baa9
+            logger.debug("No update was required for Kerberos principal %s",
86baa9
+                         trust_principal)
86baa9
+
86baa9
+        # If entry existed, no need to set Kerberos keys on it
86baa9
+        if action == ldap.update_entry:
86baa9
+            logger.debug("No need to update Kerberos keys for "
86baa9
+                         "existing Kerberos principal %s",
86baa9
+                         trust_principal)
86baa9
+            return
86baa9
+
86baa9
+        # Now that entry is updated, set its Kerberos keys.
86baa9
+        #
86baa9
+        # It would be a complication to use ipa-getkeytab LDAP extended control
86baa9
+        # here because we would need to encode the request in ASN.1 sequence
86baa9
+        # and we don't have the code to do so exposed in Python bindings.
86baa9
+        # Instead, as we run on IPA master, we can use kadmin.local for that
86baa9
+        # directly.
86baa9
+        # We pass the command as a stdin to both avoid shell interpolation
86baa9
+        # of the passwords and also to avoid its exposure to other processes
86baa9
+        # Since we don't want to record the output, make also a redacted log
86baa9
+        change_password = self.change_password_template.format(
86baa9
+            password=password,
86baa9
+            principal=trust_principal)
86baa9
+
86baa9
+        redacted = self.change_password_template.format(
86baa9
+            password='<REDACTED OUT>',
86baa9
+            principal=trust_principal)
86baa9
+        logger.debug("Updating Kerberos keys for %s with the following "
86baa9
+                     "kadmin command:\n\t%s", trust_principal, redacted)
86baa9
+
86baa9
+        ipautil.run([paths.KADMIN_LOCAL, "-x",
86baa9
+                    "ipa-setup-override-restrictions"],
86baa9
+                    stdin=change_password, skip_output=True)
86baa9
+
86baa9
+    def execute(self, **options):
86baa9
+        # First, see if trusts are enabled on the server
86baa9
+        if not self.api.Command.adtrust_is_enabled()['result']:
86baa9
+            logger.debug('AD Trusts are not enabled on this server')
86baa9
+            return False, []
86baa9
+
86baa9
+        ldap = self.api.Backend.ldap2
86baa9
+        gidNumber = get_gidNumber(ldap, self.api.env)
86baa9
+        if gidNumber is None:
86baa9
+            return False, []
86baa9
+
86baa9
+        result = self.api.Command.trustconfig_show()['result']
86baa9
+        our_nbt_name = result.get('ipantflatname', [None])[0]
86baa9
+        if not our_nbt_name:
86baa9
+            return False, []
86baa9
+
86baa9
+        trusts_dn = self.api.env.container_adtrusts + self.api.env.basedn
86baa9
+
86baa9
+        trusts = ldap.get_entries(
86baa9
+            base_dn=trusts_dn,
86baa9
+            scope=ldap.SCOPE_ONELEVEL,
86baa9
+            filter=self.trust_filter,
86baa9
+            attrs_list=self.trust_attrs)
86baa9
+
86baa9
+        # For every trust, retrieve its principals and convert
86baa9
+        for t_entry in trusts:
86baa9
+            t_dn = t_entry.dn
86baa9
+            logger.debug('Processing trust domain object %s', str(t_dn))
86baa9
+            t_realm = t_entry.single_value.get('ipaNTTrustPartner').upper()
86baa9
+            direction = int(t_entry.single_value.get('ipaNTTrustDirection'))
86baa9
+            passwd_incoming = self.retrieve_trust_password(
86baa9
+                ndr_unpack(drsblobs.trustAuthInOutBlob,
86baa9
+                           t_entry.single_value.get('ipaNTTrustAuthIncoming')))
86baa9
+            passwd_outgoing = self.retrieve_trust_password(
86baa9
+                ndr_unpack(drsblobs.trustAuthInOutBlob,
86baa9
+                           t_entry.single_value.get('ipaNTTrustAuthOutgoing')))
86baa9
+            # For outbound and inbound trusts, process four principals total
86baa9
+            if (direction & TRUST_BIDIRECTIONAL) == TRUST_BIDIRECTIONAL:
86baa9
+                # 1. OUTBOUND: krbtgt/<REMOTE REALM>@<OUR REALM> must exist
86baa9
+                trust_principal = self.tgt_principal_template.format(
86baa9
+                    remote=t_realm, local=self.api.env.realm)
86baa9
+                try:
86baa9
+                    self.set_krb_principal(trust_principal,
86baa9
+                                           passwd_outgoing,
86baa9
+                                           t_dn,
86baa9
+                                           flags=self.KRB_PRINC_CREATE_DEFAULT)
86baa9
+                except errors.NotFound:
86baa9
+                    # It makes no sense to convert this one, skip the trust
86baa9
+                    # completely, better to re-establish one
86baa9
+                    logger.error(
86baa9
+                        "Broken trust to AD: %s not found, "
86baa9
+                        "please re-establish the trust to %s",
86baa9
+                        trust_principal, t_realm)
86baa9
+                    continue
86baa9
+
86baa9
+                # 2. Create <REMOTE FLATNAME$>@<OUR REALM>
86baa9
+                nbt_name = t_entry.single_value.get('ipaNTFlatName')
86baa9
+                nbt_principal = self.nbt_principal_template.format(
86baa9
+                    nbt=nbt_name, realm=self.api.env.realm)
86baa9
+                tgt_principal = self.tgt_principal_template.format(
86baa9
+                    remote=nbt_name, local=self.api.env.realm)
86baa9
+                self.set_krb_principal([nbt_principal, tgt_principal],
86baa9
+                                       passwd_incoming,
86baa9
+                                       t_dn,
86baa9
+                                       flags=self.KRB_PRINC_CREATE_DEFAULT)
86baa9
+
86baa9
+            # 3. INBOUND: krbtgt/<OUR REALM>@<REMOTE REALM> must exist
86baa9
+            trust_principal = self.tgt_principal_template.format(
86baa9
+                remote=self.api.env.realm, local=t_realm)
86baa9
+            try:
86baa9
+                self.set_krb_principal(trust_principal, passwd_outgoing,
86baa9
+                                       t_dn,
86baa9
+                                       flags=self.KRB_PRINC_CREATE_DEFAULT)
86baa9
+            except errors.NotFound:
86baa9
+                # It makes no sense to convert this one, skip the trust
86baa9
+                # completely, better to re-establish one
86baa9
+                logger.error(
86baa9
+                    "Broken trust to AD: %s not found, "
86baa9
+                    "please re-establish the trust to %s",
86baa9
+                    trust_principal, t_realm)
86baa9
+                continue
86baa9
+
86baa9
+            # 4. Create <OUR FLATNAME$>@<REMOTE REALM>, disabled
86baa9
+            nbt_principal = self.nbt_principal_template.format(
86baa9
+                nbt=our_nbt_name, realm=t_realm)
86baa9
+            tgt_principal = self.tgt_principal_template.format(
86baa9
+                remote=our_nbt_name, local=t_realm)
86baa9
+            self.set_krb_principal([nbt_principal, tgt_principal],
86baa9
+                                   passwd_incoming,
86baa9
+                                   t_dn,
86baa9
+                                   flags=self.KRB_PRINC_CREATE_DEFAULT |
86baa9
+                                   self.KRB_PRINC_CREATE_AGENT_PERMISSION |
86baa9
+                                   self.KRB_PRINC_CREATE_DISABLED)
86baa9
+
86baa9
+        return False, []
86baa9
-- 
86baa9
2.20.1
86baa9