diff --git a/README.debrand b/README.debrand deleted file mode 100644 index 01c46d2..0000000 --- a/README.debrand +++ /dev/null @@ -1,2 +0,0 @@ -Warning: This package was configured for automatic debranding, but the changes -failed to apply. diff --git a/SOURCES/0016-Defer-creating-the-final-krb5.conf-on-clients.patch b/SOURCES/0016-Defer-creating-the-final-krb5.conf-on-clients.patch new file mode 100644 index 0000000..7395fe8 --- /dev/null +++ b/SOURCES/0016-Defer-creating-the-final-krb5.conf-on-clients.patch @@ -0,0 +1,475 @@ +From 3cbf2b25422100cc4105dfb09ee8c7bf87232198 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 22 Aug 2022 10:57:14 -0400 +Subject: [PATCH] Defer creating the final krb5.conf on clients + +A temporary krb5.conf is created early during client enrollment +and was previously used only during the initial ipa-join call. +The final krb5.conf was written soon afterward. + +If there are multiple servers it is possible that the client +may then choose a different KDC to connect. If the client +is faster than replication then the client may not exist +on all servers and therefore enrollment will fail. + +This was seen in performance testing of how many simultaneous +client enrollments are possible. + +Use a decorator to wrap the _install() method to ensure the +temporary files created during installation are cleaned up. + +https://pagure.io/freeipa/issue/9228 + +Signed-off-by: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + ipaclient/install/client.py | 377 +++++++++++++++++++----------------- + 1 file changed, 196 insertions(+), 181 deletions(-) + +diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py +index 920c517d4f2b5414d8d2af63d029949cb4f02dc6..93bc7400805dafffcb3909771f0458e5b7e4a764 100644 +--- a/ipaclient/install/client.py ++++ b/ipaclient/install/client.py +@@ -101,6 +101,37 @@ cli_basedn = None + # end of global variables + + ++def cleanup(func): ++ def inner(options, tdict): ++ # Add some additional options which contain the temporary files ++ # needed during installation. ++ fd, krb_name = tempfile.mkstemp() ++ os.close(fd) ++ ccache_dir = tempfile.mkdtemp(prefix='krbcc') ++ ++ tdict['krb_name'] = krb_name ++ tdict['ccache_dir'] = ccache_dir ++ ++ func(options, tdict) ++ ++ os.environ.pop('KRB5_CONFIG', None) ++ ++ try: ++ os.remove(krb_name) ++ except OSError: ++ logger.error("Could not remove %s", krb_name) ++ try: ++ os.rmdir(ccache_dir) ++ except OSError: ++ pass ++ try: ++ os.remove(krb_name + ".ipabkp") ++ except OSError: ++ logger.error("Could not remove %s.ipabkp", krb_name) ++ ++ return inner ++ ++ + def remove_file(filename): + """ + Deletes a file. If the file does not exist (OSError 2) does nothing. +@@ -2652,7 +2683,7 @@ def restore_time_sync(statestore, fstore): + + def install(options): + try: +- _install(options) ++ _install(options, dict()) + except ScriptError as e: + if e.rval == CLIENT_INSTALL_ERROR: + if options.force: +@@ -2679,7 +2710,8 @@ def install(options): + pass + + +-def _install(options): ++@cleanup ++def _install(options, tdict): + env = {'PATH': SECURE_PATH} + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) +@@ -2687,6 +2719,9 @@ def _install(options): + + statestore.backup_state('installation', 'complete', False) + ++ krb_name = tdict['krb_name'] ++ ccache_dir = tdict['ccache_dir'] ++ + if not options.on_master: + # Try removing old principals from the keytab + purge_host_keytab(cli_realm) +@@ -2719,182 +2754,162 @@ def _install(options): + host_principal = 'host/%s@%s' % (hostname, cli_realm) + if not options.on_master: + nolog = tuple() +- # First test out the kerberos configuration +- fd, krb_name = tempfile.mkstemp() +- os.close(fd) +- ccache_dir = tempfile.mkdtemp(prefix='krbcc') +- try: +- configure_krb5_conf( +- cli_realm=cli_realm, +- cli_domain=cli_domain, +- cli_server=cli_server, +- cli_kdc=cli_kdc, +- dnsok=False, +- filename=krb_name, +- client_domain=client_domain, +- client_hostname=hostname, +- configure_sssd=options.sssd, +- force=options.force) +- env['KRB5_CONFIG'] = krb_name +- ccache_name = os.path.join(ccache_dir, 'ccache') +- join_args = [ +- paths.SBIN_IPA_JOIN, +- "-s", cli_server[0], +- "-b", str(realm_to_suffix(cli_realm)), +- "-h", hostname, +- "-k", paths.KRB5_KEYTAB +- ] +- if options.debug: +- join_args.append("-d") +- env['XMLRPC_TRACE_CURL'] = 'yes' +- if options.force_join: +- join_args.append("-f") +- if options.principal is not None: +- stdin = None +- principal = options.principal +- if principal.find('@') == -1: +- principal = '%s@%s' % (principal, cli_realm) +- if options.password is not None: +- stdin = options.password ++ configure_krb5_conf( ++ cli_realm=cli_realm, ++ cli_domain=cli_domain, ++ cli_server=cli_server, ++ cli_kdc=cli_kdc, ++ dnsok=False, ++ filename=krb_name, ++ client_domain=client_domain, ++ client_hostname=hostname, ++ configure_sssd=options.sssd, ++ force=options.force) ++ env['KRB5_CONFIG'] = krb_name ++ ccache_name = os.path.join(ccache_dir, 'ccache') ++ join_args = [ ++ paths.SBIN_IPA_JOIN, ++ "-s", cli_server[0], ++ "-b", str(realm_to_suffix(cli_realm)), ++ "-h", hostname, ++ "-k", paths.KRB5_KEYTAB ++ ] ++ if options.debug: ++ join_args.append("-d") ++ env['XMLRPC_TRACE_CURL'] = 'yes' ++ if options.force_join: ++ join_args.append("-f") ++ if options.principal is not None: ++ stdin = None ++ principal = options.principal ++ if principal.find('@') == -1: ++ principal = '%s@%s' % (principal, cli_realm) ++ if options.password is not None: ++ stdin = options.password ++ else: ++ if not options.unattended: ++ try: ++ stdin = getpass.getpass( ++ "Password for %s: " % principal) ++ except EOFError: ++ stdin = None ++ if not stdin: ++ raise ScriptError( ++ "Password must be provided for {}.".format( ++ principal), ++ rval=CLIENT_INSTALL_ERROR) + else: +- if not options.unattended: +- try: +- stdin = getpass.getpass( +- "Password for %s: " % principal) +- except EOFError: +- stdin = None +- if not stdin: +- raise ScriptError( +- "Password must be provided for {}.".format( +- principal), +- rval=CLIENT_INSTALL_ERROR) ++ if sys.stdin.isatty(): ++ logger.error( ++ "Password must be provided in " ++ "non-interactive mode.") ++ logger.info( ++ "This can be done via " ++ "echo password | ipa-client-install ... " ++ "or with the -w option.") ++ raise ScriptError(rval=CLIENT_INSTALL_ERROR) + else: +- if sys.stdin.isatty(): +- logger.error( +- "Password must be provided in " +- "non-interactive mode.") +- logger.info( +- "This can be done via " +- "echo password | ipa-client-install ... " +- "or with the -w option.") +- raise ScriptError(rval=CLIENT_INSTALL_ERROR) +- else: +- stdin = sys.stdin.readline() ++ stdin = sys.stdin.readline() + ++ try: ++ kinit_password(principal, stdin, ccache_name, ++ config=krb_name) ++ except RuntimeError as e: ++ print_port_conf_info() ++ raise ScriptError( ++ "Kerberos authentication failed: {}".format(e), ++ rval=CLIENT_INSTALL_ERROR) ++ elif options.keytab: ++ join_args.append("-f") ++ if os.path.exists(options.keytab): + try: +- kinit_password(principal, stdin, ccache_name, +- config=krb_name) +- except RuntimeError as e: ++ kinit_keytab(host_principal, ++ options.keytab, ++ ccache_name, ++ config=krb_name, ++ attempts=options.kinit_attempts) ++ except gssapi.exceptions.GSSError as e: + print_port_conf_info() + raise ScriptError( + "Kerberos authentication failed: {}".format(e), + rval=CLIENT_INSTALL_ERROR) +- elif options.keytab: +- join_args.append("-f") +- if os.path.exists(options.keytab): +- try: +- kinit_keytab(host_principal, +- options.keytab, +- ccache_name, +- config=krb_name, +- attempts=options.kinit_attempts) +- except gssapi.exceptions.GSSError as e: +- print_port_conf_info() +- raise ScriptError( +- "Kerberos authentication failed: {}".format(e), +- rval=CLIENT_INSTALL_ERROR) +- else: +- raise ScriptError( +- "Keytab file could not be found: {}".format( +- options.keytab), +- rval=CLIENT_INSTALL_ERROR) +- elif options.password: +- nolog = (options.password,) +- join_args.append("-w") +- join_args.append(options.password) +- elif options.prompt_password: +- if options.unattended: +- raise ScriptError( +- "Password must be provided in non-interactive mode", +- rval=CLIENT_INSTALL_ERROR) +- try: +- password = getpass.getpass("Password: ") +- except EOFError: +- password = None +- if not password: +- raise ScriptError( +- "Password must be provided.", +- rval=CLIENT_INSTALL_ERROR) +- join_args.append("-w") +- join_args.append(password) +- nolog = (password,) +- +- env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name +- # Get the CA certificate ++ else: ++ raise ScriptError( ++ "Keytab file could not be found: {}".format( ++ options.keytab), ++ rval=CLIENT_INSTALL_ERROR) ++ elif options.password: ++ nolog = (options.password,) ++ join_args.append("-w") ++ join_args.append(options.password) ++ elif options.prompt_password: ++ if options.unattended: ++ raise ScriptError( ++ "Password must be provided in non-interactive mode", ++ rval=CLIENT_INSTALL_ERROR) + try: +- os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG'] +- get_ca_certs(fstore, options, cli_server[0], cli_basedn, +- cli_realm) +- del os.environ['KRB5_CONFIG'] +- except errors.FileError as e: +- logger.error('%s', e) +- raise ScriptError(rval=CLIENT_INSTALL_ERROR) +- except Exception as e: +- logger.error("Cannot obtain CA certificate\n%s", e) +- raise ScriptError(rval=CLIENT_INSTALL_ERROR) +- +- # Now join the domain +- result = run( +- join_args, raiseonerr=False, env=env, nolog=nolog, +- capture_error=True) +- stderr = result.error_output ++ password = getpass.getpass("Password: ") ++ except EOFError: ++ password = None ++ if not password: ++ raise ScriptError( ++ "Password must be provided.", ++ rval=CLIENT_INSTALL_ERROR) ++ join_args.append("-w") ++ join_args.append(password) ++ nolog = (password,) + +- if result.returncode != 0: +- logger.error("Joining realm failed: %s", stderr) +- if not options.force: +- if result.returncode == 13: +- logger.info( +- "Use --force-join option to override the host " +- "entry on the server and force client enrollment.") +- raise ScriptError(rval=CLIENT_INSTALL_ERROR) +- logger.info( +- "Use ipa-getkeytab to obtain a host " +- "principal for this server.") +- else: +- logger.info("Enrolled in IPA realm %s", cli_realm) ++ env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name ++ # Get the CA certificate ++ try: ++ os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG'] ++ get_ca_certs(fstore, options, cli_server[0], cli_basedn, ++ cli_realm) ++ except errors.FileError as e: ++ logger.error('%s', e) ++ raise ScriptError(rval=CLIENT_INSTALL_ERROR) ++ except Exception as e: ++ logger.error("Cannot obtain CA certificate\n%s", e) ++ raise ScriptError(rval=CLIENT_INSTALL_ERROR) + +- if options.principal is not None: +- run([paths.KDESTROY], raiseonerr=False, env=env) ++ # Now join the domain ++ result = run( ++ join_args, raiseonerr=False, env=env, nolog=nolog, ++ capture_error=True) ++ stderr = result.error_output + +- # Obtain the TGT. We do it with the temporary krb5.conf, so that +- # only the KDC we're installing under is contacted. +- # Other KDCs might not have replicated the principal yet. +- # Once we have the TGT, it's usable on any server. +- try: +- kinit_keytab(host_principal, paths.KRB5_KEYTAB, CCACHE_FILE, +- config=krb_name, +- attempts=options.kinit_attempts) +- env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = CCACHE_FILE +- except gssapi.exceptions.GSSError as e: +- print_port_conf_info() +- logger.error("Failed to obtain host TGT: %s", e) +- # failure to get ticket makes it impossible to login and bind +- # from sssd to LDAP, abort installation and rollback changes ++ if result.returncode != 0: ++ logger.error("Joining realm failed: %s", stderr) ++ if not options.force: ++ if result.returncode == 13: ++ logger.info( ++ "Use --force-join option to override the host " ++ "entry on the server and force client enrollment.") + raise ScriptError(rval=CLIENT_INSTALL_ERROR) ++ logger.info( ++ "Use ipa-getkeytab to obtain a host " ++ "principal for this server.") ++ else: ++ logger.info("Enrolled in IPA realm %s", cli_realm) + +- finally: +- try: +- os.remove(krb_name) +- except OSError: +- logger.error("Could not remove %s", krb_name) +- try: +- os.rmdir(ccache_dir) +- except OSError: +- pass +- try: +- os.remove(krb_name + ".ipabkp") +- except OSError: +- logger.error("Could not remove %s.ipabkp", krb_name) ++ if options.principal is not None: ++ run([paths.KDESTROY], raiseonerr=False, env=env) ++ ++ # Obtain the TGT. We do it with the temporary krb5.conf, so that ++ # only the KDC we're installing under is contacted. ++ # Other KDCs might not have replicated the principal yet. ++ # Once we have the TGT, it's usable on any server. ++ try: ++ kinit_keytab(host_principal, paths.KRB5_KEYTAB, CCACHE_FILE, ++ config=krb_name, ++ attempts=options.kinit_attempts) ++ env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = CCACHE_FILE ++ except gssapi.exceptions.GSSError as e: ++ print_port_conf_info() ++ logger.error("Failed to obtain host TGT: %s", e) ++ # failure to get ticket makes it impossible to login and bind ++ # from sssd to LDAP, abort installation and rollback changes ++ raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + # Configure ipa.conf + if not options.on_master: +@@ -2931,23 +2946,6 @@ def _install(options): + except gssapi.exceptions.GSSError as e: + logger.error("Failed to obtain host TGT: %s", e) + raise ScriptError(rval=CLIENT_INSTALL_ERROR) +- else: +- # Configure krb5.conf +- fstore.backup_file(paths.KRB5_CONF) +- configure_krb5_conf( +- cli_realm=cli_realm, +- cli_domain=cli_domain, +- cli_server=cli_server, +- cli_kdc=cli_kdc, +- dnsok=dnsok, +- filename=paths.KRB5_CONF, +- client_domain=client_domain, +- client_hostname=hostname, +- configure_sssd=options.sssd, +- force=options.force) +- +- logger.info( +- "Configured /etc/krb5.conf for IPA realm %s", cli_realm) + + # Clear out any current session keyring information + try: +@@ -3274,6 +3272,23 @@ def _install(options): + configure_nisdomain( + options=options, domain=cli_domain, statestore=statestore) + ++ # Configure the final krb5.conf ++ if not options.on_master: ++ fstore.backup_file(paths.KRB5_CONF) ++ configure_krb5_conf( ++ cli_realm=cli_realm, ++ cli_domain=cli_domain, ++ cli_server=cli_server, ++ cli_kdc=cli_kdc, ++ dnsok=dnsok, ++ filename=paths.KRB5_CONF, ++ client_domain=client_domain, ++ client_hostname=hostname, ++ configure_sssd=options.sssd, ++ force=options.force) ++ ++ logger.info("Configured /etc/krb5.conf for IPA realm %s", cli_realm) ++ + statestore.delete_state('installation', 'complete') + statestore.backup_state('installation', 'complete', True) + logger.info('Client configuration complete.') +-- +2.38.1 + diff --git a/SOURCES/0017-Vault-fix-interoperability-issues-with-older-RHEL-sy.patch b/SOURCES/0017-Vault-fix-interoperability-issues-with-older-RHEL-sy.patch new file mode 100644 index 0000000..8931ec1 --- /dev/null +++ b/SOURCES/0017-Vault-fix-interoperability-issues-with-older-RHEL-sy.patch @@ -0,0 +1,129 @@ +From ba962632cd008edd057f61e7e6fadbf464ff94f2 Mon Sep 17 00:00:00 2001 +From: Francisco Trivino +Date: Tue, 4 Oct 2022 17:26:51 +0200 +Subject: [PATCH] Vault: fix interoperability issues with older RHEL systems + +AES-128-CBC was recently enabled as default wrapping algorithm for transport of secrets. +This change was done in favor of FIPS as crypto-policies disabled 3DES in RHEL9, but +setting AES as default ended-up breaking backwards compatibility with older RHEL systems. + +This commit is tuning some defaults so that interoperability with older RHEL systems +works again. The new logic reflects: + +- when an old client is calling a new server, it doesn't send any value for wrapping_algo + and the old value is used (3DES), so that the client can decrypt using 3DES. + +- when a new client is calling a new server, it sends wrapping_algo = AES128_CBC + +- when a new client is calling an old server, it doesn't send any value and the default is + to use 3DES. + +Finally, as this logic is able to handle overlapping wrapping algorithm between server and +client, the Option "--wrapping-algo" is hidden from "ipa vault-archive --help" and "ipa +vault-retrieve --help" commands. + +Fixes: https://pagure.io/freeipa/issue/9259 +Signed-off-by: Francisco Trivino +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + API.txt | 4 ++-- + VERSION.m4 | 4 ++-- + ipaclient/plugins/vault.py | 7 ++++--- + ipaserver/plugins/vault.py | 4 ++-- + 4 files changed, 10 insertions(+), 9 deletions(-) + +diff --git a/API.txt b/API.txt +index 814124f600111e46c117a0c925e33a27a19b38e0..062a6c756babea6b091c5aaec7d0eaa908b41911 100644 +--- a/API.txt ++++ b/API.txt +@@ -6667,7 +6667,7 @@ option: Flag('shared?', autofill=True, default=False) + option: Str('username?', cli_name='user') + option: Bytes('vault_data') + option: Str('version?') +-option: StrEnum('wrapping_algo?', autofill=True, default=u'aes-128-cbc', values=[u'aes-128-cbc', u'des-ede3-cbc']) ++option: StrEnum('wrapping_algo?', autofill=True, default=u'des-ede3-cbc', values=[u'aes-128-cbc', u'des-ede3-cbc']) + output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') +@@ -6767,7 +6767,7 @@ option: Bytes('session_key') + option: Flag('shared?', autofill=True, default=False) + option: Str('username?', cli_name='user') + option: Str('version?') +-option: StrEnum('wrapping_algo?', autofill=True, default=u'aes-128-cbc', values=[u'aes-128-cbc', u'des-ede3-cbc']) ++option: StrEnum('wrapping_algo?', autofill=True, default=u'des-ede3-cbc', values=[u'aes-128-cbc', u'des-ede3-cbc']) + output: Entry('result') + output: Output('summary', type=[, ]) + output: PrimaryKey('value') +diff --git a/VERSION.m4 b/VERSION.m4 +index 0f02d48979e4af3ad737e377545c4951d5dece02..d628c69a09a43b01aad4ac1bd3a6912bef27a7fe 100644 +--- a/VERSION.m4 ++++ b/VERSION.m4 +@@ -86,8 +86,8 @@ define(IPA_DATA_VERSION, 20100614120000) + # # + ######################################################## + define(IPA_API_VERSION_MAJOR, 2) +-# Last change: add Random Serial Numbers v3 +-define(IPA_API_VERSION_MINOR, 249) ++# Last change: fix vault interoperability issues. ++define(IPA_API_VERSION_MINOR, 251) + + ######################################################## + # Following values are auto-generated from values above +diff --git a/ipaclient/plugins/vault.py b/ipaclient/plugins/vault.py +index 115171c7768d44251c17d0bcdac9c37b3a25db99..d4c84eb6bfb4cc119c599d494171b0a2417ce0ba 100644 +--- a/ipaclient/plugins/vault.py ++++ b/ipaclient/plugins/vault.py +@@ -687,7 +687,7 @@ class ModVaultData(Local): + default_algo = config.get('wrapping_default_algorithm') + if default_algo is None: + # old server +- wrapping_algo = constants.VAULT_WRAPPING_AES128_CBC ++ wrapping_algo = constants.VAULT_WRAPPING_3DES + elif default_algo in constants.VAULT_WRAPPING_SUPPORTED_ALGOS: + # try to use server default + wrapping_algo = default_algo +@@ -801,7 +801,8 @@ class vault_archive(ModVaultData): + if option.name not in ('nonce', + 'session_key', + 'vault_data', +- 'version'): ++ 'version', ++ 'wrapping_algo'): + yield option + for option in super(vault_archive, self).get_options(): + yield option +@@ -1053,7 +1054,7 @@ class vault_retrieve(ModVaultData): + + def get_options(self): + for option in self.api.Command.vault_retrieve_internal.options(): +- if option.name not in ('session_key', 'version'): ++ if option.name not in ('session_key', 'version', 'wrapping_algo'): + yield option + for option in super(vault_retrieve, self).get_options(): + yield option +diff --git a/ipaserver/plugins/vault.py b/ipaserver/plugins/vault.py +index 4d40f66c6a793a831e91c5fe25c8b5277cbd1972..574c83a9aaa64b6a4774400ea7af25343b445c03 100644 +--- a/ipaserver/plugins/vault.py ++++ b/ipaserver/plugins/vault.py +@@ -1051,7 +1051,7 @@ class vault_archive_internal(PKQuery): + 'wrapping_algo?', + doc=_('Key wrapping algorithm'), + values=VAULT_WRAPPING_SUPPORTED_ALGOS, +- default=VAULT_WRAPPING_DEFAULT_ALGO, ++ default=VAULT_WRAPPING_3DES, + autofill=True, + ), + ) +@@ -1130,7 +1130,7 @@ class vault_retrieve_internal(PKQuery): + 'wrapping_algo?', + doc=_('Key wrapping algorithm'), + values=VAULT_WRAPPING_SUPPORTED_ALGOS, +- default=VAULT_WRAPPING_DEFAULT_ALGO, ++ default=VAULT_WRAPPING_3DES, + autofill=True, + ), + ) +-- +2.38.1 + diff --git a/SPECS/freeipa.spec b/SPECS/freeipa.spec index fe340ad..10c81d9 100644 --- a/SPECS/freeipa.spec +++ b/SPECS/freeipa.spec @@ -198,7 +198,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 7%{?rc_version:.%rc_version}%{?dist} +Release: 8%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPLv3+ @@ -233,6 +233,8 @@ Patch0012: 0012-doc-Update-LDAP-grace-period-design-with-default-val.patch Patch0013: 0013-Set-default-gracelimit-on-group-password-policies-to.patch Patch0014: 0014-Set-default-on-group-pwpolicy-with-no-grace-limit-in.patch Patch0015: 0015-fix-canonicalization-issue-in-Web-UI.patch +Patch0016: 0016-Defer-creating-the-final-krb5.conf-on-clients.patch +Patch0017: 0017-Vault-fix-interoperability-issues-with-older-RHEL-sy.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -1741,6 +1743,10 @@ fi %endif %changelog +* Fri Dec 2 2022 Florence Blanc-Renaud - 4.10.0-8 +- Resolves: rhbz#2149274 vault interoperability with older RHEL systems is broken [rhel-9.1.0.z] +- Resolves: rhbz#2150270 ipa-client-install does not maintain server affinity during installation [rhel-9.1.0.z] + * Tue Oct 25 2022 Rafael Jeffman - 4.10.0-7 - Resolves: rhbz#2124547 Attempt to log in as "root" user with admin's password in Web UI does not properly fail - Resolves: rhbz#2137555 Attempt to log in as "root" user with admin's password in Web UI does not properly fail [rhel-9.1.0.z]