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

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