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