Blame SOURCES/0012-Defer-creating-the-final-krb5-conf-on-clients_rhbz#2150246.patch

097621
From 69413325158a3ea06d1491acd77ee6e0955ee89a Mon Sep 17 00:00:00 2001
097621
From: Rob Crittenden <rcritten@redhat.com>
097621
Date: Sep 26 2022 11:48:47 +0000
097621
Subject: Defer creating the final krb5.conf on clients
097621
097621
097621
A temporary krb5.conf is created early during client enrollment
097621
and was previously used only during the initial ipa-join call.
097621
The final krb5.conf was written soon afterward.
097621
097621
If there are multiple servers it is possible that the client
097621
may then choose a different KDC to connect. If the client
097621
is faster than replication then the client may not exist
097621
on all servers and therefore enrollment will fail.
097621
097621
This was seen in performance testing of how many simultaneous
097621
client enrollments are possible.
097621
097621
Use a decorator to wrap the _install() method to ensure the
097621
temporary files created during installation are cleaned up.
097621
097621
https://pagure.io/freeipa/issue/9228
097621
097621
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
097621
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
097621
097621
---
097621
097621
diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
097621
index 920c517..93bc740 100644
097621
--- a/ipaclient/install/client.py
097621
+++ b/ipaclient/install/client.py
097621
@@ -101,6 +101,37 @@ cli_basedn = None
097621
 # end of global variables
097621
 
097621
 
097621
+def cleanup(func):
097621
+    def inner(options, tdict):
097621
+        # Add some additional options which contain the temporary files
097621
+        # needed during installation.
097621
+        fd, krb_name = tempfile.mkstemp()
097621
+        os.close(fd)
097621
+        ccache_dir = tempfile.mkdtemp(prefix='krbcc')
097621
+
097621
+        tdict['krb_name'] = krb_name
097621
+        tdict['ccache_dir'] = ccache_dir
097621
+
097621
+        func(options, tdict)
097621
+
097621
+        os.environ.pop('KRB5_CONFIG', None)
097621
+
097621
+        try:
097621
+            os.remove(krb_name)
097621
+        except OSError:
097621
+            logger.error("Could not remove %s", krb_name)
097621
+        try:
097621
+            os.rmdir(ccache_dir)
097621
+        except OSError:
097621
+            pass
097621
+        try:
097621
+            os.remove(krb_name + ".ipabkp")
097621
+        except OSError:
097621
+            logger.error("Could not remove %s.ipabkp", krb_name)
097621
+
097621
+    return inner
097621
+
097621
+
097621
 def remove_file(filename):
097621
     """
097621
     Deletes a file. If the file does not exist (OSError 2) does nothing.
097621
@@ -2652,7 +2683,7 @@ def restore_time_sync(statestore, fstore):
097621
 
097621
 def install(options):
097621
     try:
097621
-        _install(options)
097621
+        _install(options, dict())
097621
     except ScriptError as e:
097621
         if e.rval == CLIENT_INSTALL_ERROR:
097621
             if options.force:
097621
@@ -2679,7 +2710,8 @@ def install(options):
097621
             pass
097621
 
097621
 
097621
-def _install(options):
097621
+@cleanup
097621
+def _install(options, tdict):
097621
     env = {'PATH': SECURE_PATH}
097621
 
097621
     fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
097621
@@ -2687,6 +2719,9 @@ def _install(options):
097621
 
097621
     statestore.backup_state('installation', 'complete', False)
097621
 
097621
+    krb_name = tdict['krb_name']
097621
+    ccache_dir = tdict['ccache_dir']
097621
+
097621
     if not options.on_master:
097621
         # Try removing old principals from the keytab
097621
         purge_host_keytab(cli_realm)
097621
@@ -2719,182 +2754,162 @@ def _install(options):
097621
     host_principal = 'host/%s@%s' % (hostname, cli_realm)
097621
     if not options.on_master:
097621
         nolog = tuple()
097621
-        # First test out the kerberos configuration
097621
-        fd, krb_name = tempfile.mkstemp()
097621
-        os.close(fd)
097621
-        ccache_dir = tempfile.mkdtemp(prefix='krbcc')
097621
-        try:
097621
-            configure_krb5_conf(
097621
-                cli_realm=cli_realm,
097621
-                cli_domain=cli_domain,
097621
-                cli_server=cli_server,
097621
-                cli_kdc=cli_kdc,
097621
-                dnsok=False,
097621
-                filename=krb_name,
097621
-                client_domain=client_domain,
097621
-                client_hostname=hostname,
097621
-                configure_sssd=options.sssd,
097621
-                force=options.force)
097621
-            env['KRB5_CONFIG'] = krb_name
097621
-            ccache_name = os.path.join(ccache_dir, 'ccache')
097621
-            join_args = [
097621
-                paths.SBIN_IPA_JOIN,
097621
-                "-s", cli_server[0],
097621
-                "-b", str(realm_to_suffix(cli_realm)),
097621
-                "-h", hostname,
097621
-                "-k", paths.KRB5_KEYTAB
097621
-            ]
097621
-            if options.debug:
097621
-                join_args.append("-d")
097621
-                env['XMLRPC_TRACE_CURL'] = 'yes'
097621
-            if options.force_join:
097621
-                join_args.append("-f")
097621
-            if options.principal is not None:
097621
-                stdin = None
097621
-                principal = options.principal
097621
-                if principal.find('@') == -1:
097621
-                    principal = '%s@%s' % (principal, cli_realm)
097621
-                if options.password is not None:
097621
-                    stdin = options.password
097621
+        configure_krb5_conf(
097621
+            cli_realm=cli_realm,
097621
+            cli_domain=cli_domain,
097621
+            cli_server=cli_server,
097621
+            cli_kdc=cli_kdc,
097621
+            dnsok=False,
097621
+            filename=krb_name,
097621
+            client_domain=client_domain,
097621
+            client_hostname=hostname,
097621
+            configure_sssd=options.sssd,
097621
+            force=options.force)
097621
+        env['KRB5_CONFIG'] = krb_name
097621
+        ccache_name = os.path.join(ccache_dir, 'ccache')
097621
+        join_args = [
097621
+            paths.SBIN_IPA_JOIN,
097621
+            "-s", cli_server[0],
097621
+            "-b", str(realm_to_suffix(cli_realm)),
097621
+            "-h", hostname,
097621
+            "-k", paths.KRB5_KEYTAB
097621
+        ]
097621
+        if options.debug:
097621
+            join_args.append("-d")
097621
+            env['XMLRPC_TRACE_CURL'] = 'yes'
097621
+        if options.force_join:
097621
+            join_args.append("-f")
097621
+        if options.principal is not None:
097621
+            stdin = None
097621
+            principal = options.principal
097621
+            if principal.find('@') == -1:
097621
+                principal = '%s@%s' % (principal, cli_realm)
097621
+            if options.password is not None:
097621
+                stdin = options.password
097621
+            else:
097621
+                if not options.unattended:
097621
+                    try:
097621
+                        stdin = getpass.getpass(
097621
+                            "Password for %s: " % principal)
097621
+                    except EOFError:
097621
+                        stdin = None
097621
+                    if not stdin:
097621
+                        raise ScriptError(
097621
+                            "Password must be provided for {}.".format(
097621
+                                principal),
097621
+                            rval=CLIENT_INSTALL_ERROR)
097621
                 else:
097621
-                    if not options.unattended:
097621
-                        try:
097621
-                            stdin = getpass.getpass(
097621
-                                "Password for %s: " % principal)
097621
-                        except EOFError:
097621
-                            stdin = None
097621
-                        if not stdin:
097621
-                            raise ScriptError(
097621
-                                "Password must be provided for {}.".format(
097621
-                                    principal),
097621
-                                rval=CLIENT_INSTALL_ERROR)
097621
+                    if sys.stdin.isatty():
097621
+                        logger.error(
097621
+                            "Password must be provided in "
097621
+                            "non-interactive mode.")
097621
+                        logger.info(
097621
+                            "This can be done via "
097621
+                            "echo password | ipa-client-install ... "
097621
+                            "or with the -w option.")
097621
+                        raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
                     else:
097621
-                        if sys.stdin.isatty():
097621
-                            logger.error(
097621
-                                "Password must be provided in "
097621
-                                "non-interactive mode.")
097621
-                            logger.info(
097621
-                                "This can be done via "
097621
-                                "echo password | ipa-client-install ... "
097621
-                                "or with the -w option.")
097621
-                            raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
-                        else:
097621
-                            stdin = sys.stdin.readline()
097621
+                        stdin = sys.stdin.readline()
097621
 
097621
+            try:
097621
+                kinit_password(principal, stdin, ccache_name,
097621
+                               config=krb_name)
097621
+            except RuntimeError as e:
097621
+                print_port_conf_info()
097621
+                raise ScriptError(
097621
+                    "Kerberos authentication failed: {}".format(e),
097621
+                    rval=CLIENT_INSTALL_ERROR)
097621
+        elif options.keytab:
097621
+            join_args.append("-f")
097621
+            if os.path.exists(options.keytab):
097621
                 try:
097621
-                    kinit_password(principal, stdin, ccache_name,
097621
-                                   config=krb_name)
097621
-                except RuntimeError as e:
097621
+                    kinit_keytab(host_principal,
097621
+                                 options.keytab,
097621
+                                 ccache_name,
097621
+                                 config=krb_name,
097621
+                                 attempts=options.kinit_attempts)
097621
+                except gssapi.exceptions.GSSError as e:
097621
                     print_port_conf_info()
097621
                     raise ScriptError(
097621
                         "Kerberos authentication failed: {}".format(e),
097621
                         rval=CLIENT_INSTALL_ERROR)
097621
-            elif options.keytab:
097621
-                join_args.append("-f")
097621
-                if os.path.exists(options.keytab):
097621
-                    try:
097621
-                        kinit_keytab(host_principal,
097621
-                                     options.keytab,
097621
-                                     ccache_name,
097621
-                                     config=krb_name,
097621
-                                     attempts=options.kinit_attempts)
097621
-                    except gssapi.exceptions.GSSError as e:
097621
-                        print_port_conf_info()
097621
-                        raise ScriptError(
097621
-                            "Kerberos authentication failed: {}".format(e),
097621
-                            rval=CLIENT_INSTALL_ERROR)
097621
-                else:
097621
-                    raise ScriptError(
097621
-                        "Keytab file could not be found: {}".format(
097621
-                            options.keytab),
097621
-                        rval=CLIENT_INSTALL_ERROR)
097621
-            elif options.password:
097621
-                nolog = (options.password,)
097621
-                join_args.append("-w")
097621
-                join_args.append(options.password)
097621
-            elif options.prompt_password:
097621
-                if options.unattended:
097621
-                    raise ScriptError(
097621
-                        "Password must be provided in non-interactive mode",
097621
-                        rval=CLIENT_INSTALL_ERROR)
097621
-                try:
097621
-                    password = getpass.getpass("Password: ")
097621
-                except EOFError:
097621
-                    password = None
097621
-                if not password:
097621
-                    raise ScriptError(
097621
-                        "Password must be provided.",
097621
-                        rval=CLIENT_INSTALL_ERROR)
097621
-                join_args.append("-w")
097621
-                join_args.append(password)
097621
-                nolog = (password,)
097621
-
097621
-            env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name
097621
-            # Get the CA certificate
097621
+            else:
097621
+                raise ScriptError(
097621
+                    "Keytab file could not be found: {}".format(
097621
+                        options.keytab),
097621
+                    rval=CLIENT_INSTALL_ERROR)
097621
+        elif options.password:
097621
+            nolog = (options.password,)
097621
+            join_args.append("-w")
097621
+            join_args.append(options.password)
097621
+        elif options.prompt_password:
097621
+            if options.unattended:
097621
+                raise ScriptError(
097621
+                    "Password must be provided in non-interactive mode",
097621
+                    rval=CLIENT_INSTALL_ERROR)
097621
             try:
097621
-                os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG']
097621
-                get_ca_certs(fstore, options, cli_server[0], cli_basedn,
097621
-                             cli_realm)
097621
-                del os.environ['KRB5_CONFIG']
097621
-            except errors.FileError as e:
097621
-                logger.error('%s', e)
097621
-                raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
-            except Exception as e:
097621
-                logger.error("Cannot obtain CA certificate\n%s", e)
097621
-                raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
-
097621
-            # Now join the domain
097621
-            result = run(
097621
-                join_args, raiseonerr=False, env=env, nolog=nolog,
097621
-                capture_error=True)
097621
-            stderr = result.error_output
097621
+                password = getpass.getpass("Password: ")
097621
+            except EOFError:
097621
+                password = None
097621
+            if not password:
097621
+                raise ScriptError(
097621
+                    "Password must be provided.",
097621
+                    rval=CLIENT_INSTALL_ERROR)
097621
+            join_args.append("-w")
097621
+            join_args.append(password)
097621
+            nolog = (password,)
097621
 
097621
-            if result.returncode != 0:
097621
-                logger.error("Joining realm failed: %s", stderr)
097621
-                if not options.force:
097621
-                    if result.returncode == 13:
097621
-                        logger.info(
097621
-                            "Use --force-join option to override the host "
097621
-                            "entry on the server and force client enrollment.")
097621
-                    raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
-                logger.info(
097621
-                    "Use ipa-getkeytab to obtain a host "
097621
-                    "principal for this server.")
097621
-            else:
097621
-                logger.info("Enrolled in IPA realm %s", cli_realm)
097621
+        env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name
097621
+        # Get the CA certificate
097621
+        try:
097621
+            os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG']
097621
+            get_ca_certs(fstore, options, cli_server[0], cli_basedn,
097621
+                         cli_realm)
097621
+        except errors.FileError as e:
097621
+            logger.error('%s', e)
097621
+            raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
+        except Exception as e:
097621
+            logger.error("Cannot obtain CA certificate\n%s", e)
097621
+            raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
 
097621
-            if options.principal is not None:
097621
-                run([paths.KDESTROY], raiseonerr=False, env=env)
097621
+        # Now join the domain
097621
+        result = run(
097621
+            join_args, raiseonerr=False, env=env, nolog=nolog,
097621
+            capture_error=True)
097621
+        stderr = result.error_output
097621
 
097621
-            # Obtain the TGT. We do it with the temporary krb5.conf, so that
097621
-            # only the KDC we're installing under is contacted.
097621
-            # Other KDCs might not have replicated the principal yet.
097621
-            # Once we have the TGT, it's usable on any server.
097621
-            try:
097621
-                kinit_keytab(host_principal, paths.KRB5_KEYTAB, CCACHE_FILE,
097621
-                             config=krb_name,
097621
-                             attempts=options.kinit_attempts)
097621
-                env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = CCACHE_FILE
097621
-            except gssapi.exceptions.GSSError as e:
097621
-                print_port_conf_info()
097621
-                logger.error("Failed to obtain host TGT: %s", e)
097621
-                # failure to get ticket makes it impossible to login and bind
097621
-                # from sssd to LDAP, abort installation and rollback changes
097621
+        if result.returncode != 0:
097621
+            logger.error("Joining realm failed: %s", stderr)
097621
+            if not options.force:
097621
+                if result.returncode == 13:
097621
+                    logger.info(
097621
+                        "Use --force-join option to override the host "
097621
+                        "entry on the server and force client enrollment.")
097621
                 raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
+            logger.info(
097621
+                "Use ipa-getkeytab to obtain a host "
097621
+                "principal for this server.")
097621
+        else:
097621
+            logger.info("Enrolled in IPA realm %s", cli_realm)
097621
 
097621
-        finally:
097621
-            try:
097621
-                os.remove(krb_name)
097621
-            except OSError:
097621
-                logger.error("Could not remove %s", krb_name)
097621
-            try:
097621
-                os.rmdir(ccache_dir)
097621
-            except OSError:
097621
-                pass
097621
-            try:
097621
-                os.remove(krb_name + ".ipabkp")
097621
-            except OSError:
097621
-                logger.error("Could not remove %s.ipabkp", krb_name)
097621
+        if options.principal is not None:
097621
+            run([paths.KDESTROY], raiseonerr=False, env=env)
097621
+
097621
+        # Obtain the TGT. We do it with the temporary krb5.conf, so that
097621
+        # only the KDC we're installing under is contacted.
097621
+        # Other KDCs might not have replicated the principal yet.
097621
+        # Once we have the TGT, it's usable on any server.
097621
+        try:
097621
+            kinit_keytab(host_principal, paths.KRB5_KEYTAB, CCACHE_FILE,
097621
+                         config=krb_name,
097621
+                         attempts=options.kinit_attempts)
097621
+            env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = CCACHE_FILE
097621
+        except gssapi.exceptions.GSSError as e:
097621
+            print_port_conf_info()
097621
+            logger.error("Failed to obtain host TGT: %s", e)
097621
+            # failure to get ticket makes it impossible to login and bind
097621
+            # from sssd to LDAP, abort installation and rollback changes
097621
+            raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
 
097621
     # Configure ipa.conf
097621
     if not options.on_master:
097621
@@ -2931,23 +2946,6 @@ def _install(options):
097621
             except gssapi.exceptions.GSSError as e:
097621
                 logger.error("Failed to obtain host TGT: %s", e)
097621
                 raise ScriptError(rval=CLIENT_INSTALL_ERROR)
097621
-        else:
097621
-            # Configure krb5.conf
097621
-            fstore.backup_file(paths.KRB5_CONF)
097621
-            configure_krb5_conf(
097621
-                cli_realm=cli_realm,
097621
-                cli_domain=cli_domain,
097621
-                cli_server=cli_server,
097621
-                cli_kdc=cli_kdc,
097621
-                dnsok=dnsok,
097621
-                filename=paths.KRB5_CONF,
097621
-                client_domain=client_domain,
097621
-                client_hostname=hostname,
097621
-                configure_sssd=options.sssd,
097621
-                force=options.force)
097621
-
097621
-            logger.info(
097621
-                "Configured /etc/krb5.conf for IPA realm %s", cli_realm)
097621
 
097621
         # Clear out any current session keyring information
097621
         try:
097621
@@ -3274,6 +3272,23 @@ def _install(options):
097621
         configure_nisdomain(
097621
             options=options, domain=cli_domain, statestore=statestore)
097621
 
097621
+    # Configure the final krb5.conf
097621
+    if not options.on_master:
097621
+        fstore.backup_file(paths.KRB5_CONF)
097621
+        configure_krb5_conf(
097621
+            cli_realm=cli_realm,
097621
+            cli_domain=cli_domain,
097621
+            cli_server=cli_server,
097621
+            cli_kdc=cli_kdc,
097621
+            dnsok=dnsok,
097621
+            filename=paths.KRB5_CONF,
097621
+            client_domain=client_domain,
097621
+            client_hostname=hostname,
097621
+            configure_sssd=options.sssd,
097621
+            force=options.force)
097621
+
097621
+        logger.info("Configured /etc/krb5.conf for IPA realm %s", cli_realm)
097621
+
097621
     statestore.delete_state('installation', 'complete')
097621
     statestore.backup_state('installation', 'complete', True)
097621
     logger.info('Client configuration complete.')
097621