diff --git a/SOURCES/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch b/SOURCES/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch new file mode 100644 index 0000000..517e6d1 --- /dev/null +++ b/SOURCES/0003-ipatests-Check-Default-PAC-type-is-added-to-config.patch @@ -0,0 +1,92 @@ +From ad4b7f6cedaed54acf279033b650010c65face10 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Tue, 20 Aug 2024 14:52:03 +0530 +Subject: [PATCH] ipatests: Check Default PAC type is added to config + +This patch checks that the default PAC type +is added to configuration i.e ipaKrbAuthzData: MS-PAC +during ipa-server-installation + +The patch also checks that if 'ipaKrbAuthzData: MS-PAC' +attribute is deleted and then when we run 'ipa-server-upgrade' +command the attribute is added back. + +Related: https://pagure.io/freeipa/issue/9632 + +Signed-off-by: Sudhir Menon +Reviewed-By: Florence Blanc-Renaud +--- + .../test_integration/test_installation.py | 15 +++++++++++ + ipatests/test_integration/test_upgrade.py | 26 ++++++++++++++++++- + 2 files changed, 40 insertions(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_installation.py b/ipatests/test_integration/test_installation.py +index ada43e33fe173ea3c315178c37e2a664b05b905b..c5565c452010f23f038ddf329454b591ef09f6af 100644 +--- a/ipatests/test_integration/test_installation.py ++++ b/ipatests/test_integration/test_installation.py +@@ -1190,6 +1190,21 @@ class TestInstallMaster(IntegrationTest): + expected_stdout=f'href="https://{self.master.hostname}/' + ) + ++ def test_pac_configuration_enabled(self): ++ """ ++ This testcase checks that the default PAC type ++ is added to configuration. ++ """ ++ base_dn = str(self.master.domain.basedn) ++ dn = DN( ++ ("cn", "ipaConfig"), ++ ("cn", "etc"), ++ base_dn ++ ) ++ result = tasks.ldapsearch_dm(self.master, str(dn), ++ ["ipaKrbAuthzData"]) ++ assert 'ipaKrbAuthzData: MS-PAC' in result.stdout_text ++ + def test_hostname_parameter(self, server_cleanup): + """ + Test that --hostname parameter is respected in interactive mode. +diff --git a/ipatests/test_integration/test_upgrade.py b/ipatests/test_integration/test_upgrade.py +index 011de939e92790734d63da2f85be1c25349116a8..a0f393780ccc25774466992976532c876aa876da 100644 +--- a/ipatests/test_integration/test_upgrade.py ++++ b/ipatests/test_integration/test_upgrade.py +@@ -165,7 +165,6 @@ class TestUpgrade(IntegrationTest): + ldap.update_entry(location_krb_rec) + + yield _setup_locations +- + ldap = self.master.ldap_connect() + + modified = False +@@ -491,3 +490,28 @@ class TestUpgrade(IntegrationTest): + tasks.reinstall_packages(self.master, ['*ipa-client']) + assert not self.master.transport.file_exists( + paths.SSH_CONFIG + ".orig") ++ ++ def test_mspac_attribute_set(self): ++ """ ++ This testcase deletes the already existing attribute ++ 'ipaKrbAuthzData: MS-PAC'. ++ The test then runs ipa-server-upgrade and checks that ++ the attribute 'ipaKrbAuthzData: MS-PAC' is added again. ++ """ ++ base_dn = str(self.master.domain.basedn) ++ dn = DN( ++ ("cn", "ipaConfig"), ++ ("cn", "etc"), ++ base_dn ++ ) ++ ldif = textwrap.dedent(""" ++ dn: cn=ipaConfig,cn=etc,{} ++ changetype: modify ++ delete: ipaKrbAuthzData ++ """).format(base_dn) ++ tasks.ldapmodify_dm(self.master, ldif) ++ tasks.kinit_admin(self.master) ++ self.master.run_command(['ipa-server-upgrade']) ++ result = tasks.ldapsearch_dm(self.master, str(dn), ++ ["ipaKrbAuthzData"]) ++ assert 'ipaKrbAuthzData: MS-PAC' in result.stdout_text +-- +2.46.2 + diff --git a/SOURCES/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch b/SOURCES/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch new file mode 100644 index 0000000..049b37c --- /dev/null +++ b/SOURCES/0004-selinux-add-all-IPA-log-files-to-ipa_log_t-file-cont.patch @@ -0,0 +1,86 @@ +From 42eb97ee6bd8011b590aef321d4386ea9352933d Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Wed, 28 Aug 2024 10:02:19 +0300 +Subject: [PATCH] selinux: add all IPA log files to ipa_log_t file context + +We have multiple log files that produced by IPA components. Some of them +are written by the tools that run as root and inherit their file context +from /var/log -> var_log_t. However, increasingly we get tools that were +run through oddjob helpers. These supposed to be run within ipa_helper_t +SELinux context which has write permissions for ipa_log_t file context. + +Add all known log files from the base platform. The following script was +used to generate them: +$ git grep '_LOG = .*ipa.*\.log' ipaplatform/base/paths.py | cut -d= -f2 | \ + xargs -I% echo -e "%\t--\tgen_context(system_u:object_r:ipa_log_t,s0)" + +/var/log/ipabackup.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclient-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclient-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclientsamba-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaclientsamba-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-ca-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-conncheck.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipareplica-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/iparestore.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-enable-sid.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-adtrust-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-dns-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-kra-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaserver-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaupgrade.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipatrust-enable-agent.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipaepn.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipa-custodia.audit.log -- gen_context(system_u:object_r:ipa_log_t,s0) +/var/log/ipa-migrate.log -- gen_context(system_u:object_r:ipa_log_t,s0) + +ipa-custodia.audit.log was already in the present list. + +Additionally, ipa-migrate-conflict.ldif is used by the ipa-migrate tool +but is not provided through the ipaplatform mechanism. It is added +explicitly. + +Fixes: https://pagure.io/freeipa/issue/9654 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + selinux/ipa.fc | 21 ++++++++++++++++++++- + 1 file changed, 20 insertions(+), 1 deletion(-) + +diff --git a/selinux/ipa.fc b/selinux/ipa.fc +index 700e3a14a11fcd403a2e6f57ec781c58dae77660..47bd19ba77418cad1f0904dc4a9a35ce9d6ff9d2 100644 +--- a/selinux/ipa.fc ++++ b/selinux/ipa.fc +@@ -24,7 +24,26 @@ + + /var/log/ipa(/.*)? gen_context(system_u:object_r:ipa_log_t,s0) + +-/var/log/ipareplica-conncheck.log.* -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipabackup.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclient-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclient-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclientsamba-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaclientsamba-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-ca-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-conncheck.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipareplica-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/iparestore.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-enable-sid.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-adtrust-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-dns-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-kra-install.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaserver-uninstall.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaupgrade.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipatrust-enable-agent.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipaepn.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipa-migrate.log -- gen_context(system_u:object_r:ipa_log_t,s0) ++/var/log/ipa-migrate-conflict.ldif -- gen_context(system_u:object_r:ipa_log_t,s0) + + /var/run/ipa(/.*)? gen_context(system_u:object_r:ipa_var_run_t,s0) + +-- +2.46.2 + diff --git a/SOURCES/0004-Add-ipa-idrange-fix.patch b/SOURCES/0005-Add-ipa-idrange-fix.patch similarity index 99% rename from SOURCES/0004-Add-ipa-idrange-fix.patch rename to SOURCES/0005-Add-ipa-idrange-fix.patch index c626995..0d291fd 100644 --- a/SOURCES/0004-Add-ipa-idrange-fix.patch +++ b/SOURCES/0005-Add-ipa-idrange-fix.patch @@ -1497,5 +1497,5 @@ index 0000000000000000000000000000000000000000..de3da9bfd221ce74f1d1bbb0dbe12e4d + assert expected_text in result.stderr_text + assert expected_user in result.stderr_text -- -2.47.0 +2.46.2 diff --git a/SOURCES/0005-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch b/SOURCES/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch similarity index 99% rename from SOURCES/0005-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch rename to SOURCES/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch index 2e4b77a..a16cc1b 100644 --- a/SOURCES/0005-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch +++ b/SOURCES/0006-ipatests-Add-missing-comma-in-test_idrange_no_rid_ba.patch @@ -32,5 +32,5 @@ index de3da9bfd221ce74f1d1bbb0dbe12e4db08b8daa..ff8fbdac9d028d26fc55f5e357f89af8 ]) -- -2.47.0 +2.46.2 diff --git a/SOURCES/0007-ipatests-Update-ipa-adtrust-install-test.patch b/SOURCES/0007-ipatests-Update-ipa-adtrust-install-test.patch new file mode 100644 index 0000000..1b809f2 --- /dev/null +++ b/SOURCES/0007-ipatests-Update-ipa-adtrust-install-test.patch @@ -0,0 +1,32 @@ +From a18eb8358675b3697ccf8f8d8dc230cc62df6a4d Mon Sep 17 00:00:00 2001 +From: Erik Belko +Date: Thu, 29 Aug 2024 16:47:21 +0200 +Subject: [PATCH] ipatests: Update ipa-adtrust-install test + +update test_user_connects_smb_share_if_locked_specific_group with wait +for SSSD to be online after ipa-adtrust-install command + +Related: https://pagure.io/freeipa/issue/9655 + +Signed-off-by: Erik Belko +Reviewed-By: Alexander Bokovoy +--- + ipatests/test_integration/test_adtrust_install.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/ipatests/test_integration/test_adtrust_install.py b/ipatests/test_integration/test_adtrust_install.py +index 72e8d874fb17adadc556ba55b825a88a3ac21a67..de252db1705ad940c3b5ee4df967d7c17a4203a7 100644 +--- a/ipatests/test_integration/test_adtrust_install.py ++++ b/ipatests/test_integration/test_adtrust_install.py +@@ -853,6 +853,8 @@ class TestIpaAdTrustInstall(IntegrationTest): + self.master.config.admin_password, + "-U"] + ) ++ # Wait for SSSD to become online before doing any other check ++ tasks.wait_for_sssd_domain_status_online(self.master) + self.master.run_command(["mkdir", "/freeipa4234"]) + self.master.run_command( + ["chcon", "-t", "samba_share_t", +-- +2.46.2 + diff --git a/SOURCES/0008-Installer-activate-ssh-service-in-sssd.conf.patch b/SOURCES/0008-Installer-activate-ssh-service-in-sssd.conf.patch new file mode 100644 index 0000000..d13e82c --- /dev/null +++ b/SOURCES/0008-Installer-activate-ssh-service-in-sssd.conf.patch @@ -0,0 +1,33 @@ +From 373d41f211c1a04dc432a068bc7d2ba825ff554c Mon Sep 17 00:00:00 2001 +From: Francisco Trivino +Date: Tue, 13 Aug 2024 12:44:21 +0200 +Subject: [PATCH] Installer: activate ssh service in sssd.conf + +This commit enables SSSD's ssh service in ipa-client-install to ensure +sss_ssh_knownhosts and sss_ssh_knownhostsproxy functions properly. + +Fixes: https://pagure.io/freeipa/issue/9649 +Related: https://pagure.io/freeipa/issue/9536 + +Signed-off-by: Francisco Trivino +Reviewed-By: Rob Crittenden +--- + ipaclient/install/client.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py +index 802db9614b24553b2b49259f3aebb366093560ac..47a371f629f6ddfb1cd5e9fff9faad737aa01f54 100644 +--- a/ipaclient/install/client.py ++++ b/ipaclient/install/client.py +@@ -974,6 +974,8 @@ def configure_sssd_conf( + + sssd_enable_service(sssdconfig, 'nss') + sssd_enable_service(sssdconfig, 'pam') ++ if options.conf_ssh: ++ sssd_enable_service(sssdconfig, 'ssh') + + domain.set_option('ipa_domain', cli_domain) + domain.set_option('ipa_hostname', client_hostname) +-- +2.46.2 + diff --git a/SOURCES/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch b/SOURCES/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch new file mode 100644 index 0000000..3ae8ee5 --- /dev/null +++ b/SOURCES/0009-ipa-migrate-fix-migration-issues-with-entries-using-.patch @@ -0,0 +1,413 @@ +From 8d242ba741ec22b258d5e70a530cefd0940783c7 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 23 Jul 2024 17:07:06 -0400 +Subject: [PATCH] ipa-migrate - fix migration issues with entries using + ipaUniqueId in the RDN + +We need to handle these entries differently and specify what attribute +and search base to use to find the entry on the local server. Most +entries can use the "cn" attribute but for selinux usermaps we need to +search using the ipaOwner attribute which is a DN, and in turn requires +additional handling/converting in order to properly check if the usermap +exists or not. + +Also fixed an issue where an attribute should be removed from the local +entry if it does not exist on the remote entry. + +And fixed the handling od "sudoOrder" which is defined as multi-valued +in the schema, but we really need to treat it as single-valued + +Fixes: https://pagure.io/freeipa/issue/9640 + +Signed-off-by: Mark Reynolds +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipaserver/install/ipa_migrate.py | 119 +++++++++++++++++++-- + ipaserver/install/ipa_migrate_constants.py | 84 +++++++++++++-- + 2 files changed, 188 insertions(+), 15 deletions(-) + +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index e21937401b3463335d8297b41a403405071d3795..78c530f24fe5d8c9f5de0f816df9904bf30c7b94 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -32,7 +32,7 @@ from ipaserver.install.ipa_migrate_constants import ( + DS_CONFIG, DB_OBJECTS, DS_INDEXES, BIND_DN, LOG_FILE_NAME, + STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS, + DNA_REGEN_VAL, DNA_REGEN_ATTRS, NIS_PLUGIN, IGNORE_ATTRS, +- DB_EXCLUDE_TREES ++ DB_EXCLUDE_TREES, POLICY_OP_ATTRS + ) + + """ +@@ -529,6 +529,14 @@ class IPAMigrate(): + # + # Helper functions + # ++ def attr_is_operational(self, attr): ++ schema = self.local_conn.schema ++ attr_obj = schema.get_obj(ldap.schema.AttributeType, attr) ++ if attr_obj is not None: ++ if attr_obj.usage == 1: ++ return True ++ return False ++ + def replace_suffix(self, entry_dn): + """ + Replace the base DN in an entry DN +@@ -1122,6 +1130,18 @@ class IPAMigrate(): + stats['reset_range'] += 1 + return entry + ++ def attr_is_required(self, attr, entry): ++ """ ++ Check if an attribute is required in this entry ++ """ ++ entry_oc = entry['objectClass'] ++ for oc in entry_oc: ++ required_attrs = self.local_conn.get_allowed_attributes( ++ [oc], raise_on_unknown=False, attributes="must") ++ if attr.lower() in required_attrs: ++ return True ++ return False ++ + def clean_entry(self, entry_dn, entry_type, entry_attrs): + """ + Clean up the entry from the remote server +@@ -1311,7 +1331,17 @@ class IPAMigrate(): + f"'{old_value}' " + "new value " + f"'{local_entry[attr][0]}'") +- ++ elif 'single' == sp_attr[1]: ++ # The attribute is defined as multivalued, but ++ # we really need to treat it as single valued ++ self.log_debug("Entry is different and will " ++ f"be updated: '{local_dn}' " ++ f"attribute '{attr}' replaced " ++ "with val " ++ f"'{remote_attrs[attr][0]}' " ++ "old value: " ++ f"{local_entry[attr][0]}") ++ local_entry[attr][0] = remote_attrs[attr][0] + goto_next_attr = True + break + +@@ -1358,6 +1388,31 @@ class IPAMigrate(): + local_entry[attr] = remote_attrs[attr] + entry_updated = True + ++ # Remove attributes in the local entry that do not exist in the ++ # remote entry ++ remove_attrs = [] ++ for attr in local_entry: ++ if (self.attr_is_operational(attr) ++ and attr.lower() not in POLICY_OP_ATTRS) or \ ++ attr.lower() in IGNORE_ATTRS or \ ++ attr.lower() in STRIP_ATTRS or \ ++ attr.lower() == "usercertificate": ++ # This is an attribute that we do not want to remove ++ continue ++ ++ if attr not in remote_attrs and \ ++ not self.attr_is_required(attr, local_entry): ++ # Mark this attribute for deletion ++ remove_attrs.append(attr) ++ entry_updated = True ++ ++ # Remove attributes ++ for remove_attr in remove_attrs: ++ self.log_debug("Entry is different and will be updated: " ++ f"'{local_dn}' attribute '{remove_attr}' " ++ "is being removed") ++ del local_entry[remove_attr] ++ + if range_reset: + stats['reset_range'] += 1 + +@@ -1371,6 +1426,9 @@ class IPAMigrate(): + """ + Process chunks of remote entries from a paged results search + ++ entry_dn = the remote entry DN ++ entry_attrs = the remote entry's attributes stored in a dict ++ + Identify entry type + Process entry (removing/change attr/val/schema) + Compare processed remote entry with local entry, merge/overwrite? +@@ -1426,6 +1484,47 @@ class IPAMigrate(): + # Based on the entry type do additional work + # + ++ # For entries with alternate identifying needs we need to rebuild the ++ # local dn. Typically this is for entries that use ipaUniqueId as the ++ # RDN attr ++ if entry_type != "custom" and 'alt_id' in DB_OBJECTS[entry_type]: ++ attr = DB_OBJECTS[entry_type]['alt_id']['attr'] ++ base = DB_OBJECTS[entry_type]['alt_id']['base'] ++ srch_filter = f'{attr}={entry_attrs[attr][0]}' ++ if DB_OBJECTS[entry_type]['alt_id']['isDN'] is True: ++ # Convert the filter to match the local suffix ++ srch_filter = self.replace_suffix(srch_filter) ++ srch_base = base + str(self.local_suffix) ++ ++ try: ++ entries = self.local_conn.get_entries(DN(srch_base), ++ filter=srch_filter) ++ if len(entries) == 1: ++ local_dn = entries[0].dn ++ elif len(entries) == 0: ++ # Not found, no problem just proceed and we will add it ++ pass ++ else: ++ # Found too many entries - should not happen ++ self.log_error('Found too many local matching entries ' ++ f'for "{local_dn}"') ++ if self.args.force: ++ stats['ignored_errors'] += 1 ++ return ++ else: ++ sys.exit(1) ++ except errors.EmptyResult: ++ # Not found, no problem just proceed and we will add it later ++ pass ++ except (errors.NetworkError, errors.DatabaseError) as e: ++ self.log_error('Failed to find a local matching entry for ' ++ f'"{local_dn}" error: {str(e)}') ++ if self.args.force: ++ stats['ignored_errors'] += 1 ++ return ++ else: ++ sys.exit(1) ++ + # See if the entry exists on the local server + try: + local_entry = self.local_conn.get_entry(DN(local_dn), +@@ -1441,14 +1540,20 @@ class IPAMigrate(): + + if self.dryrun: + self.write_update_to_ldif(local_entry) +- DB_OBJECTS[entry_type]['count'] += 1 ++ if entry_type == "custom": ++ stats['custom'] += 1 ++ else: ++ DB_OBJECTS[entry_type]['count'] += 1 + stats['total_db_migrated'] += 1 + return + + # Update the local entry + try: + self.local_conn.update_entry(local_entry) +- DB_OBJECTS[entry_type]['count'] += 1 ++ if entry_type == "custom": ++ stats['custom'] += 1 ++ else: ++ DB_OBJECTS[entry_type]['count'] += 1 + except errors.ExecutionError as e: + self.log_error(f'Failed to update "{local_dn}" error: ' + f'{str(e)}') +@@ -1567,7 +1672,7 @@ class IPAMigrate(): + """ + Used paged search for online method to avoid large memory footprint + """ +- self.log_info("Migrating database ... (this make take a while)") ++ self.log_info("Migrating database ... (this may take a while)") + if self.args.db_ldif is not None: + self.processDBOffline() + else: +@@ -1608,7 +1713,7 @@ class IPAMigrate(): + f"{len(objectclasses)} objectClasses") + + # Loop over attributes and objectclasses and count them +- schema = self.local_conn._get_schema() ++ schema = self.local_conn.schema + local_schema = schema.ldap_entry() + for schema_type in [(attributes, "attributeTypes"), + (objectclasses, "objectClasses")]: +@@ -1967,7 +2072,7 @@ class IPAMigrate(): + + # Run ipa-server-upgrade + self.log_info("Running ipa-server-upgrade ... " +- "(this make take a while)") ++ "(this may take a while)") + if self.dryrun: + self.log_info("Skipping ipa-server-upgrade in dryrun mode.") + else: +diff --git a/ipaserver/install/ipa_migrate_constants.py b/ipaserver/install/ipa_migrate_constants.py +index 0e26c75497b216f09ed450aa25a09c2102582326..250f1b5b01bf066d316a98489ab6153b89615173 100644 +--- a/ipaserver/install/ipa_migrate_constants.py ++++ b/ipaserver/install/ipa_migrate_constants.py +@@ -19,6 +19,28 @@ STRIP_OP_ATTRS = [ + 'nsuniqueid', + 'dsentrydn', + 'entryuuid', ++ 'entrydn', ++ 'entryid', ++ 'entryusn', ++ 'numsubordinates', ++ 'parentid', ++ 'tombstonenumsubordinates' ++] ++ ++# Operational attributes that we would want to remove from the local entry if ++# they don't exist in the remote entry ++POLICY_OP_ATTRS = [ ++ 'nsaccountlock', ++ 'passwordexpiratontime', ++ 'passwordgraceusertime', ++ 'pwdpolicysubentry', ++ 'passwordexpwarned', ++ 'passwordretrycount', ++ 'retrycountresettime', ++ 'accountunlocktime', ++ 'passwordhistory', ++ 'passwordallowchangetime', ++ 'pwdreset' + ] + + # Atributes to strip from users/groups +@@ -110,7 +132,7 @@ STRIP_OC = [ + # + # The DS_CONFIG mapping breaks each config entry (or type of entry) into its + # own catagory. Each catagory, or type, as DN list "dn", the attributes# we +-# are intrested in. These attributes are broken into singel valued "attrs", ++# are intrested in. These attributes are broken into single valued "attrs", + # or multi-valued attributes "multivalued". If the attributes is single + # valued then the value is replaced, if it's multivalued then it is "appended" + # +@@ -503,7 +525,7 @@ DS_CONFIG = { + } + + # +-# Slpai NIS is an optional plugin. It requires special handling ++# Slapi NIS is an optional plugin. It requires special handling + # + NIS_PLUGIN = { + 'dn': 'cn=NIS Server,cn=plugins,cn=config', +@@ -565,6 +587,12 @@ DS_INDEXES = { + # identify the entry. + # The "label" and "count" attributes are used for the Summary Report + # ++# Some entries use ipaUniqueId as the RDN attribute, this makes comparing ++# entries between the remote and local servers problematic. So we need special ++# identifying information to find the local entry. In this case we use the ++# "alt_id" key which is a dict of an attribute 'attr' and partial base DN ++# 'base' - which is expected to end in a comma. ++# + DB_OBJECTS = { + # Plugins + 'automember_def': { +@@ -640,8 +668,8 @@ DB_OBJECTS = { + 'oc': ['ipaconfigobject', 'ipaguiconfig'], + 'subtree': 'cn=ipaconfig,cn=etc,$SUFFIX', + 'special_attrs': [ +- # needs special handling, but +- # ipa-server-upgrade rewrites this attribute anyway! ++ # needs special handling, but ipa-server-upgrade rewrites this ++ # attribute anyway! + ('ipausersearchfields', 'list'), + ], + 'label': 'IPA Config', +@@ -772,11 +800,16 @@ DB_OBJECTS = { + 'mode': 'all', + 'count': 0, + }, +- 'subids': { # unknown what these entries look like TODO ++ 'subids': { + 'oc': [], + 'subtree': ',cn=subids,cn=accounts,$SUFFIX', + 'label': 'Sub IDs', +- 'mode': 'all', # TODO Maybe production only? ++ 'mode': 'production', ++ 'alt_id': { ++ 'attr': 'ipaOwner', ++ 'isDN': True, ++ 'base': 'cn=subids,cn=accounts,', ++ }, + 'count': 0, + }, + +@@ -884,6 +917,11 @@ DB_OBJECTS = { + 'oc': ['ipahbacrule'], + 'subtree': ',cn=hbac,$SUFFIX', + 'label': 'HBAC Rules', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=hbac,', ++ 'isDN': False, ++ }, + 'mode': 'all', + 'count': 0, + }, +@@ -892,6 +930,11 @@ DB_OBJECTS = { + 'selinux_usermap': { # Not sure if this is needed, entry is empty TODO + 'oc': [], + 'subtree': ',cn=usermap,cn=selinux,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=usermap,cn=selinux,', ++ 'isDN': False, ++ }, + 'label': 'Selinux Usermaps', + 'mode': 'all', + 'count': 0, +@@ -902,12 +945,27 @@ DB_OBJECTS = { + 'oc': ['ipasudorule'], + 'subtree': ',cn=sudorules,cn=sudo,$SUFFIX', + 'label': 'Sudo Rules', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=sudorules,cn=sudo,', ++ 'isDN': False, ++ }, ++ 'special_attrs': [ ++ # schema defines sudoOrder as mutlivalued, but we need to treat ++ # it as single valued ++ ('sudoorder', 'single'), ++ ], + 'mode': 'all', + 'count': 0, + }, + 'sudo_cmds': { + 'oc': ['ipasudocmd'], + 'subtree': ',cn=sudocmds,cn=sudo,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'sudoCmd', ++ 'base': 'cn=sudocmds,cn=sudo,', ++ 'isDN': False, ++ }, + 'label': 'Sudo Commands', + 'mode': 'all', + 'count': 0, +@@ -991,6 +1049,11 @@ DB_OBJECTS = { + 'oc': ['ipanisnetgroup'], + 'not_oc': ['mepmanagedentry'], + 'subtree': ',cn=ng,cn=alt,$SUFFIX', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=ng,cn=alt,', ++ 'isDN': False, ++ }, + 'label': 'Network Groups', + 'mode': 'all', + 'count': 0, +@@ -1006,9 +1069,14 @@ DB_OBJECTS = { + 'count': 0, + }, + 'caacls': { +- 'oc': ['top'], ++ 'oc': ['ipacaacl'], + 'subtree': ',cn=caacls,cn=ca,$SUFFIX', +- 'label': 'CA Certificates', ++ 'alt_id': { ++ 'attr': 'cn', ++ 'base': 'cn=caacls,cn=ca,', ++ 'isDN': False, ++ }, ++ 'label': 'CA Certificate ACLs', + 'mode': 'all', + 'count': 0, + }, +-- +2.46.2 + diff --git a/SOURCES/0010-ipa-migrate-fix-alternate-entry-search-filter.patch b/SOURCES/0010-ipa-migrate-fix-alternate-entry-search-filter.patch new file mode 100644 index 0000000..c643d26 --- /dev/null +++ b/SOURCES/0010-ipa-migrate-fix-alternate-entry-search-filter.patch @@ -0,0 +1,68 @@ +From 3b5a980f5b65b03b9fd7ad0cfbb6c87874d3ff24 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 3 Sep 2024 13:42:05 -0400 +Subject: [PATCH] ipa-migrate - fix alternate entry search filter + +Processing a filter like a DN can cause normalization issues that result +in an invalid filter. Make sure the filter is encapsulated with +parenthesis and we call replace_suffix_value() instead of +replace_suffix() + +Fixes: https://pagure.io/freeipa/issue/9658 + +Signed-off-by: Mark Reynolds + +Fix typo in test + +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/ipa_migrate.py | 4 ++-- + ipatests/test_integration/test_ipa_ipa_migration.py | 6 +++--- + 2 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index 78c530f24fe5d8c9f5de0f816df9904bf30c7b94..38356aa23ea435e2a616f48356feaea7b50dd1e4 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -1490,10 +1490,10 @@ class IPAMigrate(): + if entry_type != "custom" and 'alt_id' in DB_OBJECTS[entry_type]: + attr = DB_OBJECTS[entry_type]['alt_id']['attr'] + base = DB_OBJECTS[entry_type]['alt_id']['base'] +- srch_filter = f'{attr}={entry_attrs[attr][0]}' ++ srch_filter = f'({attr}={entry_attrs[attr][0]})' + if DB_OBJECTS[entry_type]['alt_id']['isDN'] is True: + # Convert the filter to match the local suffix +- srch_filter = self.replace_suffix(srch_filter) ++ srch_filter = self.replace_suffix_value(srch_filter) + srch_base = base + str(self.local_suffix) + + try: +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index f697bbfbfc6169309274db689501c99fe148cc70..288165e8a83a96e6f6bd4e52866f98617f497c56 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -610,7 +610,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" + MIGRATION_CONFIG_LOG_MSG = "Migrating configuration ...\n" + IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this make take a while)\n" ++ "Running ipa-server-upgrade ... (this may take a while)\n" + ) + SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" + MIGRATION_COMPLETE_LOG_MSG = "Migration complete!\n" +@@ -641,10 +641,10 @@ class TestIPAMigrateScenario1(IntegrationTest): + tasks.kinit_admin(self.replicas[0]) + MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" + MIGRATION_DATABASE_LOG_MSG = ( +- "Migrating database ... (this make take a while)\n" ++ "Migrating database ... (this may take a while)\n" + ) + IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this make take a while)\n" ++ "Running ipa-server-upgrade ... (this may take a while)\n" + ) + SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" + result = run_migrate( +-- +2.46.2 + diff --git a/SOURCES/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch b/SOURCES/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch new file mode 100644 index 0000000..127b1f7 --- /dev/null +++ b/SOURCES/0011-ipatests-provide-a-ccache-to-rpcclient-deletetrustdo.patch @@ -0,0 +1,67 @@ +From a343c149838a3058794f33c75c58b75bc1748f7f Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 17 Sep 2024 17:00:49 +0200 +Subject: [PATCH] ipatests: provide a ccache to rpcclient deletetrustdom + +With samba update to samba-4.20.4, rpcclient now needs a +ccache otherwise it prompts for a password. + +Fixes: https://pagure.io/freeipa/issue/9667 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + ipatests/pytest_ipa/integration/tasks.py | 23 ++++++++++++++++++++--- + 1 file changed, 20 insertions(+), 3 deletions(-) + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 9d6b5f67a311a28c335801d59e0ff0f0c7faccdd..677fb7534256a65940fb5280fa6412789dcba54f 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -795,15 +795,22 @@ def remove_trust_info_from_ad(master, ad_domain, ad_hostname): + kinit_as_user(master, + 'Administrator@{}'.format(ad_domain.upper()), + master.config.ad_admin_password) ++ # Find cache for the user ++ cache_args = [] ++ cache = get_credential_cache(master) ++ if cache: ++ cache_args = ["--use-krb5-ccache", cache] ++ + # Detect whether rpcclient supports -k or --use-kerberos option + res = master.run_command(['rpcclient', '-h'], raiseonerr=False) + if "--use-kerberos" in res.stderr_text: + rpcclient_krb5_knob = "--use-kerberos=desired" + else: + rpcclient_krb5_knob = "-k" +- master.run_command(['rpcclient', rpcclient_krb5_knob, ad_hostname, +- '-c', 'deletetrustdom {}'.format(master.domain.name)], +- raiseonerr=False) ++ cmd_args = ['rpcclient', rpcclient_krb5_knob, ad_hostname] ++ cmd_args.extend(cache_args) ++ cmd_args.extend(['-c', 'deletetrustdom {}'.format(master.domain.name)]) ++ master.run_command(cmd_args, raiseonerr=False) + + + def configure_auth_to_local_rule(master, ad): +@@ -1086,6 +1093,16 @@ def kinit_admin(host, raiseonerr=True): + raiseonerr=raiseonerr) + + ++def get_credential_cache(host): ++ # Return the credential cache currently in use on host or None ++ result = host.run_command(["klist"]).stdout_text ++ pattern = re.compile(r'Ticket cache: (?P.*)\n') ++ res = pattern.search(result) ++ if res: ++ return res['cache'] ++ return None ++ ++ + def uninstall_master(host, ignore_topology_disconnect=True, + ignore_last_of_role=True, clean=True, verbose=False): + uninstall_cmd = ['ipa-server-install', '--uninstall', '-U'] +-- +2.46.2 + diff --git a/SOURCES/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch b/SOURCES/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch new file mode 100644 index 0000000..79600fd --- /dev/null +++ b/SOURCES/0012-test_adtrust_install-add-use-krb5-ccache-to-smbclien.patch @@ -0,0 +1,60 @@ +From 743c7b46e463bef666dc84e9f513eb7dee7f59f6 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 17 Sep 2024 14:48:58 +0200 +Subject: [PATCH] test_adtrust_install: add --use-krb5-ccache to smbclient + command + +With samba 4.20.4 the smbclient commands needs a ccache otherwise it +prompts for a password. + +Fixes: https://pagure.io/freeipa/issue/9666 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + .../test_integration/test_adtrust_install.py | 28 ++++++++++++------- + 1 file changed, 18 insertions(+), 10 deletions(-) + +diff --git a/ipatests/test_integration/test_adtrust_install.py b/ipatests/test_integration/test_adtrust_install.py +index de252db1705ad940c3b5ee4df967d7c17a4203a7..79a91dfaa61276de74b10777c6d44b5942ed1be0 100644 +--- a/ipatests/test_integration/test_adtrust_install.py ++++ b/ipatests/test_integration/test_adtrust_install.py +@@ -873,17 +873,25 @@ class TestIpaAdTrustInstall(IntegrationTest): + "path", "/freeipa4234"]) + self.master.run_command(["touch", "before"]) + self.master.run_command(["touch", "after"]) +- self.master.run_command( +- ["smbclient", "--use-kerberos=desired", +- "-c=put before", "//{}/share".format( +- self.master.hostname)] +- ) ++ # Find cache for the admin user ++ cache_args = [] ++ cache = tasks.get_credential_cache(self.master) ++ if cache: ++ cache_args = ["--use-krb5-ccache", cache] ++ ++ cmd_args = ["smbclient", "--use-kerberos=desired"] ++ cmd_args.extend(cache_args) ++ cmd_args.extend([ ++ "-c=put before", "//{}/share".format(self.master.hostname) ++ ]) ++ self.master.run_command(cmd_args) + self.master.run_command( + ["net", "conf", "setparm", "share", + "valid users", "@admins"]) +- result = self.master.run_command( +- ["smbclient", "--use-kerberos=desired", +- "-c=put after", "//{}/share".format( +- self.master.hostname)] +- ) ++ cmd_args = ["smbclient", "--use-kerberos=desired"] ++ cmd_args.extend(cache_args) ++ cmd_args.extend([ ++ "-c=put after", "//{}/share".format(self.master.hostname) ++ ]) ++ result = self.master.run_command(cmd_args) + assert msg not in result.stdout_text +-- +2.46.2 + diff --git a/SOURCES/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch b/SOURCES/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch new file mode 100644 index 0000000..19498d0 --- /dev/null +++ b/SOURCES/0013-Don-t-rely-on-removing-the-CA-to-uninstall-the-ACME-.patch @@ -0,0 +1,209 @@ +From a785d0c561b8e22bd9d56739481095e07e0a7eb7 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 30 Sep 2024 13:30:46 -0400 +Subject: [PATCH] Don't rely on removing the CA to uninstall the ACME depoyment + +There has always been a pki-server commnd acme-remove. We were +not aware that it should be called prior to removing a CA. In +11.5.0 this is strongly encouraged by the PKI team. In 11.6.0 +ACME is treated as a full subsystem so will be removed in the +future using pkidestroy -s ACME + +The new class acmeinstance.ACMEInstance is introduced so its +uninstallation can be handled in a similar way as the other +PKI services via DogtagInstance. It is, right now, a pretty +thin wrapper. + +We can discuss moving the ACME installation routines here at +some point. It would be ok as long as we don't have to introduce +another PKI restart as part of it. + +In PKI 11.6.0 pkidestroy has new options to ensure a clean +uninstall: --remove-conf --remove-logs. Pass those options +into pkidestroy calls for 11.6.0+. + +Clean up an additional IPA-generated file that needs to be +cleaned up during uninstall: /root/kracert.p12. 11.6.0 is +more sensitive to leftover files than previous versions. + +Fixes: https://pagure.io/freeipa/issue/9673 +Fixes: https://pagure.io/freeipa/issue/9674 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + ipaserver/install/acmeinstance.py | 31 +++++++++++++ + ipaserver/install/ca.py | 5 ++- + ipaserver/install/cainstance.py | 1 + + ipaserver/install/dogtaginstance.py | 44 +++++++++++++------ + .../test_integration/test_uninstallation.py | 21 +++++++++ + 5 files changed, 87 insertions(+), 15 deletions(-) + create mode 100644 ipaserver/install/acmeinstance.py + +diff --git a/ipaserver/install/acmeinstance.py b/ipaserver/install/acmeinstance.py +new file mode 100644 +index 0000000000000000000000000000000000000000..0027c314545f384d9b6ee24b279479e5360d8bef +--- /dev/null ++++ b/ipaserver/install/acmeinstance.py +@@ -0,0 +1,31 @@ ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++import logging ++ ++from ipaserver.install.dogtaginstance import DogtagInstance ++ ++logger = logging.getLogger(__name__) ++ ++ ++class ACMEInstance(DogtagInstance): ++ """ ++ ACME is deployed automatically with a CA subsystem but it is the ++ responsibility of IPA to uninstall the service. ++ ++ This is mostly a placeholder for the uninstaller. We can ++ eventually move the ACME installation routines into this class ++ if we want but it might result in an extra PKI restart which ++ would be slow. ++ """ ++ def __init__(self, realm=None, host_name=None): ++ super(ACMEInstance, self).__init__( ++ realm=realm, ++ subsystem="ACME", ++ service_desc="ACME server", ++ host_name=host_name ++ ) ++ ++ def uninstall(self): ++ DogtagInstance.uninstall(self) +diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py +index ffcb5268399ce71128fc8de5f54d433d35e99dd2..520e3fc5de1084e7c22c0cf7eaa86e1d3c421373 100644 +--- a/ipaserver/install/ca.py ++++ b/ipaserver/install/ca.py +@@ -22,7 +22,7 @@ from ipaplatform.constants import constants + from ipaserver.install import sysupgrade + from ipapython.install import typing + from ipapython.install.core import group, knob, extend_knob +-from ipaserver.install import cainstance, bindinstance, dsinstance ++from ipaserver.install import acmeinstance, cainstance, bindinstance, dsinstance + from ipapython import ipautil, certdb + from ipapython import ipaldap + from ipapython.admintool import ScriptError +@@ -715,6 +715,9 @@ def install_step_1(standalone, replica_config, options, custodia): + + + def uninstall(): ++ acme = acmeinstance.ACMEInstance(api.env.realm) ++ acme.uninstall() ++ + ca_instance = cainstance.CAInstance(api.env.realm) + ca_instance.stop_tracking_certificates() + ipautil.remove_file(paths.RA_AGENT_PEM) +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index 5dac2c0441752e7bb569cde1fc93bc17c3128cdf..5c2c9f8b981cf5d587865f7680e2b231eae655e2 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -1118,6 +1118,7 @@ class CAInstance(DogtagInstance): + + ipautil.remove_file(paths.DOGTAG_ADMIN_P12) + ipautil.remove_file(paths.CACERT_P12) ++ ipautil.remove_file(paths.ADMIN_CERT_PATH) + + def unconfigure_certmonger_renewal_guard(self): + if not self.is_configured(): +diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py +index e89492312deb8ca20668a62fd7a2a20e2866a3fb..4b0f4d274b0c33140ed6f939f1a3fd8b75930ff9 100644 +--- a/ipaserver/install/dogtaginstance.py ++++ b/ipaserver/install/dogtaginstance.py +@@ -304,21 +304,37 @@ class DogtagInstance(service.Service): + if self.is_installed(): + self.print_msg("Unconfiguring %s" % self.subsystem) + +- args = [paths.PKIDESTROY, +- "-i", "pki-tomcat", "--force", +- "-s", self.subsystem] +- +- # specify --log-file on PKI 11.0.0 or later +- ++ args = [] + pki_version = pki.util.Version(pki.specification_version()) +- if pki_version >= pki.util.Version("11.0.0"): +- timestamp = time.strftime( +- "%Y%m%d%H%M%S", +- time.localtime(time.time())) +- log_file = os.path.join( +- paths.VAR_LOG_PKI_DIR, +- "pki-%s-destroy.%s.log" % (self.subsystem.lower(), timestamp)) +- args.extend(["--log-file", log_file]) ++ if self.subsystem == "ACME": ++ if pki_version < pki.util.Version("11.0.0"): ++ return ++ elif ( ++ pki.util.Version("11.0.0") <= pki_version ++ <= pki.util.Version("11.5.0") ++ ): ++ args = ['pki-server', 'acme-remove'] ++ else: ++ # fall through for PKI >= 11.6.0 ++ pass ++ if not args: ++ args = [paths.PKIDESTROY, ++ "-i", "pki-tomcat", "--force", ++ "-s", self.subsystem] ++ ++ # specify --log-file on PKI 11.0.0 or later ++ ++ if pki_version >= pki.util.Version("11.0.0"): ++ timestamp = time.strftime( ++ "%Y%m%d%H%M%S", ++ time.localtime(time.time())) ++ log_file = os.path.join( ++ paths.VAR_LOG_PKI_DIR, ++ "pki-%s-destroy.%s.log" % ++ (self.subsystem.lower(), timestamp)) ++ args.extend(["--log-file", log_file]) ++ if pki_version >= pki.util.Version("11.6.0"): ++ args.extend(["--remove-conf", "--remove-logs"]) + + try: + ipautil.run(args) +diff --git a/ipatests/test_integration/test_uninstallation.py b/ipatests/test_integration/test_uninstallation.py +index 4f8f17ce3ad8d5376ecba11442f379e5691de7f7..049c50db536ae1070f5f958e76b12a1518da0aba 100644 +--- a/ipatests/test_integration/test_uninstallation.py ++++ b/ipatests/test_integration/test_uninstallation.py +@@ -197,6 +197,7 @@ class TestUninstallCleanup(IntegrationTest): + '/var/lib/sss/pubconf/krb5.include.d/localauth_plugin', + '/var/named/dynamic/managed-keys.bind', + '/var/named/dynamic/managed-keys.bind.jnl', ++ '/var/lib/systemd/coredump/', + ] + + leftovers = [] +@@ -217,3 +218,23 @@ class TestUninstallCleanup(IntegrationTest): + leftovers.append(line) + + assert len(leftovers) == 0 ++ ++ ++class TestUninstallReinstall(IntegrationTest): ++ """Test install, uninstall, re-install. ++ ++ Reinstall with PKI 11.6.0 was failing ++ https://pagure.io/freeipa/issue/9673 ++ """ ++ ++ num_replicas = 0 ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master(cls.master, setup_dns=False) ++ ++ def test_uninstall_server(self): ++ tasks.uninstall_master(self.master) ++ ++ def test_reinstall_server(self): ++ tasks.install_master(self.master, setup_dns=False) +-- +2.46.2 + diff --git a/SOURCES/0006-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch b/SOURCES/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch similarity index 99% rename from SOURCES/0006-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch rename to SOURCES/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch index bf52648..910e631 100644 --- a/SOURCES/0006-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch +++ b/SOURCES/0014-ipatests-Fixes-for-ipa-idrange-fix-testsuite.patch @@ -31,5 +31,5 @@ index ff8fbdac9d028d26fc55f5e357f89af879a61723..0c915bd0931ed11a3aa86c533ee8748a def install(cls, mh): super(TestIpaIdrangeFix, cls).install(mh) -- -2.47.0 +2.46.2 diff --git a/SOURCES/0003-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch b/SOURCES/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch similarity index 99% rename from SOURCES/0003-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch rename to SOURCES/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch index b498de6..ca6e6da 100644 --- a/SOURCES/0003-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch +++ b/SOURCES/0015-Do-not-let-user-with-an-expired-OTP-token-to-log-in-.patch @@ -261,5 +261,5 @@ index 350371bfe1e4c1cc6dcc89f6584f813fcb0d32a0..878b4fb560ba8d7768ead54b06565646 + master.run_command(['ipa', 'pwpolicy-mod', '--minlife', '1']) + master.run_command(['ipa', 'user-del', USER1]) -- -2.47.0 +2.46.2 diff --git a/SOURCES/0016-ipatests-Activate-ssh-in-sssd.conf.patch b/SOURCES/0016-ipatests-Activate-ssh-in-sssd.conf.patch new file mode 100644 index 0000000..9e24025 --- /dev/null +++ b/SOURCES/0016-ipatests-Activate-ssh-in-sssd.conf.patch @@ -0,0 +1,41 @@ +From 761647f842567713032709753b6d63467d9871a6 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Mon, 23 Sep 2024 14:05:43 +0530 +Subject: [PATCH] ipatests: Activate ssh in sssd.conf + +This testcase checks that services: ssh +is included in the sssd.conf file when +ipa-client-install is successful. + +Ref: https://pagure.io/freeipa/issue/9649 + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_installation_client.py | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/ipatests/test_integration/test_installation_client.py b/ipatests/test_integration/test_installation_client.py +index f8567b39eead4dffd522aad504fa72a086969257..884bff2f2f3e49f66da391424e128d8e31b82a8a 100644 +--- a/ipatests/test_integration/test_installation_client.py ++++ b/ipatests/test_integration/test_installation_client.py +@@ -94,6 +94,16 @@ class TestInstallClient(IntegrationTest): + ).encode() not in krb5_cfg + tasks.uninstall_client(self.clients[0]) + ++ def test_check_ssh_service_is_activated(self): ++ """ ++ This test checks all default services are activated ++ in sssd.conf including ssh ++ """ ++ tasks.install_client(self.master, self.clients[0]) ++ sssd_cfg = self.clients[0].get_file_contents(paths.SSSD_CONF) ++ assert 'services = nss, pam, ssh, sudo' in sssd_cfg.decode() ++ tasks.uninstall_client(self.clients[0]) ++ + def test_install_with_automount(self): + """Test that installation with automount is successful""" + tasks.install_client(self.master, self.clients[0], +-- +2.46.2 + diff --git a/SOURCES/0017-ipa-migrate-man-page-fix-typos-and-errors.patch b/SOURCES/0017-ipa-migrate-man-page-fix-typos-and-errors.patch new file mode 100644 index 0000000..a48b5d4 --- /dev/null +++ b/SOURCES/0017-ipa-migrate-man-page-fix-typos-and-errors.patch @@ -0,0 +1,131 @@ +From f978fa05e3ed9d4ad9d20493c05c77fb9b4976a7 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 15 Oct 2024 17:04:55 +0200 +Subject: [PATCH] ipa-migrate man page: fix typos and errors + +ipa-migrate man page mentions non-existing option --hostname. +Fix the SYNOPSIS and various typos. + +Fixes: https://pagure.io/freeipa/issue/9681 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Mark Reynolds +--- + install/tools/man/ipa-migrate.1 | 38 +++++++++++++++------------------ + 1 file changed, 17 insertions(+), 21 deletions(-) + +diff --git a/install/tools/man/ipa-migrate.1 b/install/tools/man/ipa-migrate.1 +index 47ae47ea4afa3a5a6fe25dd9bbd14c27ab5f1fdb..5106f4f0f5b5928909ccd5abcef3bb6d1586f5df 100644 +--- a/install/tools/man/ipa-migrate.1 ++++ b/install/tools/man/ipa-migrate.1 +@@ -5,11 +5,11 @@ + .SH "NAME" + ipa\-migrate \- Migrate an IPA server from one machine to another + .SH "SYNOPSIS" +-ipa\-migrate ++\fBipa\-migrate\fR [OPTIONS] \fBprod\-mode\fR|\fBstage\-mode\fR \fIhostname\fR + .SH "DESCRIPTION" + + Use the \fIipa-migrate\fR command to migrate one +-IPA server to an existing local IPA server installation. ++IPA server \fIhostname\fR to an existing local IPA server installation. + + Migrate IPA schema, configuration, and database to a local IPA server. This + migration can be done online, where the tool will query the remote server. Or, +@@ -19,7 +19,6 @@ and then use an exported LDIF file for the database migration portion (this + might be more useful for very large databases as you don't need to worry about + network interruptions) + +-.SH POSITIONAL ARGUMENTS + .TP + \fBprod\-mode\fR + In this mode everything will be migrated including the current user SIDs and +@@ -28,13 +27,10 @@ DNA ranges + \fBstage\-mode\fR + In this mode, SIDs & DNA ranges are not migrated, and DNA attributes are reset + +-.SH "COMMANDS" ++.SH "OPTIONS" + .TP + \fB\-v\fR, \fB\-\-verbose\fR +-Use verbose output while running the migration tool. +-.TP +-\fB\-e\fR, \fB\-\-hostname=HOSTNAME\fR +-The host name of the remote IPA server that is being migrated from. ++Use verbose output while running the migration tool + .TP + \fB\-D\fR, \fB\-\-bind\-dn=BIND_DN\fR + The Bind DN (Distinguished Name) or an LDAP entry to bind to the remote IPA server with. +@@ -43,10 +39,10 @@ access to read the userPassword attribute. If ommitted the default is "cn=direc + .TP + \fB\-w\fR, \fB\-\-bind\-pw=PASSWORD\fR + The password for the Bind DN that is authenticating against the remote IPA server. If +-a password is not provided then the tool with prompt for the password if needed. ++a password is not provided then the tool with prompt for the password if needed + .TP +-\fB\-Just\fR, \fB\-\-bind\-pw\-file=FILE_PATH\fR +-Path to a file containing the password for the Bind DN. ++\fB\-j\fR, \fB\-\-bind\-pw\-file=FILE_PATH\fR ++Path to a file containing the password for the Bind DN + .TP + \fB\-Z\fR, \fB\-\-cacertfile=FILE_PATH\fR + Path to a file containing a CA Certificate that the remote server trusts +@@ -55,23 +51,23 @@ Path to a file containing a CA Certificate that the remote server trusts + Path to a file containing the migration log. By default the tool will use \fI/var/log/ipa-migrate.log\fR + .TP + \fB\-x\fR, \fB\-\-dryrun\fR +-Go through the migration process but do not write and data to the new IPA server. ++Go through the migration process but do not write any data to the new IPA server + .TP + \fB\-o\fR, \fB\-\-dryrun\-record=FILE_PATH\fR + Go through the migration process but do not write any data to the new IPA server. However, write the +-migration operations to an LDIF file which can be applied later or reused for multiple migrations. ++migration operations to an LDIF file which can be applied later or reused for multiple migrations + .TP + \fB\-r\fR, \fB\-\-reset\-range\fR + Reset the ID range for migrated users/groups. In "stage-mode" this is done automatically + .TP + \fB\-F\fR, \fB\-\-force\fR +-Ignore any errors and continue to proceed with migration effort. ++Ignore any errors and continue to proceed with migration effort + .TP + \fB\-q\fR, \fB\-\-quiet\fR +-Only log errors during the migration process. ++Only log errors during the migration process + .TP + \fB\-B\fR, \fB\-\-migrate\-dns\fR +-Migrate thr DNS records ++Migrate the DNS records + .TP + \fB\-S\fR, \fB\-\-skip\-schema\fR + Do not migrate the database schema +@@ -80,21 +76,21 @@ Do not migrate the database schema + Do not migrate the database configuration (dse.ldif/cn=config) + .TP + \fB\-O\fR, \fB\-\-schema\-overwrite\fR +-Overwrite existing schema definitions. By default duplicate schema is skipped. ++Overwrite existing schema definitions. By default duplicate schema is skipped + .TP + \fB\-s\fR, \fB\-\-subtree=DN\fR + Specifies a custom database subtree that should be included in the migration. + This is only needed if non-default subtrees/branches were added to the database +-outside of IPA. ++outside of IPA + .TP + \fB\-f\fR, \fB\-\-db\-ldif=FILE_PATH\fR +-LDIF file containing the entire backend. If omitted the tool will query the remote IPA server. ++LDIF file containing the entire backend. If omitted the tool will query the remote IPA server + .TP + \fB\-m\fR, \fB\-\-schema\-ldif=FILE_PATH\fR +-LDIF file containing the schema. If omitted the tool will query the remote IPA server. ++LDIF file containing the schema. If omitted the tool will query the remote IPA server + .TP + \fB\-g\fR, \fB\-\-config\-ldif=FILE_PATH\fR +-LDIF file containing the entire "cn=config" DIT. If omitted the tool will query the remote IPA server. ++LDIF file containing the entire "cn=config" DIT. If omitted the tool will query the remote IPA server + .TP + \fB\-n\fR, \fB\-\-no\-prompt\fR + Do not prompt for confirmation before starting migration. Use at your own risk! +-- +2.46.2 + diff --git a/SOURCES/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch b/SOURCES/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch new file mode 100644 index 0000000..7660ce0 --- /dev/null +++ b/SOURCES/0018-ipatests-Test-for-ipa-hbac-rule-duplication.patch @@ -0,0 +1,61 @@ +From 7f4e7e1d6a2ae9d05a2dfcf620f4df07d09d9d2b Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Thu, 3 Oct 2024 18:45:31 +0530 +Subject: [PATCH] ipatests: Test for ipa hbac rule duplication + +This test checks that ipa-migrate is not creating duplicate default hbac rules +for allow_all and allow_systemd-user rules. + +Related: https://pagure.io/freeipa/issue/9640 + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + .../test_ipa_ipa_migration.py | 26 +++++++++++++++++++ + 1 file changed, 26 insertions(+) + +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index 288165e8a83a96e6f6bd4e52866f98617f497c56..70c268951a0d7e40806742b16e62b764b2bae37b 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -9,6 +9,7 @@ from __future__ import absolute_import + from ipatests.test_integration.base import IntegrationTest + from ipatests.pytest_ipa.integration import tasks + from ipaplatform.paths import paths ++from collections import Counter + + import pytest + import textwrap +@@ -920,3 +921,28 @@ class TestIPAMigrateScenario1(IntegrationTest): + ) + assert result.returncode == 1 + assert ERR_MSG in result.stderr_text ++ ++ def test_ipa_hbac_rule_duplication(self): ++ """ ++ This testcase checks that default hbac rules ++ are not duplicated on the local server when ++ ipa-migrate command is run. ++ """ ++ run_migrate( ++ self.replicas[0], ++ "prod-mode", ++ self.master.hostname, ++ "cn=Directory Manager", ++ self.master.config.admin_password, ++ extra_args=['-n'] ++ ) ++ result = self.replicas[0].run_command( ++ ['ipa', 'hbacrule-find'] ++ ) ++ lines = result.stdout_text.splitlines() ++ line = [] ++ for i in lines: ++ line.append(i.strip()) ++ count = Counter(line) ++ assert count.get('Rule name: allow_all') < 2 ++ assert count.get('Rule name: allow_systemd-user') < 2 +-- +2.46.2 + diff --git a/SOURCES/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch b/SOURCES/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch new file mode 100644 index 0000000..0d8eacd --- /dev/null +++ b/SOURCES/0019-ipatests-refactor-password-file-handling-in-TestHSMI.patch @@ -0,0 +1,116 @@ +From 142f52fc981fe9f1d693b79a7b49506af2e98829 Mon Sep 17 00:00:00 2001 +From: Mohammad Rizwan +Date: Mon, 19 Aug 2024 16:08:53 +0530 +Subject: [PATCH] ipatests: refactor password file handling in TestHSMInstall + +When token and associated certs are not being cleaned +up properly, the subsequent installation fails. Hence +Password file related scenarios moved out to new test class +so that it have fresh installation. + +Signed-off-by: Mohammad Rizwan +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + .../nightly_ipa-4-12_latest.yaml | 12 ++++++++ + .../nightly_ipa-4-12_latest_selinux.yaml | 13 ++++++++ + ipatests/test_integration/test_hsm.py | 30 ++++++++++--------- + 3 files changed, 41 insertions(+), 14 deletions(-) + +diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml +index 6d18e708fb0512ce21d8db68d4f1ab26849f40b7..07e2a8399ae4cc953adb415b975101ed20c67fd2 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml +@@ -1950,6 +1950,18 @@ jobs: + timeout: 6300 + topology: *master_3repl_1client + ++ fedora-latest-ipa-4-12/test_hsm_TestHSMInstallPasswordFile: ++ requires: [fedora-latest-ipa-4-12/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-12/build_url}' ++ test_suite: test_integration/test_hsm.py::TestHSMInstallPasswordFile ++ template: *ci-ipa-4-12-latest ++ timeout: 6300 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-12/test_hsm_TestHSMInstallADTrustBase: + requires: [fedora-latest-ipa-4-12/build] + priority: 50 +diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml +index 52686df9713975c9590b8a99edb7c3442531fecc..11046be13fca1e7403d0fd74329a66ded3927a6c 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml +@@ -2105,6 +2105,19 @@ jobs: + timeout: 6300 + topology: *master_3repl_1client + ++ fedora-latest-ipa-4-12/test_hsm_TestHSMInstallPasswordFile: ++ requires: [fedora-latest-ipa-4-12/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-12/build_url}' ++ selinux_enforcing: True ++ test_suite: test_integration/test_hsm.py::TestHSMInstallPasswordFile ++ template: *ci-ipa-4-12-latest ++ timeout: 6300 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-12/test_hsm_TestHSMInstallADTrustBase: + requires: [fedora-latest-ipa-4-12/build] + priority: 50 +diff --git a/ipatests/test_integration/test_hsm.py b/ipatests/test_integration/test_hsm.py +index 374f5c25fd3453cd45a15d2b0f20cee424282595..42895fcd60a7c02d3b6103c2f6751a367da30b2f 100644 +--- a/ipatests/test_integration/test_hsm.py ++++ b/ipatests/test_integration/test_hsm.py +@@ -312,24 +312,26 @@ class TestHSMInstall(BaseHSMTest): + assert returncode == 0 + assert output == "No issues found." + +- def test_hsm_install_server_password_file(self): +- check_version(self.master) +- # cleanup before fresh install with password file +- for client in self.clients: +- tasks.uninstall_client(client) + +- for replica in self.replicas: +- tasks.uninstall_master(replica) ++class TestHSMInstallPasswordFile(BaseHSMTest): + +- tasks.uninstall_master(self.master) ++ num_replicas = 1 + +- delete_hsm_token([self.master] + self.replicas, self.token_name) +- self.token_name, self.token_password = get_hsm_token(self.master) +- self.master.put_file_contents(self.token_password_file, +- self.token_password) +- self.replicas[0].put_file_contents(self.token_password_file, +- self.token_password) ++ @classmethod ++ def install(cls, mh): ++ check_version(cls.master) ++ # Enable pkiuser to read softhsm tokens ++ cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods']) ++ cls.token_name, cls.token_password = get_hsm_token(cls.master) ++ cls.master.put_file_contents( ++ cls.token_password_file, cls.token_password ++ ) ++ cls.replicas[0].put_file_contents( ++ cls.token_password_file, cls.token_password ++ ) + ++ def test_hsm_install_server_password_file(self): ++ check_version(self.master) + tasks.install_master( + self.master, setup_dns=self.master_with_dns, + setup_kra=self.master_with_kra, +-- +2.46.2 + diff --git a/SOURCES/0020-ipatests-2FA-test-cases.patch b/SOURCES/0020-ipatests-2FA-test-cases.patch new file mode 100644 index 0000000..398202f --- /dev/null +++ b/SOURCES/0020-ipatests-2FA-test-cases.patch @@ -0,0 +1,276 @@ +From 6ac11ae003740faf19f3c75bf542ec44f717114f Mon Sep 17 00:00:00 2001 +From: Madhuri Upadhye +Date: Tue, 23 Jul 2024 18:14:36 +0530 +Subject: [PATCH] ipatests: 2FA test cases + +Added following: + +Added 'ssh_2fa_with_cmd' method for authentication, +as for '\n' with paramiko did not work. In a test case +need to just press `Enter` for `second factor`. +Advantage of above function is no having paramiko +dependancy. +We can run the any command in same session after +authentication of user. + +Test cases: +1. Authenticate the user only with password, +just press enter at `Second factor` and check tgt after auth. +when User authentication types: otp, password +2. Authenticate the user with password and otpvalues and +check tgt of user after auth when +User authentication types: otp, password + +related: https://github.com/SSSD/sssd/pull/7500 + +Signed-off-by: Madhuri Upadhye +Reviewed-By: Florence Blanc-Renaud +--- + ipatests/test_integration/test_otp.py | 192 ++++++++++++++++++++++++-- + 1 file changed, 181 insertions(+), 11 deletions(-) + +diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py +index 878b4fb560ba8d7768ead54b065656462545babd..0babb45897c6107bf354477dbb0d3a805a3116f5 100644 +--- a/ipatests/test_integration/test_otp.py ++++ b/ipatests/test_integration/test_otp.py +@@ -5,26 +5,27 @@ + """ + import base64 + import logging +-import pytest + import re +-import time ++import tempfile + import textwrap +-from urllib.parse import urlparse, parse_qs +-from paramiko import AuthenticationException ++import time ++from urllib.parse import parse_qs, urlparse + ++import pytest + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.twofactor.hotp import HOTP + from cryptography.hazmat.primitives.twofactor.totp import TOTP +- +-from ipatests.test_integration.base import IntegrationTest +-from ipaplatform.paths import paths +-from ipatests.pytest_ipa.integration import tasks +-from ipapython.dn import DN +- + from ldap.controls.simple import BooleanControl ++from paramiko import AuthenticationException + + from ipalib import errors ++from ipaplatform.osinfo import osinfo ++from ipaplatform.paths import paths ++from ipapython.dn import DN ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++from ipatests.util import xfail_context + + PASSWORD = "DummyPassword123" + USER = "opttestuser" +@@ -84,6 +85,65 @@ def kinit_otp(host, user, *, password, otp, success=True): + ) + + ++def ssh_2fa_with_cmd(host, hostname, username, password, otpvalue, ++ command="exit 0"): ++ """ ssh to user and in same session pass the command to check tgt of user ++ :param host: host to ssh ++ :param hostname: hostname to ssh ++ :param str username: The name of user ++ :param str password: password, usually the first factor ++ :param str otpvalue: generated pin of user ++ :param str command: command to execute in same session, ++ by deafult set to "exit 0" ++ :return: object class of expect command run ++ """ ++ temp_conf = tempfile.NamedTemporaryFile(suffix='.exp', delete=False) ++ with open(temp_conf.name, 'w') as tfile: ++ tfile.write('proc exitmsg { msg code } {\n') ++ tfile.write('\t# Close spawned program, if we are in the prompt\n') ++ tfile.write('\tcatch close\n\n') ++ tfile.write('\t# Wait for the exit code\n') ++ tfile.write('\tlassign [wait] pid spawnid os_error_flag rc\n\n') ++ tfile.write('\tputs ""\n') ++ tfile.write('\tputs "expect result: $msg"\n') ++ tfile.write('\tputs "expect exit code: $code"\n') ++ tfile.write('\tputs "expect spawn exit code: $rc"\n') ++ tfile.write('\texit $code\n') ++ tfile.write('}\n') ++ tfile.write('set timeout 60\n') ++ tfile.write('set prompt ".*\\[#\\$>\\] $"\n') ++ tfile.write(f'set password "{password}"\n') ++ tfile.write(f'set otpvalue "{otpvalue}"\n') ++ tfile.write(f'spawn ssh -o NumberOfPasswordPrompts=1 -o ' ++ f'StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' ++ f' -l {username} {hostname} {command}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Enter first factor:*" {send -- "$password\r"}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Enter second factor:*" {send -- "$otpvalue\r"}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('eof {exitmsg "Unexpected end of file" 202}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('"Authentication failure" ' ++ '{exitmsg "Authentication failure" 1}\n') ++ tfile.write('eof {exitmsg "Authentication successful" 0}\n') ++ tfile.write('timeout {exitmsg "Unexpected output" 201}\n') ++ tfile.write('}\n') ++ tfile.write('expect {\n') ++ tfile.write('exitmsg "Unexpected code path" 203\n') ++ tfile.write('EOF\n') ++ tfile.write('}') ++ host.transport.put_file(temp_conf.name, '/tmp/ssh.exp') ++ tasks.clear_sssd_cache(host) ++ expect_cmd = 'expect -f /tmp/ssh.exp' ++ cmd = host.run_command(expect_cmd, raiseonerr=False) ++ return cmd ++ ++ + def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""): + """ + :param hostname: hostname +@@ -91,6 +151,7 @@ def ssh_2f(hostname, username, answers_dict, port=22, unwanted_prompt=""): + :param answers_dict: dictionary of options with prompt_message and value. + :param port: port for ssh + """ ++ + # Handler for server questions + def answer_handler(title, instructions, prompt_list): + resp = [] +@@ -131,8 +192,9 @@ class TestOTPToken(IntegrationTest): + + @classmethod + def install(cls, mh): +- super(TestOTPToken, cls).install(mh) + master = cls.master ++ tasks.install_packages(master, ['expect']) ++ super(TestOTPToken, cls).install(mh) + + tasks.kinit_admin(master) + # create service with OTP auth indicator +@@ -398,6 +460,114 @@ class TestOTPToken(IntegrationTest): + self.master.run_command(['semanage', 'login', '-D']) + sssd_conf_backup.restore() + ++ def test_2fa_only_with_password(self): ++ """Test ssh with 2FA only with the password(first factor) when ++ user-auth-type is opt and password. ++ ++ Test for : https://github.com/SSSD/sssd/pull/7500 ++ ++ Add the IPA user and user-auth-type set to opt and password. ++ Authenticate the user only with password, just press enter ++ at `Second factor` ++ """ ++ master = self.master ++ USER3 = 'sshuser3' ++ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF) ++ first_prompt = 'Enter first factor:' ++ second_prompt = 'Enter second factor:' ++ add_contents = textwrap.dedent(''' ++ [prompting/2fa/sshd] ++ single_prompt = False ++ first_prompt = {0} ++ second_prompt = {1} ++ ''').format(first_prompt, second_prompt) ++ set_sssd_conf(master, add_contents) ++ tasks.create_active_user(master, USER3, PASSWORD) ++ tasks.kinit_admin(master) ++ master.run_command(['ipa', 'user-mod', USER3, '--user-auth-type=otp', ++ '--user-auth-type=password']) ++ try: ++ otpuid, totp = add_otptoken(master, USER3, otptype='totp') ++ master.run_command(['ipa', 'otptoken-show', otpuid]) ++ totp.generate(int(time.time())).decode('ascii') ++ otpvalue = "\n" ++ tasks.clear_sssd_cache(self.master) ++ github_ticket = "https://github.com/SSSD/sssd/pull/7500" ++ sssd_version = tasks.get_sssd_version(master) ++ rhel_fail = ( ++ osinfo.id == 'rhel' ++ and sssd_version < tasks.parse_version("2.9.5") ++ ) ++ fedora_fail = ( ++ osinfo.id == 'fedora' ++ and sssd_version == tasks.parse_version("2.9.5") ++ ) ++ with xfail_context(rhel_fail or fedora_fail, reason=github_ticket): ++ result = ssh_2fa_with_cmd(master, ++ self.master.external_hostname, ++ USER3, PASSWORD, otpvalue=otpvalue, ++ command="klist") ++ print(result.stdout_text) ++ assert ('Authentication successful') in result.stdout_text ++ assert USER3 in result.stdout_text ++ assert (f'Default principal: ' ++ f'{USER3}@{self.master.domain.realm}' in ++ result.stdout_text) ++ cmd = self.master.run_command(['semanage', 'login', '-l']) ++ assert USER3 in cmd.stdout_text ++ finally: ++ master.run_command(['ipa', 'user-del', USER3]) ++ self.master.run_command(['semanage', 'login', '-D']) ++ sssd_conf_backup.restore() ++ ++ def test_2fa_with_otp_password(self): ++ """Test ssh with 2FA only with password and otpvalue when ++ user-auth-type is opt and password. ++ ++ Test for : https://github.com/SSSD/sssd/pull/7500 ++ ++ Add the IPA user and user-auth-type set to opt and password. ++ Authenticate the user only with password and otpvalue. ++ """ ++ master = self.master ++ USER4 = 'sshuser4' ++ sssd_conf_backup = tasks.FileBackup(master, paths.SSSD_CONF) ++ first_prompt = 'Enter first factor:' ++ second_prompt = 'Enter second factor:' ++ add_contents = textwrap.dedent(''' ++ [prompting/2fa/sshd] ++ single_prompt = False ++ first_prompt = {0} ++ second_prompt = {1} ++ ''').format(first_prompt, second_prompt) ++ set_sssd_conf(master, add_contents) ++ tasks.create_active_user(master, USER4, PASSWORD) ++ tasks.kinit_admin(master) ++ ++ master.run_command(['ipa', 'user-mod', USER4, '--user-auth-type=otp', ++ '--user-auth-type=password']) ++ try: ++ otpuid, totp = add_otptoken(master, USER4, otptype='totp') ++ master.run_command(['ipa', 'otptoken-show', otpuid]) ++ otpvalue = totp.generate(int(time.time())).decode('ascii') ++ tasks.clear_sssd_cache(self.master) ++ result = ssh_2fa_with_cmd(master, ++ self.master.external_hostname, ++ USER4, PASSWORD, otpvalue=otpvalue, ++ command="klist") ++ print(result.stdout_text) ++ cmd = self.master.run_command(['semanage', 'login', '-l']) ++ # check the output ++ assert ('Authentication successful') in result.stdout_text ++ assert USER4 in result.stdout_text ++ assert (f'Default principal: {USER4}@' ++ f'{self.master.domain.realm}' in result.stdout_text) ++ assert USER4 in cmd.stdout_text ++ finally: ++ master.run_command(['ipa', 'user-del', USER4]) ++ self.master.run_command(['semanage', 'login', '-D']) ++ sssd_conf_backup.restore() ++ + @pytest.fixture + def setup_otp_nsslapd(self): + check_services = self.master.run_command( +-- +2.46.2 + diff --git a/SOURCES/0021-Small-fixup-to-determine-which-ACME-uninstaller-to-u.patch b/SOURCES/0021-Small-fixup-to-determine-which-ACME-uninstaller-to-u.patch new file mode 100644 index 0000000..e96338f --- /dev/null +++ b/SOURCES/0021-Small-fixup-to-determine-which-ACME-uninstaller-to-u.patch @@ -0,0 +1,34 @@ +From 9a2de23eb5e00efa72189c4a86d9db1fab52c2ca Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Thu, 24 Oct 2024 11:49:17 -0400 +Subject: [PATCH] Small fixup to determine which ACME uninstaller to use + +The conditional was <= 11.5.0 which it should have been +< 11.6.0 to allow for small updates to the 11.5.0 branch. + +Fixes: https://pagure.io/freeipa/issue/9673 +Fixes: https://pagure.io/freeipa/issue/9674 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/dogtaginstance.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py +index 4b0f4d274b0c33140ed6f939f1a3fd8b75930ff9..58421a1d8859e5dd1357e07fd605e84e49048951 100644 +--- a/ipaserver/install/dogtaginstance.py ++++ b/ipaserver/install/dogtaginstance.py +@@ -311,7 +311,7 @@ class DogtagInstance(service.Service): + return + elif ( + pki.util.Version("11.0.0") <= pki_version +- <= pki.util.Version("11.5.0") ++ < pki.util.Version("11.6.0") + ): + args = ['pki-server', 'acme-remove'] + else: +-- +2.47.0 + diff --git a/SOURCES/0022-UnsafeIPAddress-pass-flag-0-to-IPNetwork.patch b/SOURCES/0022-UnsafeIPAddress-pass-flag-0-to-IPNetwork.patch new file mode 100644 index 0000000..d1f15f2 --- /dev/null +++ b/SOURCES/0022-UnsafeIPAddress-pass-flag-0-to-IPNetwork.patch @@ -0,0 +1,34 @@ +From a9e653ca36a0829ae59cd204e7388d7a6c91e082 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Fri, 13 Sep 2024 09:58:36 +0200 +Subject: [PATCH] UnsafeIPAddress: pass flag=0 to IPNetwork + +When parsing a string, the constructor tries to parse the value +as an IP Address first, or falls back to an IPNetwork with the +flags INET_PTON. + +Use the flag 0 instead for an IPNetwork. + +Fixes: https://pagure.io/freeipa/issue/9645 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + ipapython/ipautil.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py +index 3e98bfd6a66f24933e7e4de8efb79f4f5bf8bd0e..c237d59fb4b8be4187fb0efb04b097ff4df6c182 100644 +--- a/ipapython/ipautil.py ++++ b/ipapython/ipautil.py +@@ -119,7 +119,7 @@ class UnsafeIPAddress(netaddr.IPAddress): + if addr.version != 6: + raise + except ValueError: +- self._net = netaddr.IPNetwork(addr, flags=self.netaddr_ip_flags) ++ self._net = netaddr.IPNetwork(addr, flags=0) + addr = self._net.ip + super(UnsafeIPAddress, self).__init__(addr, + flags=self.netaddr_ip_flags) +-- +2.47.0 + diff --git a/SOURCES/0023-ipa-migrate-dryrun-write-updates-crashes-when-removi.patch b/SOURCES/0023-ipa-migrate-dryrun-write-updates-crashes-when-removi.patch new file mode 100644 index 0000000..2ed299e --- /dev/null +++ b/SOURCES/0023-ipa-migrate-dryrun-write-updates-crashes-when-removi.patch @@ -0,0 +1,35 @@ +From 3d0962014adda39b754c4274ccb5ca5d70963c33 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 21 Oct 2024 13:51:13 -0400 +Subject: [PATCH] ipa-migrate - dryrun write updates crashes when removing + values + +When removing values the mod value list is None and that leads to a +crash when trying to iterate it. Instead check that the vals are not +None before looping. + +Fixes: https://pagure.io/freeipa/issue/9682 + +Signed-off-by: MArk Reynolds +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/install/ipa_migrate.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index 38356aa23ea435e2a616f48356feaea7b50dd1e4..f35629378490d3d45ca97f2aa5b4390c67d660ed 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -622,7 +622,7 @@ class IPAMigrate(): + else: + action = "replace" + ldif_entry += f"{action}: {attr}\n" +- for val in vals: ++ for val in list(vals or []): + ldif_entry += get_ldif_attr_val(attr, val) + ldif_entry += "-\n" + ldif_entry += "\n" +-- +2.47.0 + diff --git a/SOURCES/0024-ipa-migrate-should-migrate-dns-forward-zones.patch b/SOURCES/0024-ipa-migrate-should-migrate-dns-forward-zones.patch new file mode 100644 index 0000000..ac75a1c --- /dev/null +++ b/SOURCES/0024-ipa-migrate-should-migrate-dns-forward-zones.patch @@ -0,0 +1,30 @@ +From 6bdb8603054fc60e9479f6aaf8b6315dfe508891 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 22 Oct 2024 13:00:03 -0400 +Subject: [PATCH] ipa-migrate should migrate dns forward zones + +Fixes: https://pagure.io/freeipa/issue/9686 + +Signed-off-by: Mark Reynolds +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/install/ipa_migrate_constants.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipaserver/install/ipa_migrate_constants.py b/ipaserver/install/ipa_migrate_constants.py +index 250f1b5b01bf066d316a98489ab6153b89615173..c140414ea6c607a93e35ef0705480d1002b7945e 100644 +--- a/ipaserver/install/ipa_migrate_constants.py ++++ b/ipaserver/install/ipa_migrate_constants.py +@@ -993,7 +993,7 @@ DB_OBJECTS = { + 'count': 0, + }, + 'dns_records': { +- 'oc': ['idnsrecord', 'idnszone'], ++ 'oc': ['idnsrecord', 'idnszone', 'idnsforwardzone'], + 'subtree': ',cn=dns,$SUFFIX', + 'label': 'DNS Records', + 'mode': 'all', +-- +2.47.0 + diff --git a/SOURCES/0025-ipatests-Tests-for-ipa-migrate-tool.patch b/SOURCES/0025-ipatests-Tests-for-ipa-migrate-tool.patch new file mode 100644 index 0000000..3b3c85f --- /dev/null +++ b/SOURCES/0025-ipatests-Tests-for-ipa-migrate-tool.patch @@ -0,0 +1,784 @@ +From 9da927c8eec7db6d1c75c296eef45beb93797e58 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Thu, 1 Aug 2024 16:30:16 +0530 +Subject: [PATCH] ipatests: Tests for ipa-migrate tool + +This patch includes test to covers below scenarios + +1. hbac and sudo rules are migrated to local server +2. uid for user migrated varies in stage/prod mode. +3. subids are migrated to local server +4. idranges are migrated to local server +5. vaults are not migrated to local server. +6. Ensure trust related data is also migrated to local server +7. Added paths.IPA_MIGRATE_LOG in ipatests/pytest_ipa/integration/__init__.py + +Signed-off-by: Sudhir Menon +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Florence Blanc-Renaud +--- + ipatests/pytest_ipa/integration/__init__.py | 2 + + .../test_ipa_ipa_migration.py | 596 ++++++++++++++---- + 2 files changed, 460 insertions(+), 138 deletions(-) + +diff --git a/ipatests/pytest_ipa/integration/__init__.py b/ipatests/pytest_ipa/integration/__init__.py +index 34b6ef0fb1e49fbb9c86e7496de50cf5cda5e91e..eb032cd72d2aa2a5ed4c476e3cb04dc77f607eaa 100644 +--- a/ipatests/pytest_ipa/integration/__init__.py ++++ b/ipatests/pytest_ipa/integration/__init__.py +@@ -88,6 +88,8 @@ CLASS_LOGFILES = [ + paths.VAR_LOG_AUDIT, + # sssd + paths.VAR_LOG_SSSD_DIR, ++ # ipa-ipa-migration logs ++ paths.IPA_MIGRATE_LOG, + # system + paths.RESOLV_CONF, + paths.HOSTS, +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index 70c268951a0d7e40806742b16e62b764b2bae37b..d852ca63a6b3a7e7118d66ce1cd9bb98e56f1a73 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -12,6 +12,7 @@ from ipaplatform.paths import paths + from collections import Counter + + import pytest ++import re + import textwrap + + +@@ -65,29 +66,7 @@ def prepare_ipa_server(master): + "--secondary-rid-base=400000", + ] + ) +- +- # Add Automount locations and maps +- master.run_command(["ipa", "automountlocation-add", "baltimore"]) +- master.run_command(["ipa", "automountmap-add", "baltimore", "auto.share"]) +- master.run_command( +- [ +- "ipa", +- "automountmap-add-indirect", +- "baltimore", +- "--parentmap=auto.share", +- "--mount=sub auto.man", +- ] +- ) +- master.run_command( +- [ +- "ipa", +- "automountkey-add", +- "baltimore", +- "auto.master", +- "--key=/share", +- "--info=auto.share", +- ] +- ) ++ master.run_command(["ipactl", "restart"]) + + # Run ipa-adtrust-install + master.run_command(["dnf", "install", "-y", "ipa-server-trust-ad"]) +@@ -235,6 +214,17 @@ def prepare_ipa_server(master): + ["ipa", "hbacrule-add-service", "--hbacsvcs=sshd", "testuser_sshd"] + ) + ++ # Add DNSForwardzone ++ master.run_command( ++ [ ++ "ipa", ++ "dnsforwardzone-add", ++ "forwardzone.test", ++ "--forwarder", ++ "10.11.12.13", ++ ] ++ ) ++ + # Vault addition + master.run_command( + [ +@@ -244,6 +234,7 @@ def prepare_ipa_server(master): + "vault1234", + "--type", + "symmetric", ++ "testvault", + ] + ) + +@@ -260,7 +251,46 @@ def prepare_ipa_server(master): + + # Modify passkeyconfig + master.run_command( +- ["ipa", "passkeyconfig-mod", "--require-user-verification=FALSE"] ++ [ ++ "ipa", "passkeyconfig-mod", ++ "--require-user-verification=FALSE" ++ ] ++ ) ++ ++ # Adding automountlocation, maps, keys ++ master.run_command( ++ [ ++ "ipa", "automountlocation-add", ++ "baltimore" ++ ] ++ ) ++ ++ master.run_command( ++ [ ++ "ipa", "automountmap-add", ++ "baltimore", ++ "auto.share" ++ ] ++ ) ++ ++ master.run_command( ++ [ ++ "ipa", "automountmap-add-indirect", ++ "baltimore", ++ "--parentmap=auto.share", ++ "--mount=sub", ++ "auto.man", ++ ] ++ ) ++ ++ master.run_command( ++ [ ++ "ipa", "automountkey-add", ++ "baltimore", ++ "auto.master", ++ "--key=/share", ++ "--info=auto.share", ++ ] + ) + + +@@ -288,12 +318,24 @@ def run_migrate( + return result + + +-class TestIPAMigrateScenario1(IntegrationTest): ++@pytest.fixture() ++def empty_log_file(request): + """ +- Tier-1 tests for ipa-migrate tool with DNS enabled on +- local and remote server ++ This fixture empties the log file before ipa-migrate tool ++ is run since the log is appended everytime the tool is run. + """ ++ request.cls.replicas[0].run_command( ++ ["truncate", "-s", "0", paths.IPA_MIGRATE_LOG] ++ ) ++ yield + ++ ++class MigrationTest(IntegrationTest): ++ """ ++ This class will help setup remote IPA server(cls.master) ++ and local IPA server(cls.replicas[0]) and it will ++ also prepare the remote IPA before migration actually begins. ++ """ + num_replicas = 1 + num_clients = 1 + topology = "line" +@@ -303,14 +345,14 @@ class TestIPAMigrateScenario1(IntegrationTest): + tasks.install_master(cls.master, setup_dns=True, setup_kra=True) + prepare_ipa_server(cls.master) + tasks.install_client(cls.master, cls.clients[0], nameservers=None) ++ tasks.install_master(cls.replicas[0], setup_dns=True, setup_kra=True) + +- def test_remote_server(self): +- """ +- This test installs IPA server instead of replica on +- system under test with the same realm and domain name. +- """ +- tasks.install_master(self.replicas[0], setup_dns=True, setup_kra=True) + ++class TestIPAMigrateCLIOptions(MigrationTest): ++ """ ++ Tests to check CLI options for ipa-migrate tool with ++ DNS enabled on local and remote server. ++ """ + def test_ipa_migrate_without_kinit_as_admin(self): + """ + This test checks that ipa-migrate tool displays +@@ -417,7 +459,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + """ + ldif_file = "/tmp/test.ldif" + param = ['-x', '-o', ldif_file] +- run_migrate( ++ result = run_migrate( + self.replicas[0], + "stage-mode", + self.master.hostname, +@@ -426,45 +468,21 @@ class TestIPAMigrateScenario1(IntegrationTest): + extra_args=param, + ) + assert self.replicas[0].transport.file_exists("/tmp/test.ldif") ++ assert result.returncode == 0 + +- @pytest.fixture() +- def empty_log_file(self): +- """ +- This fixture empties the log file before ipa-migrate tool +- is run since the log is appended everytime the tool is run. +- """ +- self.replicas[0].run_command( +- ["truncate", "-s", "0", paths.IPA_MIGRATE_LOG] +- ) +- yield +- +- def test_ipa_sigden_plugin_fail_error(self, empty_log_file): +- """ +- This testcase checks that sidgen plugin fail error is +- not seen during migrate prod-mode +- """ +- SIDGEN_ERR_MSG = "SIDGEN task failed: \n" +- run_migrate( +- self.replicas[0], +- "stage-mode", +- self.master.hostname, +- "cn=Directory Manager", +- self.master.config.admin_password, +- extra_args=['-x'], +- ) +- error_msg = self.replicas[0].get_file_contents( +- paths.IPA_MIGRATE_LOG, encoding="utf-8" +- ) +- assert SIDGEN_ERR_MSG not in error_msg +- +- def test_ipa_migrate_stage_mode_dry_run(self, empty_log_file): ++ def test_ipa_migrate_stage_mode_dry_run(self): + """ + Test ipa-migrate stage mode with dry-run option ++ This test also checks SIDGEN task failure is ++ not seen in ipa migrate log. + """ + tasks.kinit_admin(self.master) + tasks.kinit_admin(self.replicas[0]) ++ SIDGEN_ERR_MSG = "SIDGEN task failed: \n" + IPA_MIGRATE_STAGE_DRY_RUN_LOG = "--dryrun=True\n" +- IPA_SERVER_UPRGADE_LOG = "Skipping ipa-server-upgrade in dryrun mode.\n" ++ IPA_SERVER_UPRGADE_LOG = ( ++ "Skipping ipa-server-upgrade in dryrun mode.\n" ++ ) + IPA_SKIP_SIDGEN_LOG = "Skipping SIDGEN task in dryrun mode." + result = run_migrate( + self.replicas[0], +@@ -481,6 +499,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + assert IPA_MIGRATE_STAGE_DRY_RUN_LOG in install_msg + assert IPA_SERVER_UPRGADE_LOG in install_msg + assert IPA_SKIP_SIDGEN_LOG in install_msg ++ assert SIDGEN_ERR_MSG not in install_msg + + def test_ipa_migrate_prod_mode_dry_run(self, empty_log_file): + """ +@@ -509,7 +528,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + assert IPA_SERVER_UPRGADE_LOG in install_msg + assert IPA_SIDGEN_LOG in install_msg + +- def test_ipa_migrate_with_skip_schema_option_dry_run(self, empty_log_file): ++ def test_ipa_migrate_skip_schema_dry_run(self, empty_log_file): + """ + This test checks that ipa-migrate tool works + with -S(schema) options in stage mode +@@ -532,7 +551,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + ) + assert SKIP_SCHEMA_MSG_LOG in install_msg + +- def test_ipa_migrate_with_skip_config_option_dry_run(self, empty_log_file): ++ def test_ipa_migrate_skip_config_dry_run(self, empty_log_file): + """ + This test checks that ipa-migrate tool works + with -C(config) options in stage mode +@@ -579,7 +598,7 @@ class TestIPAMigrateScenario1(IntegrationTest): + ) + assert RESET_RANGE_LOG in install_msg + +- def test_ipa_migrate_stage_mode_dry_override_schema(self, empty_log_file): ++ def test_ipa_migrate_stage_mode_override_schema(self, empty_log_file): + """ + This test checks that -O option (override schema) works + in dry mode +@@ -601,70 +620,6 @@ class TestIPAMigrateScenario1(IntegrationTest): + ) + assert SCHEMA_OVERRIDE_LOG in install_msg + +- def test_ipa_migrate_stage_mode(self, empty_log_file): +- """ +- This test checks that ipa-migrate is successful +- in dry run mode +- """ +- tasks.kinit_admin(self.master) +- tasks.kinit_admin(self.replicas[0]) +- MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" +- MIGRATION_CONFIG_LOG_MSG = "Migrating configuration ...\n" +- IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this may take a while)\n" +- ) +- SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" +- MIGRATION_COMPLETE_LOG_MSG = "Migration complete!\n" +- result = run_migrate( +- self.replicas[0], +- "stage-mode", +- self.master.hostname, +- "cn=Directory Manager", +- self.master.config.admin_password, +- extra_args=['-n'], +- ) +- install_msg = self.replicas[0].get_file_contents( +- paths.IPA_MIGRATE_LOG, encoding="utf-8" +- ) +- assert result.returncode == 0 +- assert MIGRATION_SCHEMA_LOG_MSG in install_msg +- assert MIGRATION_CONFIG_LOG_MSG in install_msg +- assert IPA_UPGRADE_LOG_MSG in install_msg +- assert SIDGEN_TASK_LOG_MSG in install_msg +- assert MIGRATION_COMPLETE_LOG_MSG in install_msg +- +- def test_ipa_migrate_prod_mode(self, empty_log_file): +- """ +- This test checks that ipa-migrate is successful +- in prod run mode +- """ +- tasks.kinit_admin(self.master) +- tasks.kinit_admin(self.replicas[0]) +- MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" +- MIGRATION_DATABASE_LOG_MSG = ( +- "Migrating database ... (this may take a while)\n" +- ) +- IPA_UPGRADE_LOG_MSG = ( +- "Running ipa-server-upgrade ... (this may take a while)\n" +- ) +- SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" +- result = run_migrate( +- self.replicas[0], +- "prod-mode", +- self.master.hostname, +- "cn=Directory Manager", +- self.master.config.admin_password, +- extra_args=['-n'], +- ) +- install_msg = self.replicas[0].get_file_contents( +- paths.IPA_MIGRATE_LOG, encoding="utf-8" +- ) +- assert result.returncode == 0 +- assert MIGRATION_SCHEMA_LOG_MSG in install_msg +- assert MIGRATION_DATABASE_LOG_MSG in install_msg +- assert IPA_UPGRADE_LOG_MSG in install_msg +- assert SIDGEN_TASK_LOG_MSG in install_msg +- + def test_ipa_migrate_with_bind_pwd_file_option(self, empty_log_file): + """ + This testcase checks that ipa-migrate tool +@@ -801,6 +756,9 @@ class TestIPAMigrateScenario1(IntegrationTest): + + @pytest.fixture() + def modify_dns_zone(self): ++ """ ++ This fixture adds dnszone and then removes the zone. ++ """ + zone_name = 'ipatest.test' + self.master.run_command( + ["ipa", "dnszone-add", zone_name, "--force"] +@@ -844,6 +802,20 @@ class TestIPAMigrateScenario1(IntegrationTest): + assert DNS_LOG2 in install_msg + assert DNS_LOG3 in install_msg + ++ def test_ipa_migrate_dns_forwardzone(self): ++ """ ++ This testcase checks that DNS forwardzone is ++ also migrated in prod-mode ++ """ ++ zone_name = "forwardzone.test" ++ result = self.replicas[0].run_command( ++ ["ipa", "dnsforwardzone-show", zone_name] ++ ) ++ assert 'Zone name: {}'.format(zone_name) in result.stdout_text ++ assert 'Active zone: True' in result.stdout_text ++ assert 'Zone forwarders: 10.11.12.13' in result.stdout_text ++ assert 'Forward policy: first' in result.stdout_text ++ + def test_ipa_migrate_version_option(self): + """ + The -V option has been removed. +@@ -922,20 +894,179 @@ class TestIPAMigrateScenario1(IntegrationTest): + assert result.returncode == 1 + assert ERR_MSG in result.stderr_text + +- def test_ipa_hbac_rule_duplication(self): ++ ++class TestIPAMigrationStageMode(MigrationTest): ++ """ ++ Tests for ipa-migrate tool in stage mode ++ """ ++ def test_ipa_migrate_stage_mode(self, empty_log_file): + """ +- This testcase checks that default hbac rules +- are not duplicated on the local server when +- ipa-migrate command is run. ++ This test checks that ipa-migrate is successful ++ in dry run mode + """ ++ tasks.kinit_admin(self.master) ++ tasks.kinit_admin(self.replicas[0]) ++ MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" ++ MIGRATION_CONFIG_LOG_MSG = "Migrating configuration ...\n" ++ IPA_UPGRADE_LOG_MSG = ( ++ "Running ipa-server-upgrade ... (this may take a while)\n" ++ ) ++ SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" ++ MIGRATION_COMPLETE_LOG_MSG = "Migration complete!\n" + run_migrate( ++ self.replicas[0], ++ "stage-mode", ++ self.master.hostname, ++ "cn=Directory Manager", ++ self.master.config.admin_password, ++ extra_args=['-n'], ++ ) ++ install_msg = self.replicas[0].get_file_contents( ++ paths.IPA_MIGRATE_LOG, encoding="utf-8" ++ ) ++ assert MIGRATION_SCHEMA_LOG_MSG in install_msg ++ assert MIGRATION_CONFIG_LOG_MSG in install_msg ++ assert IPA_UPGRADE_LOG_MSG in install_msg ++ assert SIDGEN_TASK_LOG_MSG in install_msg ++ assert MIGRATION_COMPLETE_LOG_MSG in install_msg ++ ++ def test_ipa_migrate_stage_mode_new_user(self): ++ """ ++ This testcase checks that when a new user is added and ++ ipa-migrate is run in stage-mode, uid/gid of the ++ migrated user is not preserved i.e we have different ++ uid/gid for user on remote and local IPA server. ++ """ ++ username = 'testuser4' ++ base_dn = str(self.master.domain.basedn) ++ LOG_MSG1 = ( ++ "DEBUG Resetting the DNA range for new entry: " ++ "uid={},cn=users,cn=accounts,{}\n" ++ ).format(username, base_dn) ++ install_msg = self.replicas[0].get_file_contents( ++ paths.IPA_MIGRATE_LOG, encoding="utf-8" ++ ) ++ assert LOG_MSG1 not in install_msg ++ tasks.clear_sssd_cache(self.master) ++ self.master.run_command(['ipa', 'user-show', username]) ++ cmd1 = self.master.run_command(['id', username]) ++ tasks.clear_sssd_cache(self.replicas[0]) ++ self.replicas[0].run_command(['ipa', 'user-show', username]) ++ cmd2 = self.replicas[0].run_command(['id', username]) ++ assert cmd1.stdout_text != cmd2.stdout_text ++ ++ ++class TestIPAMigrationProdMode(MigrationTest): ++ """ ++ Tests for ipa-migrate tool in prod mode ++ """ ++ def test_ipa_migrate_prod_mode(self, empty_log_file): ++ """ ++ This test checks that ipa-migrate is successful ++ in prod run mode ++ """ ++ tasks.kinit_admin(self.master) ++ tasks.kinit_admin(self.replicas[0]) ++ MIGRATION_SCHEMA_LOG_MSG = "Migrating schema ...\n" ++ MIGRATION_DATABASE_LOG_MSG = ( ++ "Migrating database ... (this may take a while)\n" ++ ) ++ IPA_UPGRADE_LOG_MSG = ( ++ "Running ipa-server-upgrade ... (this may take a while)\n" ++ ) ++ SIDGEN_TASK_LOG_MSG = "Running SIDGEN task ...\n" ++ result = run_migrate( + self.replicas[0], + "prod-mode", + self.master.hostname, + "cn=Directory Manager", + self.master.config.admin_password, +- extra_args=['-n'] ++ extra_args=['-n'], + ) ++ install_msg = self.replicas[0].get_file_contents( ++ paths.IPA_MIGRATE_LOG, encoding="utf-8" ++ ) ++ assert result.returncode == 0 ++ assert MIGRATION_SCHEMA_LOG_MSG in install_msg ++ assert MIGRATION_DATABASE_LOG_MSG in install_msg ++ assert IPA_UPGRADE_LOG_MSG in install_msg ++ assert SIDGEN_TASK_LOG_MSG in install_msg ++ ++ def test_ipa_migrate_prod_mode_hbac_rule(self): ++ """ ++ This testcase checks that hbac rule is migrated from ++ remote server to local server in prod mode. ++ """ ++ hbac_rule_name1 = 'test1' ++ hbac_rule_name2 = 'testuser_sshd' ++ tasks.kinit_admin(self.replicas[0]) ++ cmd1 = self.replicas[0].run_command( ++ ["ipa", "hbacrule-find", hbac_rule_name1]) ++ cmd2 = self.replicas[0].run_command( ++ ["ipa", "hbacrule-find", hbac_rule_name2]) ++ assert hbac_rule_name1 in cmd1.stdout_text ++ assert hbac_rule_name2 in cmd2.stdout_text ++ ++ def test_ipa_migrate_prod_mode_sudo_rule(self): ++ """ ++ This testcase checks that sudo cmd and rules are ++ migrated from remote server to local server in prod mode. ++ """ ++ sudorule = 'readfiles' ++ sudocmd = '/usr/bin/less' ++ tasks.kinit_admin(self.replicas[0]) ++ cmd1 = self.replicas[0].run_command( ++ ["ipa", "sudorule-find", sudorule]) ++ cmd2 = self.replicas[0].run_command( ++ ["ipa", "sudocmd-find", sudocmd]) ++ assert 'Rule name: readfiles\n' in cmd1.stdout_text ++ assert 'Sudo Command: /usr/bin/less\n' in cmd2.stdout_text ++ ++ def test_ipa_migrate_prod_mode_new_user_sid(self): ++ """ ++ This testcase checks that in prod-mode uid/gid of the ++ migrated user is preserved i.e we have same ++ uid/gid for user on remote and local IPA server. ++ """ ++ username = 'testuser4' ++ tasks.clear_sssd_cache(self.master) ++ result1 = self.master.run_command(['id', username]) ++ tasks.clear_sssd_cache(self.replicas[0]) ++ result2 = self.replicas[0].run_command(['id', username]) ++ assert result1.stdout_text == result2.stdout_text ++ ++ def test_check_vault_is_not_migrated(self): ++ """ ++ This testcase checks that vault is ++ not migrated ++ """ ++ vault_name = "testvault" ++ CMD_OUTPUT = "Number of entries returned 0" ++ cmd = self.replicas[0].run_command( ++ ["ipa", "vault-find", vault_name], raiseonerr=False) ++ assert cmd.returncode != 0 ++ assert CMD_OUTPUT in cmd.stdout_text ++ ++ def test_ipa_migrate_subids(self): ++ """ ++ This testcase checks that subids for users are migrated ++ to the local server from the remote server ++ """ ++ user_name = 'admin' ++ CMD_MSG = "1 subordinate id matched" ++ cmd = self.replicas[0].run_command( ++ ['ipa', 'subid-find', ++ '--owner', user_name] ++ ) ++ assert cmd.returncode == 0 ++ assert CMD_MSG in cmd.stdout_text ++ ++ def test_ipa_hbac_rule_duplication(self): ++ """ ++ This testcase checks that default hbac rules ++ are not duplicated on the local server when ++ ipa-migrate command is run. ++ """ + result = self.replicas[0].run_command( + ['ipa', 'hbacrule-find'] + ) +@@ -946,3 +1077,192 @@ class TestIPAMigrateScenario1(IntegrationTest): + count = Counter(line) + assert count.get('Rule name: allow_all') < 2 + assert count.get('Rule name: allow_systemd-user') < 2 ++ ++ def test_ipa_migrate_otptoken(self): ++ """ ++ This testcase checks that the otptoken ++ is migrated for the user. ++ """ ++ owner = "testuser1" ++ CMD_OUTPUT = "1 OTP token matched" ++ result = self.replicas[0].run_command([ ++ "ipa", "otptoken-find" ++ ]) ++ assert CMD_OUTPUT in result.stdout_text ++ assert 'Type: TOTP' in result.stdout_text ++ assert 'Owner: {}'.format(owner) in result.stdout_text ++ ++ def test_ipa_migrate_check_passkey_config(self): ++ """ ++ This testcase checks that passkey config ++ is migrated ++ """ ++ CMD_OUTPUT = "Require user verification: False" ++ result = self.replicas[0].run_command([ ++ "ipa", "passkeyconfig-show" ++ ]) ++ assert CMD_OUTPUT in result.stdout_text ++ ++ def test_ipa_migrate_check_service_status(self): ++ """ ++ This testcase checks that ipactl and sssd ++ services are running post ipa-migrate tool ++ successful runs completed ++ """ ++ cmd1 = self.replicas[0].run_command([ ++ "ipactl", "status" ++ ]) ++ assert cmd1.returncode == 0 ++ cmd2 = self.replicas[0].run_command([ ++ "systemctl", "status", "sssd" ++ ]) ++ assert cmd2.returncode == 0 ++ ++ def test_custom_idrange_is_migrated(self): ++ """ ++ This testcase checks that custom idrange is migrated ++ from remote server to local server in production ++ mode. ++ """ ++ range_name = "testrange" ++ CMD_OUTPUT = ( ++ "---------------\n" ++ "1 range matched\n" ++ "---------------\n" ++ " Range name: testrange\n" ++ " First Posix ID of the range: 10000\n" ++ " Number of IDs in the range: 10000\n" ++ " First RID of the corresponding RID range: 300000\n" ++ " First RID of the secondary RID range: 400000\n" ++ " Range type: local domain range\n" ++ "----------------------------\n" ++ "Number of entries returned 1\n" ++ "----------------------------\n" ++ ) ++ cmd = self.replicas[0].run_command( ++ ["ipa", "idrange-find", range_name]) ++ assert CMD_OUTPUT in cmd.stdout_text ++ ++ def test_automountlocation_is_migrated(self): ++ """ ++ This testcase checks that automount location/maps ++ and keys are migrated. ++ """ ++ base_dn = str(self.master.domain.basedn) ++ automount_cn = "automount" ++ loc_name = "baltimore" ++ auto_map_name = "auto.share" ++ DEBUG_LOG = ( ++ "Added entry: cn={},cn={},{}\n" ++ ).format(loc_name, automount_cn, base_dn) ++ CMD1_OUTPUT = ( ++ " Location: baltimore\n" ++ ) ++ CMD2_OUTPUT = ( ++ " Map: auto.share\n" ++ ) ++ CMD3_OUTPUT = ( ++ "-----------------------\n" ++ "1 automount key matched\n" ++ "-----------------------\n" ++ " Key: sub\n" ++ " Mount information: -fstype=autofs ldap:auto.man\n" ++ ) ++ cmd1 = self.replicas[0].run_command( ++ ["ipa", "automountlocation-show", loc_name]) ++ cmd2 = self.replicas[0].run_command( ++ ["ipa", "automountmap-find", loc_name]) ++ cmd3 = self.replicas[0].run_command( ++ ["ipa", "automountkey-find", loc_name, auto_map_name] ++ ) ++ install_msg = self.replicas[0].get_file_contents( ++ paths.IPA_MIGRATE_LOG, encoding="utf-8" ++ ) ++ assert CMD1_OUTPUT in cmd1.stdout_text ++ assert CMD2_OUTPUT in cmd2.stdout_text ++ assert CMD3_OUTPUT in cmd3.stdout_text ++ assert DEBUG_LOG in install_msg ++ ++ ++class TestIPAMigrationWithADtrust(IntegrationTest): ++ """ ++ Test for ipa-migrate tool with IPA Master having trust setup ++ with Windows AD. ++ """ ++ topology = "line" ++ num_ad_domains = 1 ++ num_replicas = 1 ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master( ++ cls.master, setup_dns=True, extra_args=['--no-dnssec-validation'] ++ ) ++ cls.ad = cls.ads[0] ++ cls.ad_domain = cls.ad.domain.name ++ tasks.install_adtrust(cls.master) ++ tasks.configure_dns_for_trust(cls.master, cls.ad) ++ tasks.establish_trust_with_ad(cls.master, cls.ad.domain.name) ++ ++ def test_install_local_server(self): ++ """ ++ This test installs local IPA Server() i.e new IPA server with ++ the same realm and domain name that will receive the migration data. ++ """ ++ tasks.install_master( ++ self.replicas[0], setup_dns=True, ++ extra_args=['--no-dnssec-validation'] ++ ) ++ tasks.install_adtrust(self.replicas[0]) ++ ++ def test_check_ad_attributes_migrate_prod_mode(self): ++ """ ++ This test checks that IPA-AD trust related attributes ++ are migrated to local server. ++ """ ++ result = run_migrate( ++ self.replicas[0], ++ "prod-mode", ++ self.master.hostname, ++ "cn=Directory Manager", ++ self.master.config.admin_password, ++ extra_args=['-n'] ++ ) ++ assert result.returncode == 0 ++ trust1 = self.master.run_command( ++ ['ipa', 'trust-show', self.ad_domain] ++ ).stdout_text ++ trust2 = self.replicas[0].run_command( ++ ['ipa', 'trust-show', self.ad_domain]).stdout_text ++ assert trust1 == trust2 ++ ++ def test_check_domain_sid_is_migrated(self): ++ """ ++ This testcase checks that domain sid is ++ migrated from a remote server having trust with AD ++ to local server and is displayed in the ++ ipa trustconfig-show command ++ """ ++ regexp = (r'Security Identifier: (.*)$') ++ cmd1 = self.master.run_command(["ipa", "trustconfig-show"]) ++ sid1 = re.findall(regexp, cmd1.stdout_text, re.MULTILINE) ++ cmd2 = self.replicas[0].run_command( ++ ["ipa", "trustconfig-show"] ++ ) ++ sid2 = re.findall(regexp, cmd2.stdout_text, re.MULTILINE) ++ assert sid1 == sid2 ++ ++ def test_check_ad_idrange_is_migrated(self): ++ """ ++ This testcase checks AD idrange is migrated ++ from remote IPA server having trust with AD ++ to local IPA server ++ """ ++ ad_domain_name = self.ad.domain.name.upper() ++ cmd1 = self.master.run_command( ++ ["ipa", "idrange-show", ad_domain_name + "_id_range"] ++ ) ++ cmd2 = self.replicas[0].run_command( ++ ["ipa", "idrange-show", ad_domain_name + "_id_range"] ++ ) ++ assert cmd1.stdout_text == cmd2.stdout_text +-- +2.47.0 + diff --git a/SOURCES/0026-Fix-Organization-field-in-Okta-not-required.patch b/SOURCES/0026-Fix-Organization-field-in-Okta-not-required.patch new file mode 100644 index 0000000..10f6abb --- /dev/null +++ b/SOURCES/0026-Fix-Organization-field-in-Okta-not-required.patch @@ -0,0 +1,34 @@ +From c64c098e1d0ae492499caa83a1b73532da511f84 Mon Sep 17 00:00:00 2001 +From: Carla Martinez +Date: Tue, 29 Oct 2024 15:23:55 +0100 +Subject: [PATCH] Fix: 'Organization' field in Okta not required + +Although the 'Organization' field is not required +when using the Okta template, the WebUI requires it +in order to create a new IDP. If this is not provided, +an error is shown. + +Fixes: https://pagure.io/freeipa/issue/9687 +Signed-off-by: Carla Martinez +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + install/ui/src/freeipa/idp.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/install/ui/src/freeipa/idp.js b/install/ui/src/freeipa/idp.js +index ada09c0754f5a51575831e127deb81d1f27f44d1..04daad591a8e94ea9b8c146c12e0c84aaad6cee4 100644 +--- a/install/ui/src/freeipa/idp.js ++++ b/install/ui/src/freeipa/idp.js +@@ -41,7 +41,7 @@ idp.templates = [ + fields: ['ipaidporg']}, + { value: 'okta', + label: text.get('@i18n:objects.idp.template_okta'), +- fields: ['ipaidporg', 'ipaidpbaseurl']} ++ fields: ['ipaidpbaseurl']} + ]; + + +-- +2.47.0 + diff --git a/SOURCES/0027-ipatests-install-master-with-allow-zone-overlap.patch b/SOURCES/0027-ipatests-install-master-with-allow-zone-overlap.patch new file mode 100644 index 0000000..04aabe6 --- /dev/null +++ b/SOURCES/0027-ipatests-install-master-with-allow-zone-overlap.patch @@ -0,0 +1,42 @@ +From baa9fc3e3e2f6b39db5ec465c92dc597cd5399b9 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 12 Nov 2024 16:44:46 +0100 +Subject: [PATCH] ipatests: install master with allow-zone-overlap + +In the IPA to IPA migration tests, install the destination master +with --setup-dns --allow-zone-overlap to allow installation +even if the zone is already served by the source master. + +Fixes: https://pagure.io/freeipa/issue/9697 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Sudhir Menon +--- + ipatests/test_integration/test_ipa_ipa_migration.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index d852ca63a6b3a7e7118d66ce1cd9bb98e56f1a73..0c637a0141d95f34f951c60a9648adf8e87eaa63 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -345,7 +345,8 @@ class MigrationTest(IntegrationTest): + tasks.install_master(cls.master, setup_dns=True, setup_kra=True) + prepare_ipa_server(cls.master) + tasks.install_client(cls.master, cls.clients[0], nameservers=None) +- tasks.install_master(cls.replicas[0], setup_dns=True, setup_kra=True) ++ tasks.install_master(cls.replicas[0], setup_dns=True, setup_kra=True, ++ extra_args=['--allow-zone-overlap']) + + + class TestIPAMigrateCLIOptions(MigrationTest): +@@ -1211,7 +1212,7 @@ class TestIPAMigrationWithADtrust(IntegrationTest): + """ + tasks.install_master( + self.replicas[0], setup_dns=True, +- extra_args=['--no-dnssec-validation'] ++ extra_args=['--no-dnssec-validation', '--allow-zone-overlap'] + ) + tasks.install_adtrust(self.replicas[0]) + +-- +2.47.0 + diff --git a/SOURCES/0028-selinux-allow-Cockpit-to-use-HTTP-keytab-on-IPA-serv.patch b/SOURCES/0028-selinux-allow-Cockpit-to-use-HTTP-keytab-on-IPA-serv.patch new file mode 100644 index 0000000..a432535 --- /dev/null +++ b/SOURCES/0028-selinux-allow-Cockpit-to-use-HTTP-keytab-on-IPA-serv.patch @@ -0,0 +1,148 @@ +From c71e12e902b3912c31245d46ad6f2c2ddee01126 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Tue, 1 Oct 2024 11:28:28 +0300 +Subject: [PATCH] selinux: allow Cockpit to use HTTP keytab on IPA servers + +Cockpit can use GSSAPI authentication and has pretty good definition of +how to enable it: https://cockpit-project.org/guide/latest/sso.html. +These instructions work on IPA clients but they cannot be used on IPA +servers because IPA framework already owns HTTP/.. Kerberos service and +its keytab. + +Luckily, there are two changes that need to be done to enable Cockpit +single sign-on with GSSAPI on IPA servers: + + - create a symlink /etc/cockpit/krb5.keytab to + /var/lib/ipa/gssproxy/http.keytab + + - add SELinux policy to allow cockpit_session_t to operate on + /var/lib/ipa/gssproxy/http.keytab file + +For existing installation an upgrade process would restore SELinux +context of the http.keytab file to the new value. + +Note that Cockpit documentation above also talks about Kerberos service +modifications to enable delegation. These modifications should not be +done for IPA servers' HTTP services, as these services are already +enabled to handle delegation. + +Fixes: https://pagure.io/freeipa/issue/9675 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipaserver/install/server/upgrade.py | 1 + + selinux/ipa.fc | 2 ++ + selinux/ipa.if | 24 ++++++++++++++++++++++++ + selinux/ipa.te | 19 +++++++++++++++++++ + 4 files changed, 46 insertions(+) + +diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py +index 31d4f8398cfb0251cc59ada909eb55635b83e960..d5c466ee2f905eafd15663fef46d052ade30d742 100644 +--- a/ipaserver/install/server/upgrade.py ++++ b/ipaserver/install/server/upgrade.py +@@ -1124,6 +1124,7 @@ def update_http_keytab(http): + paths.OLD_IPA_KEYTAB, e + ) + http.keytab_user.chown(http.keytab) ++ tasks.restore_context(http.keytab) + + + def ds_enable_sidgen_extdom_plugins(ds): +diff --git a/selinux/ipa.fc b/selinux/ipa.fc +index 47bd19ba77418cad1f0904dc4a9a35ce9d6ff9d2..15e8e41aa50228ff560e338044240b46bc24cc40 100644 +--- a/selinux/ipa.fc ++++ b/selinux/ipa.fc +@@ -22,6 +22,8 @@ + + /var/lib/ipa(/.*)? gen_context(system_u:object_r:ipa_var_lib_t,s0) + ++/var/lib/ipa/gssproxy/http.keytab -- gen_context(system_u:object_r:ipa_http_keytab_t,s0) ++ + /var/log/ipa(/.*)? gen_context(system_u:object_r:ipa_log_t,s0) + + /var/log/ipabackup.log -- gen_context(system_u:object_r:ipa_log_t,s0) +diff --git a/selinux/ipa.if b/selinux/ipa.if +index 8c47e7963af92b1ddcd59d92aa45d6b8e9c0c6cc..8f3147e10bd294665dd41e1c1f99c993d9699d20 100644 +--- a/selinux/ipa.if ++++ b/selinux/ipa.if +@@ -155,6 +155,7 @@ interface(`ipa_manage_log',` + ######################################## + ## + ## Allow domain to manage ipa lib files/dirs. ++## This includes reading ipa_http_keytab_t files. + ## + ## + ## +@@ -164,10 +165,33 @@ interface(`ipa_manage_log',` + # + interface(`ipa_read_lib',` + gen_require(` ++ type ipa_http_keytab_t; + type ipa_var_lib_t; + ') + + read_files_pattern($1, ipa_var_lib_t, ipa_var_lib_t) ++ read_files_pattern($1, ipa_http_keytab_t, ipa_http_keytab_t) ++ list_dirs_pattern($1, ipa_var_lib_t, ipa_var_lib_t) ++') ++ ++######################################## ++## ++## Allow domain to manage ipa HTTP keytab file. ++## This includes reading ipa_var_lib_t directories. ++## ++## ++## ++## Domain allowed access. ++## ++## ++# ++interface(`ipa_read_http_keytab',` ++ gen_require(` ++ type ipa_http_keytab_t; ++ type ipa_var_lib_t; ++ ') ++ ++ read_files_pattern($1, ipa_http_keytab_t, ipa_http_keytab_t) + list_dirs_pattern($1, ipa_var_lib_t, ipa_var_lib_t) + ') + +diff --git a/selinux/ipa.te b/selinux/ipa.te +index 2546a9bd9468200185c484974a9e71f16f89de71..e4ce66687a48b27e85591cdd8352f7cac94d3151 100644 +--- a/selinux/ipa.te ++++ b/selinux/ipa.te +@@ -43,6 +43,9 @@ logging_log_file(ipa_log_t) + type ipa_var_lib_t; + files_type(ipa_var_lib_t) + ++type ipa_http_keytab_t; ++files_type(ipa_http_keytab_t) ++ + type ipa_var_run_t; + files_pid_file(ipa_var_run_t) + +@@ -516,3 +519,19 @@ optional_policy(` + ') + allow certmonger_t pki_tomcat_etc_rw_t:file { getattr ioctl open read }; + ') ++ ++# gssproxy needs to read http keytab ++optional_policy(` ++ gen_require(` ++ type gssproxy_t; ++ ') ++ ipa_read_http_keytab(gssproxy_t) ++') ++ ++# Allow Cockpit to use HTTP keytab on IPA servers for GSSAPI authentication ++optional_policy(` ++ gen_require(` ++ type cockpit_session_t; ++ ') ++ ipa_read_http_keytab(cockpit_session_t) ++') +-- +2.47.0 + diff --git a/SOURCES/0029-Minimal-test-for-Cockpit-integration-on-IPA-master.patch b/SOURCES/0029-Minimal-test-for-Cockpit-integration-on-IPA-master.patch new file mode 100644 index 0000000..d079f16 --- /dev/null +++ b/SOURCES/0029-Minimal-test-for-Cockpit-integration-on-IPA-master.patch @@ -0,0 +1,94 @@ +From 0dadcbb4ac9f6142b5130f025f64d918d6f208a9 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Tue, 8 Oct 2024 10:25:08 +0300 +Subject: [PATCH] Minimal test for Cockpit integration on IPA master + +Add a test to share HTTP service keytab on IPA master between IPA and +Cockpit. The test configures Cockpit with IPA CA-issued certificate and +allows Cockpit to access IPA HTTP service keytab for authentication. + +The test then attempts to authenticate with GSSAPI as admin user. A +successful result is when we receive CSRF token from the Cockpit as +the result of this authentication. This means we have logged in +successfully with Kerberos. + +Fixes: https://pagure.io/freeipa/issue/9675 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_cockpit.py | 61 +++++++++++++++++++++++ + 1 file changed, 61 insertions(+) + create mode 100644 ipatests/test_integration/test_cockpit.py + +diff --git a/ipatests/test_integration/test_cockpit.py b/ipatests/test_integration/test_cockpit.py +new file mode 100644 +index 0000000000000000000000000000000000000000..cdc96170a116536c7aa00be78cc4e0225804e21c +--- /dev/null ++++ b/ipatests/test_integration/test_cockpit.py +@@ -0,0 +1,61 @@ ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++ ++from __future__ import absolute_import ++ ++import time ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++from ipaplatform.paths import paths ++ ++ ++class TestCockpitIntegration(IntegrationTest): ++ topology = "line" ++ reqcert = '/etc/cockpit/ws-certs.d/99-cockpit.cert' ++ reqkey = '/etc/cockpit/ws-certs.d/99-cockpit.key' ++ symlink = '/etc/cockpit/krb5.keytab' ++ ++ @classmethod ++ def uninstall(cls, mh): ++ cls.master.run_command(['ipa-getcert', 'stop-tracking', '-f', ++ cls.reqcert], raiseonerr=False) ++ cls.master.run_command(['rm', '-f', cls.symlink], raiseonerr=False) ++ cls.master.run_command(['systemctl', 'disable', '--now', ++ 'cockpit.socket']) ++ super(TestCockpitIntegration, cls).uninstall(mh) ++ ++ @classmethod ++ def install(cls, mh): ++ master = cls.master ++ ++ # Install Cockpit and configure it to use IPA certificate and keytab ++ master.run_command(['dnf', 'install', '-y', 'cockpit', 'curl'], ++ raiseonerr=False) ++ ++ super(TestCockpitIntegration, cls).install(mh) ++ ++ master.run_command(['ipa-getcert', 'request', '-f', cls.reqcert, '-k', ++ cls.reqkey, '-D', cls.master.hostname, '-K', ++ 'host/' + cls.master.hostname, '-m', '0640', '-o', ++ 'root:cockpit-ws', '-O', 'root:root', '-M', ++ '0644'], raiseonerr=False) ++ ++ master.run_command(['ln', '-s', paths.HTTP_KEYTAB, cls.symlink], ++ raiseonerr=False) ++ ++ time.sleep(5) ++ master.run_command(['systemctl', 'enable', '--now', 'cockpit.socket']) ++ ++ def test_login_with_kerberos(self): ++ """ ++ Login to Cockpit using GSSAPI authentication ++ """ ++ master = self.master ++ tasks.kinit_admin(master) ++ ++ cockpit_login = f'https://{master.hostname}:9090/cockpit/login' ++ result = master.run_command([paths.BIN_CURL, '-u:', '--negotiate', ++ '--cacert', paths.IPA_CA_CRT, ++ cockpit_login]) ++ assert ("csrf-token" in result.stdout_text) +-- +2.47.0 + diff --git a/SOURCES/0030-ipaserver-dcerpc-support-Samba-4.21.patch b/SOURCES/0030-ipaserver-dcerpc-support-Samba-4.21.patch new file mode 100644 index 0000000..241d410 --- /dev/null +++ b/SOURCES/0030-ipaserver-dcerpc-support-Samba-4.21.patch @@ -0,0 +1,38 @@ +From c306c613399cdd9a2c716b83ce0d47d320aec2a8 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Tue, 19 Nov 2024 12:57:46 +0200 +Subject: [PATCH] ipaserver/dcerpc: support Samba 4.21 + +Samba 4.21 moved samba.trust_utils module to samba.lsa_utils. + +Fixes: https://pagure.io/freeipa/issue/9702 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/dcerpc.py | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py +index a28c72361276f12a1a02cd126425ac3c62eddd4f..3344ea226e3cba61912e717f9c375612bb4707e0 100644 +--- a/ipaserver/dcerpc.py ++++ b/ipaserver/dcerpc.py +@@ -55,9 +55,13 @@ from samba import ntstatus + import samba + + try: +- from samba.trust_utils import CreateTrustedDomainRelax ++ from samba.lsa_utils import CreateTrustedDomainRelax + except ImportError: +- CreateTrustedDomainRelax = None ++ try: ++ from samba.trust_utils import CreateTrustedDomainRelax ++ except ImportError: ++ CreateTrustedDomainRelax = None ++ + try: + from samba import arcfour_encrypt + except ImportError: +-- +2.47.0 + diff --git a/SOURCES/0031-Allow-looking-up-constants.Group-by-gid-in-addition-.patch b/SOURCES/0031-Allow-looking-up-constants.Group-by-gid-in-addition-.patch new file mode 100644 index 0000000..0e4333b --- /dev/null +++ b/SOURCES/0031-Allow-looking-up-constants.Group-by-gid-in-addition-.patch @@ -0,0 +1,66 @@ +From 184589fac4ff36b5583541f40dff91296c33370a Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 2 Dec 2024 10:23:29 -0500 +Subject: [PATCH] Allow looking up constants.Group by gid in addition to name + +This adds flexibility so we can look up groups by both gid and +by name in order to have a more consistent API for management. + +Related: https://pagure.io/freeipa/issue/9709 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaplatform/base/constants.py | 5 ++++- + ipatests/test_ipaplatform/test_constants.py | 11 +++++++++++ + 2 files changed, 15 insertions(+), 1 deletion(-) + +diff --git a/ipaplatform/base/constants.py b/ipaplatform/base/constants.py +index 1689efe52466f00fd8b014f720e1d21ebdbf2504..f1ef7efff502573bab82e890bcdf87c0ec52a399 100644 +--- a/ipaplatform/base/constants.py ++++ b/ipaplatform/base/constants.py +@@ -86,7 +86,10 @@ class Group(_Entity): + try: + self._entity = entity = grp.getgrnam(self) + except KeyError: +- raise ValueError(f"group '{self!s}' not found") from None ++ try: ++ self._entity = entity = grp.getgrgid(int(self)) ++ except (TypeError, ValueError): ++ raise ValueError(f"group '{self!s}' not found") from None + return entity + + @property +diff --git a/ipatests/test_ipaplatform/test_constants.py b/ipatests/test_ipaplatform/test_constants.py +index b57bfa48e5ccefe2b22cb00aca8436e0edc01a30..9bb12283609f87bcd875a2c55ee1e8b714dd8b3a 100644 +--- a/ipatests/test_ipaplatform/test_constants.py ++++ b/ipatests/test_ipaplatform/test_constants.py +@@ -1,6 +1,7 @@ + # + # Copyright (C) 2020 FreeIPA Contributors see COPYING for license + # ++import grp + import pytest + + from ipaplatform.base.constants import User, Group +@@ -40,6 +41,16 @@ def test_group(): + assert Group(str(group)) is not group + + ++def test_numeric_group(): ++ g = grp.getgrnam('apache') ++ group = Group(g.gr_gid) ++ assert group.gid == g.gr_gid ++ assert type(str(group)) is str ++ assert repr(group) == '' % g.gr_gid ++ assert group.gid == g.gr_gid ++ assert group.entity.gr_gid == g.gr_gid ++ ++ + def test_group_invalid(): + invalid = Group("invalid") + with pytest.raises(ValueError) as e: +-- +2.47.1 + diff --git a/SOURCES/0032-Pass-all-pkiuser-groups-as-suplementary-when-validat.patch b/SOURCES/0032-Pass-all-pkiuser-groups-as-suplementary-when-validat.patch new file mode 100644 index 0000000..d916be8 --- /dev/null +++ b/SOURCES/0032-Pass-all-pkiuser-groups-as-suplementary-when-validat.patch @@ -0,0 +1,72 @@ +From 934d4a291d44a40b5ea006aa1f09afa8e4a985fc Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 2 Dec 2024 10:27:15 -0500 +Subject: [PATCH] Pass all pkiuser groups as suplementary when validating an + HSM + +We were doing a "best effort" when validating the HSM token is +visible with a valid PIN when it came to groups. A specific +workaround was added for softhsm2 but this didn't carry over +to other HSMs that may have group-specific read/write access. + +Use the new capability in ipaplatform.constants.py::Group to be +able to use generate a valid entry from a group GID. Pair this +with os.getgrouplist() and all groups will be passed correctly +via ipautil.run(). + +Fixes: https://pagure.io/freeipa/issue/9709 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/ca.py | 12 ++++-------- + 1 file changed, 4 insertions(+), 8 deletions(-) + +diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py +index 520e3fc5de1084e7c22c0cf7eaa86e1d3c421373..2959aceed5cd2fd4851457eaa4aeac3c0905d27d 100644 +--- a/ipaserver/install/ca.py ++++ b/ipaserver/install/ca.py +@@ -211,11 +211,7 @@ def hsm_validator(token_name, token_library, token_password): + ) + pkiuser = constants.PKI_USER + pkigroup = constants.PKI_GROUP +- if 'libsofthsm' in token_library: +- import grp +- group = grp.getgrnam(constants.ODS_GROUP) +- if str(constants.PKI_USER) in group.gr_mem: +- pkigroup = constants.ODS_GROUP ++ group_list = os.getgrouplist(pkiuser, pkigroup.gid) + with certdb.NSSDatabase() as tempnssdb: + tempnssdb.create_db(user=str(pkiuser), group=str(pkigroup)) + # Try adding the token library to the temporary database in +@@ -231,7 +227,7 @@ def hsm_validator(token_name, token_library, token_password): + # It may fail if p11-kit has already registered the library, that's + # ok. + ipautil.run(command, stdin='\n', cwd=tempnssdb.secdir, +- runas=pkiuser, suplementary_groups=[pkigroup], ++ runas=pkiuser, suplementary_groups=group_list, + raiseonerr=False) + + command = [ +@@ -242,7 +238,7 @@ def hsm_validator(token_name, token_library, token_password): + ] + lines = ipautil.run( + command, cwd=tempnssdb.secdir, capture_output=True, +- runas=pkiuser, suplementary_groups=[pkigroup]).output ++ runas=pkiuser, suplementary_groups=group_list).output + found = False + token_line = f'token: {token_name}' + for line in lines.split('\n'): +@@ -265,7 +261,7 @@ def hsm_validator(token_name, token_library, token_password): + ] + result = ipautil.run(args, cwd=tempnssdb.secdir, + runas=pkiuser, +- suplementary_groups=[pkigroup], ++ suplementary_groups=group_list, + capture_error=True, raiseonerr=False) + if result.returncode != 0 and len(result.error_output): + if 'SEC_ERROR_BAD_PASSWORD' in result.error_output: +-- +2.47.1 + diff --git a/SOURCES/0007-ipalib-x509-support-PyCA-44.0.patch b/SOURCES/0033-ipalib-x509-support-PyCA-44.0.patch similarity index 100% rename from SOURCES/0007-ipalib-x509-support-PyCA-44.0.patch rename to SOURCES/0033-ipalib-x509-support-PyCA-44.0.patch diff --git a/SOURCES/0008-pyca-adapt-import-paths-for-TripleDES-cipher.patch b/SOURCES/0034-pyca-adapt-import-paths-for-TripleDES-cipher.patch similarity index 100% rename from SOURCES/0008-pyca-adapt-import-paths-for-TripleDES-cipher.patch rename to SOURCES/0034-pyca-adapt-import-paths-for-TripleDES-cipher.patch diff --git a/SOURCES/0009-ipa-pwd-extop-clarify-OTP-use-over-LDAP-binds.patch b/SOURCES/0035-ipa-pwd-extop-clarify-OTP-use-over-LDAP-binds.patch similarity index 100% rename from SOURCES/0009-ipa-pwd-extop-clarify-OTP-use-over-LDAP-binds.patch rename to SOURCES/0035-ipa-pwd-extop-clarify-OTP-use-over-LDAP-binds.patch diff --git a/SOURCES/0010-adtrust-add-missing-ipaAllowedOperations-objectclass.patch b/SOURCES/0036-adtrust-add-missing-ipaAllowedOperations-objectclass.patch similarity index 100% rename from SOURCES/0010-adtrust-add-missing-ipaAllowedOperations-objectclass.patch rename to SOURCES/0036-adtrust-add-missing-ipaAllowedOperations-objectclass.patch diff --git a/SOURCES/0037-Fix-the-typo-in-ipa_migrate_constants.patch b/SOURCES/0037-Fix-the-typo-in-ipa_migrate_constants.patch new file mode 100644 index 0000000..c75955a --- /dev/null +++ b/SOURCES/0037-Fix-the-typo-in-ipa_migrate_constants.patch @@ -0,0 +1,37 @@ +From 6a2310eda39b1341258211c7630ef4baf4555df5 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Mon, 9 Dec 2024 23:03:56 +0530 +Subject: [PATCH] Fix the typo in ipa_migrate_constants. + +ipa-migrate.log displays Privileges migrated as Privledges +due to typo in labelling i.e 'label': 'Privledges' +Hence changed that to 'label': 'Privileges' + +---- LOG FILE ---- +INFO - Privledges: 3 +------------------ + +Fixes: https://pagure.io/freeipa/issue/9715 + +Signed-off-by: Sudhir Menon +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/ipa_migrate_constants.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ipaserver/install/ipa_migrate_constants.py b/ipaserver/install/ipa_migrate_constants.py +index c140414ea6c607a93e35ef0705480d1002b7945e..e8192fb1aabae1c36669370eff242428a1f0355f 100644 +--- a/ipaserver/install/ipa_migrate_constants.py ++++ b/ipaserver/install/ipa_migrate_constants.py +@@ -886,7 +886,7 @@ DB_OBJECTS = { + 'pbac_priv': { + 'oc': ['groupofnames'], + 'subtree': ',cn=privileges,cn=pbac,$SUFFIX', +- 'label': 'Privledges', ++ 'label': 'Privileges', + 'mode': 'all', + 'count': 0, + }, +-- +2.47.1 + diff --git a/SOURCES/0038-test_ipahealthcheck-skip-connectivity_and_data-check.patch b/SOURCES/0038-test_ipahealthcheck-skip-connectivity_and_data-check.patch new file mode 100644 index 0000000..c13ec2a --- /dev/null +++ b/SOURCES/0038-test_ipahealthcheck-skip-connectivity_and_data-check.patch @@ -0,0 +1,35 @@ +From d8d4c5507bd3784d323e6836ff8456ba9608f115 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Fri, 20 Sep 2024 09:36:41 +0200 +Subject: [PATCH] test_ipahealthcheck: skip connectivity_and_data check + +PKI removed the clones.check connectivity_and_data check in +11.5 and master branches. Skip the test depending on PKI version. +The most recent version on 11.5 is 11.5.4 and still contains the check, +hence skipping if version >= 11.5.5. + +Fixes: https://pagure.io/freeipa/issue/9668 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_ipahealthcheck.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/ipatests/test_integration/test_ipahealthcheck.py b/ipatests/test_integration/test_ipahealthcheck.py +index a3e10b04487f0a12fdde19a8d1971a09f7d79b63..cc51a5a6a62fbc50927fc2fc51f129a069e70b69 100644 +--- a/ipatests/test_integration/test_ipahealthcheck.py ++++ b/ipatests/test_integration/test_ipahealthcheck.py +@@ -1219,6 +1219,9 @@ class TestIpaHealthCheck(IntegrationTest): + This testcase checks that when ClonesConnectivyAndDataCheck + is run it doesn't display source not found error + """ ++ if (tasks.get_pki_version( ++ self.master) >= tasks.parse_version('11.5.5')): ++ raise pytest.skip("PKI dropped ClonesConnectivyAndDataCheck") + error_msg = ( + "Source 'pki.server.healthcheck.clones.connectivity_and_data' " + "not found" +-- +2.47.1 + diff --git a/SOURCES/0039-ipatests-Fixes-for-ipa-ipa-migration-tool.patch b/SOURCES/0039-ipatests-Fixes-for-ipa-ipa-migration-tool.patch new file mode 100644 index 0000000..dffb3c4 --- /dev/null +++ b/SOURCES/0039-ipatests-Fixes-for-ipa-ipa-migration-tool.patch @@ -0,0 +1,36 @@ +From c294b7f6dbcae9c01d5366b19b3b070ffb730fa6 Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Wed, 11 Dec 2024 15:12:48 +0530 +Subject: [PATCH] ipatests: Fixes for ipa-ipa-migration tool + +The test test_ipa_migrate_with_invalid_host has been +failing in downstream run due to mismatch in the expected test output, +hence the assert statement has been modified. + +Related: https://pagure.io/freeipa/issue/3656 + +Signed-off-by: Sudhir Menon +Reviewed-By: Florence Blanc-Renaud +--- + ipatests/test_integration/test_ipa_ipa_migration.py | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py +index 0c637a0141d95f34f951c60a9648adf8e87eaa63..95c29234fc7893d3eae5d900a58aa7b1162ed61d 100644 +--- a/ipatests/test_integration/test_ipa_ipa_migration.py ++++ b/ipatests/test_integration/test_ipa_ipa_migration.py +@@ -437,10 +437,8 @@ class TestIPAMigrateCLIOptions(MigrationTest): + """ + hostname = "server.invalid.host" + ERR_MSG = ( +- "IPA to IPA migration starting ...\n" + "Failed to bind to remote server: cannot connect to " +- "'ldap://" +- "{}': \n".format(hostname) ++ "'ldap://{}':".format(hostname) + ) + result = run_migrate( + self.replicas[0], +-- +2.47.1 + diff --git a/SOURCES/0040-Installation-test-KRA-on-replica-after-cert-renewal.patch b/SOURCES/0040-Installation-test-KRA-on-replica-after-cert-renewal.patch new file mode 100644 index 0000000..ce5085c --- /dev/null +++ b/SOURCES/0040-Installation-test-KRA-on-replica-after-cert-renewal.patch @@ -0,0 +1,47 @@ +From ff5bb485d709ea02422cbafba51d23db384822af Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Fri, 20 Dec 2024 15:45:01 +0100 +Subject: [PATCH] Installation test: KRA on replica after cert renewal + +Add a new test installing the KRA on a replica after the +KRA certs have been renewed on the master. + +Related: https://pagure.io/freeipa/issue/9692 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_installation.py | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_installation.py b/ipatests/test_integration/test_installation.py +index c5565c452010f23f038ddf329454b591ef09f6af..1fae472673a3934c86af2f9adc7b343c70e25b2b 100644 +--- a/ipatests/test_integration/test_installation.py ++++ b/ipatests/test_integration/test_installation.py +@@ -1322,7 +1322,7 @@ class TestInstallMaster(IntegrationTest): + + class TestInstallMasterKRA(IntegrationTest): + +- num_replicas = 0 ++ num_replicas = 1 + + @classmethod + def install(cls, mh): +@@ -1379,6 +1379,14 @@ class TestInstallMasterKRA(IntegrationTest): + ) + assert starting_serial != int(cert.serial_number) + ++ def test_install_replica_after_kracert_renewal(self): ++ """ ++ Test replica installation with CA after the KRA certs renewal ++ """ ++ tasks.install_replica(self.master, self.replicas[0], ++ setup_ca=True) ++ tasks.install_kra(self.replicas[0]) ++ + + class TestInstallMasterDNS(IntegrationTest): + +-- +2.47.1 + diff --git a/SOURCES/0041-KRA-cert-renewal-update-ca.connector.KRA.transportCe.patch b/SOURCES/0041-KRA-cert-renewal-update-ca.connector.KRA.transportCe.patch new file mode 100644 index 0000000..1666f04 --- /dev/null +++ b/SOURCES/0041-KRA-cert-renewal-update-ca.connector.KRA.transportCe.patch @@ -0,0 +1,45 @@ +From a707083b0987e6ffabb817fcc5e5138b4c755459 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Fri, 20 Dec 2024 17:01:56 +0100 +Subject: [PATCH] KRA cert renewal: update ca.connector.KRA.transportCert + +After the KRA transport cert has been renewed, the value +of ca.connector.KRA.transportCert must also be updated in +/etc/pki/pki-tomcat/ca/CS.cfg. +Otherwise replica installation with KRA fails. + +Fixes: https://pagure.io/freeipa/issue/9692 + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/install/cainstance.py | 13 ++++++++----- + 1 file changed, 8 insertions(+), 5 deletions(-) + +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index 5c2c9f8b981cf5d587865f7680e2b231eae655e2..e03a8c863e14782679e19c6887f5e220131e4234 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -1225,11 +1225,14 @@ class CAInstance(DogtagInstance): + """ + + # The cert directive to update per nickname +- directives = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert', +- 'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert', +- 'caSigningCert cert-pki-ca': 'ca.signing.cert', +- 'subsystemCert cert-pki-ca': 'ca.subsystem.cert', +- 'Server-Cert cert-pki-ca': 'ca.sslserver.cert'} ++ directives = { ++ 'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert', ++ 'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert', ++ 'caSigningCert cert-pki-ca': 'ca.signing.cert', ++ 'subsystemCert cert-pki-ca': 'ca.subsystem.cert', ++ 'Server-Cert cert-pki-ca': 'ca.sslserver.cert', ++ 'transportCert cert-pki-kra': 'ca.connector.KRA.transportCert' ++ } + + try: + self.backup_config() +-- +2.47.1 + diff --git a/SOURCES/0042-Add-30-second-timeout-for-certmonger-request-start-t.patch b/SOURCES/0042-Add-30-second-timeout-for-certmonger-request-start-t.patch new file mode 100644 index 0000000..53dfeb2 --- /dev/null +++ b/SOURCES/0042-Add-30-second-timeout-for-certmonger-request-start-t.patch @@ -0,0 +1,45 @@ +From 2506d5de5a9dd8ebe6efc777c2eb76461f5b57e2 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 6 Jan 2025 10:12:15 -0500 +Subject: [PATCH] Add 30-second timeout for certmonger request/start tracking + +certmonger needs to validate that the PIN/password and/or token +are valid and available. In the case of a very slow HSM this can +take longer than the 5-second default timeout. + +We saw an HSM that took 18 seconds to start tracking the CA signing +certificate so default to 30 to be safe. + +Fixes: https://pagure.io/freeipa/issue/9725 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +--- + ipalib/install/certmonger.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ipalib/install/certmonger.py b/ipalib/install/certmonger.py +index 7b22295152f752b6ab4de0f3525d48c541677aff..efc1ba4f42eab98df5fac51bafa3acc83ae91831 100644 +--- a/ipalib/install/certmonger.py ++++ b/ipalib/install/certmonger.py +@@ -477,7 +477,7 @@ def request_cert( + request_parameters['cert-perms'] = perms[0] + request_parameters['key-perms'] = perms[1] + +- result = cm.obj_if.add_request(request_parameters) ++ result = cm.obj_if.add_request(request_parameters, timeout=30) + try: + if result[0]: + request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF, +@@ -581,7 +581,7 @@ def start_tracking( + if nss_user: + params['nss-user'] = nss_user + +- result = cm.obj_if.add_request(params) ++ result = cm.obj_if.add_request(params, timeout=30) + try: + if result[0]: + request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF, +-- +2.47.1 + diff --git a/SOURCES/0011-CVE-2024-11029.patch b/SOURCES/0043-Unify-use-of-option-parsers.patch similarity index 51% rename from SOURCES/0011-CVE-2024-11029.patch rename to SOURCES/0043-Unify-use-of-option-parsers.patch index 105a6ea..3147117 100644 --- a/SOURCES/0011-CVE-2024-11029.patch +++ b/SOURCES/0043-Unify-use-of-option-parsers.patch @@ -1,1124 +1,624 @@ -From 363857b47de1b56e449c9ba635e57ede2b9c22c8 Mon Sep 17 00:00:00 2001 -From: Alexander Bokovoy -Date: Fri, 13 Dec 2024 13:42:36 +0200 -Subject: [PATCH 1/3] Unify use of option parsers - -Do not use direct optparse references, instead import IPAOptionParser - -Signed-off-by: Alexander Bokovoy ---- - install/tools/ipa-adtrust-install.in | 4 +--- - install/tools/ipa-managed-entries.in | 3 ++- - ipaclient/install/ipa_client_automount.py | 4 ++-- - ipaclient/install/ipa_client_samba.py | 4 ++-- - ipalib/cli.py | 21 ++++++++++++--------- - ipalib/plugable.py | 8 ++++---- - ipapython/admintool.py | 3 +-- - ipapython/config.py | 18 +++++++++++------- - ipapython/install/cli.py | 9 ++++----- - ipaserver/install/ipa_acme_manage.py | 6 ++---- - ipaserver/install/ipa_backup.py | 5 ++--- - ipaserver/install/ipa_cacert_manage.py | 9 ++++----- - ipaserver/install/ipa_kra_install.py | 5 ++--- - ipaserver/install/ipa_restore.py | 5 ++--- - ipaserver/install/ipa_server_certinstall.py | 7 +++---- - ipatests/i18n.py | 8 ++++---- - makeapi.in | 5 ++--- - 17 files changed, 60 insertions(+), 64 deletions(-) - -diff --git a/install/tools/ipa-adtrust-install.in b/install/tools/ipa-adtrust-install.in -index cb2b78e50..e7b0e3692 100644 ---- a/install/tools/ipa-adtrust-install.in -+++ b/install/tools/ipa-adtrust-install.in -@@ -29,8 +29,6 @@ import sys - - import six - --from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module -- - from ipalib.install import sysrestore - from ipaserver.install import adtrust, service - from ipaserver.install.installutils import ( -@@ -41,7 +39,7 @@ from ipapython.admintool import ScriptError - from ipapython import version - from ipapython import ipautil - from ipalib import api, errors, krb_utils --from ipapython.config import IPAOptionParser -+from ipapython.config import IPAOptionParser, SUPPRESS_HELP - from ipaplatform.paths import paths - from ipapython.ipa_log_manager import standard_logging_setup - -diff --git a/install/tools/ipa-managed-entries.in b/install/tools/ipa-managed-entries.in -index e9be41b7a..e3f121943 100644 ---- a/install/tools/ipa-managed-entries.in -+++ b/install/tools/ipa-managed-entries.in -@@ -39,7 +39,8 @@ logger = logging.getLogger(os.path.basename(__file__)) - def parse_options(): - usage = "%prog [options] \n" - usage += "%prog [options]\n" -- parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) -+ parser = config.IPAOptionParser(usage=usage, -+ formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information about the update(s)") -diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py -index 4439932bd..9f49ff9ed 100644 ---- a/ipaclient/install/ipa_client_automount.py -+++ b/ipaclient/install/ipa_client_automount.py -@@ -34,7 +34,6 @@ import SSSDConfig - - from six.moves.urllib.parse import urlsplit - --from optparse import OptionParser # pylint: disable=deprecated-module - from ipapython import ipachangeconf - from ipaclient import discovery - from ipaclient.install.client import ( -@@ -52,6 +51,7 @@ from ipaplatform.tasks import tasks - from ipaplatform import services - from ipaplatform.paths import paths - from ipapython.admintool import ScriptError -+from ipapython.config import IPAOptionParser - - - logger = logging.getLogger(os.path.basename(__file__)) -@@ -59,7 +59,7 @@ logger = logging.getLogger(os.path.basename(__file__)) - - def parse_options(): - usage = "%prog [options]\n" -- parser = OptionParser(usage=usage) -+ parser = IPAOptionParser(usage=usage) - parser.add_option("--server", dest="server", help="FQDN of IPA server") - parser.add_option( - "--location", -diff --git a/ipaclient/install/ipa_client_samba.py b/ipaclient/install/ipa_client_samba.py -index 81d670c34..5c33abb4c 100755 ---- a/ipaclient/install/ipa_client_samba.py -+++ b/ipaclient/install/ipa_client_samba.py -@@ -9,7 +9,6 @@ import logging - import os - import gssapi - from urllib.parse import urlsplit --from optparse import OptionParser # pylint: disable=deprecated-module - from contextlib import contextmanager - - from ipaclient import discovery -@@ -31,6 +30,7 @@ from ipaplatform.constants import constants - from ipaplatform import services - from ipapython.admintool import ScriptError - from samba import generate_random_password -+from ipapython.config import IPAOptionParser - - logger = logging.getLogger(os.path.basename(__file__)) - logger.setLevel(logging.DEBUG) -@@ -68,7 +68,7 @@ def use_api_as_principal(principal, keytab): - - def parse_options(): - usage = "%prog [options]\n" -- parser = OptionParser(usage=usage) -+ parser = IPAOptionParser(usage=usage) - parser.add_option( - "--server", - dest="server", -diff --git a/ipalib/cli.py b/ipalib/cli.py -index d9c2ac165..667b213fd 100644 ---- a/ipalib/cli.py -+++ b/ipalib/cli.py -@@ -30,7 +30,6 @@ import textwrap - import sys - import getpass - import code --import optparse # pylint: disable=deprecated-module - import os - import pprint - import fcntl -@@ -71,6 +70,8 @@ from ipalib.text import _ - from ipalib import api - from ipapython.dnsutil import DNSName - from ipapython.admintool import ScriptError -+from ipapython.config import (IPAOptionParser, IPAFormatter, -+ OptionGroup, make_option) - - import datetime - -@@ -1121,7 +1122,8 @@ class Collector: - def __todict__(self): - return dict(self.__options) - --class CLIOptionParserFormatter(optparse.IndentedHelpFormatter): -+ -+class CLIOptionParserFormatter(IPAFormatter): - def format_argument(self, name, help_string): - result = [] - opt_width = self.help_position - self.current_indent - 2 -@@ -1141,7 +1143,8 @@ class CLIOptionParserFormatter(optparse.IndentedHelpFormatter): - result.append("\n") - return "".join(result) - --class CLIOptionParser(optparse.OptionParser): -+ -+class CLIOptionParser(IPAOptionParser): - """ - This OptionParser subclass adds an ability to print positional - arguments in CLI help. Custom formatter is used to format the argument -@@ -1151,13 +1154,13 @@ class CLIOptionParser(optparse.OptionParser): - self._arguments = [] - if 'formatter' not in kwargs: - kwargs['formatter'] = CLIOptionParserFormatter() -- optparse.OptionParser.__init__(self, *args, **kwargs) -+ IPAOptionParser.__init__(self, *args, **kwargs) - - def format_option_help(self, formatter=None): - """ - Prepend argument help to standard OptionParser's option help - """ -- option_help = optparse.OptionParser.format_option_help(self, formatter) -+ option_help = IPAOptionParser.format_option_help(self, formatter) - - if isinstance(formatter, CLIOptionParserFormatter): - heading = unicode(_("Positional arguments")) -@@ -1272,7 +1275,7 @@ class cli(backend.Executioner): - """Get or create an option group for the given name""" - option_group = option_groups.get(group_name) - if option_group is None: -- option_group = optparse.OptionGroup(parser, group_name) -+ option_group = OptionGroup(parser, group_name) - parser.add_option_group(option_group) - option_groups[group_name] = option_group - return option_group -@@ -1298,7 +1301,7 @@ class cli(backend.Executioner): - option_names = ['--%s' % cli_name] - if option.cli_short_name: - option_names.append('-%s' % option.cli_short_name) -- opt = optparse.make_option(*option_names, **kw) -+ opt = make_option(*option_names, **kw) - if option.option_group is None: - parser.add_option(opt) - else: -@@ -1312,7 +1315,7 @@ class cli(backend.Executioner): - group = _get_option_group(unicode(_('Deprecated options'))) - for alias in option.deprecated_cli_aliases: - name = '--%s' % alias -- group.add_option(optparse.make_option(name, **new_kw)) -+ group.add_option(make_option(name, **new_kw)) - - for arg in cmd.args(): - name = self.__get_arg_name(arg, format_name=False) -@@ -1442,7 +1445,7 @@ class cli(backend.Executioner): - ) - - --class IPAHelpFormatter(optparse.IndentedHelpFormatter): -+class IPAHelpFormatter(IPAFormatter): - """Formatter suitable for printing IPA command help - - The default help formatter reflows text to fit the terminal, but it -diff --git a/ipalib/plugable.py b/ipalib/plugable.py -index 2e2861df0..a87e6e891 100644 ---- a/ipalib/plugable.py -+++ b/ipalib/plugable.py -@@ -33,7 +33,6 @@ import sys - import threading - import os - from os import path --import optparse # pylint: disable=deprecated-module - import textwrap - import collections - import importlib -@@ -47,6 +46,7 @@ from ipalib.util import classproperty - from ipalib.base import ReadOnly, lock, islocked - from ipalib.constants import DEFAULT_CONFIG - from ipapython import ipa_log_manager, ipautil -+from ipapython.config import IPAOptionParser, IPAFormatter - from ipapython.ipa_log_manager import ( - LOGGING_FORMAT_FILE, - LOGGING_FORMAT_STDERR) -@@ -526,7 +526,7 @@ class API(ReadOnly): - - def build_global_parser(self, parser=None, context=None): - """ -- Add global options to an optparse.OptionParser instance. -+ Add global options to an IPAOptionParser instance. - """ - def config_file_callback(option, opt, value, parser): - if not os.path.isfile(value): -@@ -536,7 +536,7 @@ class API(ReadOnly): - parser.values.conf = value - - if parser is None: -- parser = optparse.OptionParser( -+ parser = IPAOptionParser( - add_help_option=False, - formatter=IPAHelpFormatter(), - usage='%prog [global-options] COMMAND [command-options]', -@@ -821,7 +821,7 @@ class API(ReadOnly): - return self.__next[plugin] - - --class IPAHelpFormatter(optparse.IndentedHelpFormatter): -+class IPAHelpFormatter(IPAFormatter): - def format_epilog(self, epilog): - text_width = self.width - self.current_indent - indent = " " * self.current_indent -diff --git a/ipapython/admintool.py b/ipapython/admintool.py -index fdb4400d8..dff9112eb 100644 ---- a/ipapython/admintool.py -+++ b/ipapython/admintool.py -@@ -26,7 +26,6 @@ import logging - import sys - import os - import traceback --from optparse import OptionGroup # pylint: disable=deprecated-module - - from ipaplatform.osinfo import osinfo - from ipapython import version -@@ -113,7 +112,7 @@ class AdminTool: - :param parser: The parser to add options to - :param debug_option: Add a --debug option as an alias to --verbose - """ -- group = OptionGroup(parser, "Logging and output options") -+ group = config.OptionGroup(parser, "Logging and output options") - group.add_option("-v", "--verbose", dest="verbose", default=False, - action="store_true", help="print debugging information") - if debug_option: -diff --git a/ipapython/config.py b/ipapython/config.py -index f53d0f998..7af4dfdeb 100644 ---- a/ipapython/config.py -+++ b/ipapython/config.py -@@ -18,9 +18,9 @@ - # - from __future__ import absolute_import - --# pylint: disable=deprecated-module --from optparse import ( -- Option, Values, OptionParser, IndentedHelpFormatter, OptionValueError) -+# pylint: disable=deprecated-module, disable=unused-import -+from optparse import (Option, Values, OptionGroup, OptionParser, SUPPRESS_HELP, -+ IndentedHelpFormatter, OptionValueError, make_option) - # pylint: enable=deprecated-module - from copy import copy - from configparser import ConfigParser as SafeConfigParser -@@ -113,10 +113,14 @@ class IPAOptionParser(OptionParser): - description=None, - formatter=None, - add_help_option=True, -- prog=None): -- OptionParser.__init__(self, usage, option_list, option_class, -- version, conflict_handler, description, -- formatter, add_help_option, prog) -+ prog=None, -+ epilog=None): -+ OptionParser.__init__(self, usage=usage, option_list=option_list, -+ option_class=option_class, version=version, -+ conflict_handler=conflict_handler, -+ description=description, formatter=formatter, -+ add_help_option=add_help_option, prog=prog, -+ epilog=epilog) - - def get_safe_opts(self, opts): - """ -diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py -index ab212be4e..a048b3c7c 100644 ---- a/ipapython/install/cli.py -+++ b/ipapython/install/cli.py -@@ -9,12 +9,11 @@ Command line support. - import collections - import enum - import logging --import optparse # pylint: disable=deprecated-module - import signal - - import six - --from ipapython import admintool -+from ipapython import admintool, config - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.ipautil import (CheckedIPAddress, CheckedIPAddressLoopback, - private_ccache) -@@ -158,7 +157,7 @@ class ConfigureTool(admintool.AdminTool): - try: - opt_group = groups[group_cls] - except KeyError: -- opt_group = groups[group_cls] = optparse.OptionGroup( -+ opt_group = groups[group_cls] = config.OptionGroup( - parser, "{0} options".format(group_cls.description)) - parser.add_option_group(opt_group) - -@@ -232,7 +231,7 @@ class ConfigureTool(admintool.AdminTool): - if not hidden: - help = knob_cls.description - else: -- help = optparse.SUPPRESS_HELP -+ help = config.SUPPRESS_HELP - - opt_group.add_option( - *opt_strs, -@@ -256,7 +255,7 @@ class ConfigureTool(admintool.AdminTool): - - # fake option parser to parse positional arguments - # (because optparse does not support positional argument parsing) -- fake_option_parser = optparse.OptionParser() -+ fake_option_parser = config.IPAOptionParser() - self.add_options(fake_option_parser, True) - - fake_option_map = {option.dest: option -diff --git a/ipaserver/install/ipa_acme_manage.py b/ipaserver/install/ipa_acme_manage.py -index dc2359f49..0decab394 100644 ---- a/ipaserver/install/ipa_acme_manage.py -+++ b/ipaserver/install/ipa_acme_manage.py -@@ -7,14 +7,12 @@ import enum - import pki.util - import logging - --from optparse import OptionGroup # pylint: disable=deprecated-module -- - from ipalib import api, errors, x509 - from ipalib import _ - from ipalib.facts import is_ipa_configured - from ipaplatform.paths import paths - from ipapython.admintool import AdminTool --from ipapython import cookie, dogtag -+from ipapython import cookie, dogtag, config - from ipapython.ipautil import run - from ipapython.certdb import NSSDatabase, EXTERNAL_CA_TRUST_FLAGS - from ipaserver.install import cainstance -@@ -143,7 +141,7 @@ class IPAACMEManage(AdminTool): - @classmethod - def add_options(cls, parser): - -- group = OptionGroup(parser, 'Pruning') -+ group = config.OptionGroup(parser, 'Pruning') - group.add_option( - "--enable", dest="enable", action="store_true", - default=False, help="Enable certificate pruning") -diff --git a/ipaserver/install/ipa_backup.py b/ipaserver/install/ipa_backup.py -index 982e5dfc4..b6af63813 100644 ---- a/ipaserver/install/ipa_backup.py -+++ b/ipaserver/install/ipa_backup.py -@@ -20,7 +20,6 @@ - from __future__ import absolute_import, print_function - - import logging --import optparse # pylint: disable=deprecated-module - import os - import shutil - import sys -@@ -32,7 +31,7 @@ import six - from ipaplatform.paths import paths - from ipaplatform import services - from ipalib import api, errors --from ipapython import version -+from ipapython import version, config - from ipapython.ipautil import run, write_tmp_file - from ipapython import admintool, certdb - from ipapython.dn import DN -@@ -245,7 +244,7 @@ class Backup(admintool.AdminTool): - - parser.add_option( - "--gpg-keyring", dest="gpg_keyring", -- help=optparse.SUPPRESS_HELP) -+ help=config.SUPPRESS_HELP) - parser.add_option( - "--gpg", dest="gpg", action="store_true", - default=False, help="Encrypt the backup") -diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py -index f6ab736fa..048245237 100644 ---- a/ipaserver/install/ipa_cacert_manage.py -+++ b/ipaserver/install/ipa_cacert_manage.py -@@ -22,14 +22,13 @@ from __future__ import print_function, absolute_import - import datetime - import logging - import os --from optparse import OptionGroup # pylint: disable=deprecated-module - import gssapi - - from ipalib.constants import ( - RENEWAL_CA_NAME, RENEWAL_REUSE_CA_NAME, RENEWAL_SELFSIGNED_CA_NAME, - IPA_CA_CN) - from ipalib.install import certmonger, certstore --from ipapython import admintool, ipautil -+from ipapython import admintool, ipautil, config - from ipapython.certdb import (EMPTY_TRUST_FLAGS, - EXTERNAL_CA_TRUST_FLAGS, - TrustFlags, -@@ -61,7 +60,7 @@ class CACertManage(admintool.AdminTool): - "-p", "--password", dest='password', - help="Directory Manager password") - -- renew_group = OptionGroup(parser, "Renew options") -+ renew_group = config.OptionGroup(parser, "Renew options") - renew_group.add_option( - "--self-signed", dest='self_signed', - action='store_true', -@@ -89,7 +88,7 @@ class CACertManage(admintool.AdminTool): - "certificate chain") - parser.add_option_group(renew_group) - -- install_group = OptionGroup(parser, "Install options") -+ install_group = config.OptionGroup(parser, "Install options") - install_group.add_option( - "-n", "--nickname", dest='nickname', - help="Nickname for the certificate") -@@ -98,7 +97,7 @@ class CACertManage(admintool.AdminTool): - help="Trust flags for the certificate in certutil format") - parser.add_option_group(install_group) - -- delete_group = OptionGroup(parser, "Delete options") -+ delete_group = config.OptionGroup(parser, "Delete options") - delete_group.add_option( - "-f", "--force", action='store_true', - help="Force removing the CA even if chain validation fails") -diff --git a/ipaserver/install/ipa_kra_install.py b/ipaserver/install/ipa_kra_install.py -index 3e4cd67fa..8a09179f7 100644 ---- a/ipaserver/install/ipa_kra_install.py -+++ b/ipaserver/install/ipa_kra_install.py -@@ -22,13 +22,12 @@ from __future__ import print_function, absolute_import - import logging - import sys - import tempfile --from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module - - from textwrap import dedent - from ipalib import api - from ipalib.constants import DOMAIN_LEVEL_1 - from ipaplatform.paths import paths --from ipapython import admintool -+from ipapython import admintool, config - from ipaserver.install import service - from ipaserver.install import cainstance - from ipaserver.install import custodiainstance -@@ -73,7 +72,7 @@ class KRAInstall(admintool.AdminTool): - parser.add_option( - "--uninstall", - dest="uninstall", action="store_true", default=False, -- help=SUPPRESS_HELP) -+ help=config.SUPPRESS_HELP) - - parser.add_option( - "--pki-config-override", dest="pki_config_override", -diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py -index 57ad8dd05..8d75a0e6b 100644 ---- a/ipaserver/install/ipa_restore.py -+++ b/ipaserver/install/ipa_restore.py -@@ -20,7 +20,6 @@ - from __future__ import absolute_import, print_function - - import logging --import optparse # pylint: disable=deprecated-module - import os - import shutil - import sys -@@ -34,7 +33,7 @@ import six - from ipaclient.install.client import update_ipa_nssdb - from ipalib import api, errors - from ipalib.constants import FQDN --from ipapython import version, ipautil -+from ipapython import version, ipautil, config - from ipapython.ipautil import run, user_input - from ipapython import admintool, certdb - from ipapython.dn import DN -@@ -190,7 +189,7 @@ class Restore(admintool.AdminTool): - help="Directory Manager password") - parser.add_option( - "--gpg-keyring", dest="gpg_keyring", -- help=optparse.SUPPRESS_HELP) -+ help=config.SUPPRESS_HELP) - parser.add_option( - "--data", dest="data_only", action="store_true", - default=False, help="Restore only the data") -diff --git a/ipaserver/install/ipa_server_certinstall.py b/ipaserver/install/ipa_server_certinstall.py -index e29f00ec3..e9f680b1d 100644 ---- a/ipaserver/install/ipa_server_certinstall.py -+++ b/ipaserver/install/ipa_server_certinstall.py -@@ -22,12 +22,11 @@ from __future__ import print_function, absolute_import - import os - import os.path - import tempfile --import optparse # pylint: disable=deprecated-module - - from ipalib import x509 - from ipalib.install import certmonger - from ipaplatform.paths import paths --from ipapython import admintool, dogtag -+from ipapython import admintool, dogtag, config - from ipapython.certdb import NSSDatabase, get_ca_nickname - from ipapython.dn import DN - from ipapython import ipaldap -@@ -65,8 +64,8 @@ class ServerCertInstall(admintool.AdminTool): - help="The password of the PKCS#12 file") - parser.add_option( - "--dirsrv_pin", "--http_pin", -- dest="pin", -- help=optparse.SUPPRESS_HELP) -+ dest="pin", sensitive=True, -+ help=config.SUPPRESS_HELP) - parser.add_option( - "--cert-name", - dest="cert_name", metavar="NAME", -diff --git a/ipatests/i18n.py b/ipatests/i18n.py -index 49f5c4c32..57915c286 100644 ---- a/ipatests/i18n.py -+++ b/ipatests/i18n.py -@@ -22,7 +22,6 @@ from __future__ import print_function - - # WARNING: Do not import ipa modules, this is also used as a - # stand-alone script (invoked from po Makefile). --import optparse # pylint: disable=deprecated-module - import sys - import gettext - import re -@@ -30,6 +29,7 @@ import os - import traceback - import polib - from collections import namedtuple -+from ipapython import config - - import six - -@@ -722,9 +722,9 @@ usage =''' - def main(): - global verbose, print_traceback, pedantic, show_strings - -- parser = optparse.OptionParser(usage=usage) -+ parser = config.IPAOptionParser(usage=usage) - -- mode_group = optparse.OptionGroup(parser, 'Operational Mode', -+ mode_group = config.OptionGroup(parser, 'Operational Mode', - 'You must select one these modes to run in') - - mode_group.add_option('-g', '--test-gettext', action='store_const', const='test_gettext', dest='mode', -@@ -748,7 +748,7 @@ def main(): - parser.add_option('--traceback', action='store_true', dest='print_traceback', default=False, - help='print the traceback when an exception occurs') - -- param_group = optparse.OptionGroup(parser, 'Run Time Parameters', -+ param_group = config.OptionGroup(parser, 'Run Time Parameters', - 'These may be used to modify the run time defaults') - - param_group.add_option('--test-lang', action='store', dest='test_lang', default='test', -diff --git a/makeapi.in b/makeapi.in -index a801b9253..8fc87d23d 100644 ---- a/makeapi.in -+++ b/makeapi.in -@@ -38,6 +38,7 @@ from ipalib.parameters import Param - from ipalib.output import Output - from ipalib.text import Gettext, NGettext, ConcatenatedLazyText - from ipalib.capabilities import capabilities -+from ipapython import config - - API_FILE='API.txt' - -@@ -84,9 +85,7 @@ OUTPUT_IGNORED_ATTRIBUTES = ( - ) - - def parse_options(): -- from optparse import OptionParser # pylint: disable=deprecated-module -- -- parser = OptionParser() -+ parser = config.IPAOptionParser() - parser.add_option("--validate", dest="validate", action="store_true", - default=False, help="Validate the API vs the stored API") - --- -2.47.1 - - -From 6c9186404e683be27289a86982fe1fcabe9ebef3 Mon Sep 17 00:00:00 2001 -From: Alexander Bokovoy -Date: Fri, 8 Nov 2024 14:59:20 +0200 -Subject: [PATCH 2/3] ipa tools: remove sensitive material from the commandline - -When command line tools accept passwords, remove them from the command -line so that they don't get visible in '/proc/pid/commandline'. - -There is no common method to access the original ARGV vector and modify -it from Python. Since this mostly affects Linux systems where IPA -services run, we expect use of GNU libc and thus can rely on internal -glibc symbols. If they aren't available, the code will skip removing -passwords. - -Fixes: CVE-2024-11029 - -Signed-off-by: Alexander Bokovoy ---- - .../com.redhat.idm.trust-fetch-domains.in | 5 ++- - install/tools/ipa-adtrust-install.in | 3 +- - install/tools/ipa-ca-install.in | 2 + - install/tools/ipa-compat-manage.in | 6 ++- - install/tools/ipa-csreplica-manage.in | 12 +++--- - install/tools/ipa-managed-entries.in | 5 ++- - install/tools/ipa-replica-conncheck.in | 7 ++-- - install/tools/ipa-replica-manage.in | 10 +++-- - ipapython/admintool.py | 40 +++++++++++++++++++ - ipaserver/install/ipa_migrate.py | 17 +++++++- - ipaserver/install/ipa_restore.py | 2 +- - ipaserver/install/ipa_server_certinstall.py | 2 +- - 12 files changed, 90 insertions(+), 21 deletions(-) - -diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in -index 45c1f1463..b86be0212 100644 ---- a/install/oddjob/com.redhat.idm.trust-fetch-domains.in -+++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in -@@ -15,6 +15,7 @@ import six - import gssapi - - from ipalib.install.kinit import kinit_keytab, kinit_password -+from ipapython.admintool import admin_cleanup_global_argv - - if six.PY3: - unicode = str -@@ -52,11 +53,13 @@ def parse_options(): - "--password", - action="store", - dest="password", -- help="Display debugging information", -+ help="Password for Active Directory administrator", -+ sensitive=True - ) - - options, args = parser.parse_args() - safe_options = parser.get_safe_opts(options) -+ admin_cleanup_global_argv(parser, options, sys.argv) - - # We only use first argument of the passed args but as D-BUS interface - # in oddjobd cannot expose optional, we fill in empty slots from IPA side -diff --git a/install/tools/ipa-adtrust-install.in b/install/tools/ipa-adtrust-install.in -index e7b0e3692..1efccdb67 100644 ---- a/install/tools/ipa-adtrust-install.in -+++ b/install/tools/ipa-adtrust-install.in -@@ -35,7 +35,7 @@ from ipaserver.install.installutils import ( - read_password, - check_server_configuration, - run_script) --from ipapython.admintool import ScriptError -+from ipapython.admintool import ScriptError, admin_cleanup_global_argv - from ipapython import version - from ipapython import ipautil - from ipalib import api, errors, krb_utils -@@ -93,6 +93,7 @@ def parse_options(): - - options, _args = parser.parse_args() - safe_options = parser.get_safe_opts(options) -+ admin_cleanup_global_argv(parser, options, sys.argv) - - return safe_options, options - -diff --git a/install/tools/ipa-ca-install.in b/install/tools/ipa-ca-install.in -index 3c27a6b27..319b75a56 100644 ---- a/install/tools/ipa-ca-install.in -+++ b/install/tools/ipa-ca-install.in -@@ -42,6 +42,7 @@ from ipalib.constants import DOMAIN_LEVEL_1 - from ipapython.config import IPAOptionParser - from ipapython.ipa_log_manager import standard_logging_setup - from ipaplatform.paths import paths -+from ipapython.admintool import admin_cleanup_global_argv - - logger = logging.getLogger(os.path.basename(__file__)) - -@@ -132,6 +133,7 @@ def parse_options(): - - options, args = parser.parse_args() - safe_options = parser.get_safe_opts(options) -+ admin_cleanup_global_argv(parser, options, sys.argv) - - if args: - parser.error("Too many arguments provided") -diff --git a/install/tools/ipa-compat-manage.in b/install/tools/ipa-compat-manage.in -index 70dd7c451..fb25c22ed 100644 ---- a/install/tools/ipa-compat-manage.in -+++ b/install/tools/ipa-compat-manage.in -@@ -24,7 +24,6 @@ from __future__ import print_function - import sys - from ipaplatform.paths import paths - try: -- from optparse import OptionParser # pylint: disable=deprecated-module - from ipapython import ipautil, config - from ipaserver.install import installutils - from ipaserver.install.ldapupdate import LDAPUpdate -@@ -32,6 +31,7 @@ try: - from ipalib import api, errors - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.dn import DN -+ from ipapython.admintool import admin_cleanup_global_argv - except ImportError as e: - print("""\ - There was a problem importing one of the required Python modules. The -@@ -47,7 +47,8 @@ nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) - def parse_options(): - usage = "%prog [options] \n" - usage += "%prog [options]\n" -- parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) -+ parser = config.IPAOptionParser(usage=usage, -+ formatter=config.IPAFormatter()) - - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help="Display debugging information about the update(s)") -@@ -56,6 +57,7 @@ def parse_options(): - - config.add_standard_options(parser) - options, args = parser.parse_args() -+ admin_cleanup_global_argv(parser, options, sys.argv) - - return options, args - -diff --git a/install/tools/ipa-csreplica-manage.in b/install/tools/ipa-csreplica-manage.in -index 6f248cc50..2fab27a94 100644 ---- a/install/tools/ipa-csreplica-manage.in -+++ b/install/tools/ipa-csreplica-manage.in -@@ -32,8 +32,8 @@ from ipaserver.install import (replication, installutils, bindinstance, - from ipalib import api, errors - from ipalib.constants import FQDN - from ipalib.util import has_managed_topology, print_replication_status --from ipapython import ipautil, ipaldap, version --from ipapython.admintool import ScriptError -+from ipapython import ipautil, ipaldap, version, config -+from ipapython.admintool import admin_cleanup_global_argv, ScriptError - from ipapython.dn import DN - - logger = logging.getLogger(os.path.basename(__file__)) -@@ -54,11 +54,10 @@ commands = { - - - def parse_options(): -- from optparse import OptionParser # pylint: disable=deprecated-module -- -- parser = OptionParser(version=version.VERSION) -+ parser = config.IPAOptionParser(version=version.VERSION) - parser.add_option("-H", "--host", dest="host", help="starting host") -- parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") -+ parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True, -+ help="Directory Manager password") - parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, - help="provide additional information") - parser.add_option("-f", "--force", dest="force", action="store_true", default=False, -@@ -66,6 +65,7 @@ def parse_options(): - parser.add_option("--from", dest="fromhost", help="Host to get data from") - - options, args = parser.parse_args() -+ admin_cleanup_global_argv(parser, options, sys.argv) - - valid_syntax = False - -diff --git a/install/tools/ipa-managed-entries.in b/install/tools/ipa-managed-entries.in -index e3f121943..ff2fd6a58 100644 ---- a/install/tools/ipa-managed-entries.in -+++ b/install/tools/ipa-managed-entries.in -@@ -24,7 +24,6 @@ import logging - import os - import re - import sys --from optparse import OptionParser # pylint: disable=deprecated-module - - from ipaplatform.paths import paths - from ipapython import config -@@ -32,6 +31,7 @@ from ipaserver.install import installutils - from ipalib import api, errors - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.dn import DN -+from ipapython.admintool import admin_cleanup_global_argv - - logger = logging.getLogger(os.path.basename(__file__)) - -@@ -51,9 +51,10 @@ def parse_options(): - action="store_true", - help="List available Managed Entries") - parser.add_option("-p", "--password", dest="dirman_password", -- help="Directory Manager password") -+ sensitive=True, help="Directory Manager password") - - options, args = parser.parse_args() -+ admin_cleanup_global_argv(parser, options, sys.argv) - - return options, args - -diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in -index 8eee82483..81b7d13ac 100644 ---- a/install/tools/ipa-replica-conncheck.in -+++ b/install/tools/ipa-replica-conncheck.in -@@ -23,15 +23,15 @@ from __future__ import print_function - import logging - - from ipapython import ipachangeconf --from ipapython.config import IPAOptionParser -+from ipapython.config import (IPAOptionParser, OptionGroup, -+ OptionValueError) -+from ipapython.admintool import admin_cleanup_global_argv - from ipapython.dn import DN - from ipapython import version - from ipapython import ipautil, certdb - from ipalib import api, errors, x509 - from ipalib.constants import FQDN - from ipaserver.install import installutils --# pylint: disable=deprecated-module --from optparse import OptionGroup, OptionValueError - # pylint: enable=deprecated-module - from ipapython.ipa_log_manager import standard_logging_setup - import copy -@@ -189,6 +189,7 @@ def parse_options(): - - options, _args = parser.parse_args() - safe_options = parser.get_safe_opts(options) -+ admin_cleanup_global_argv(parser, options, sys.argv) - - if options.master and options.replica: - parser.error("on-master and on-replica options are mutually exclusive!") -diff --git a/install/tools/ipa-replica-manage.in b/install/tools/ipa-replica-manage.in -index d6e6ef57c..7e5b31a59 100644 ---- a/install/tools/ipa-replica-manage.in -+++ b/install/tools/ipa-replica-manage.in -@@ -43,6 +43,7 @@ from ipalib.util import ( - print_replication_status, - verify_host_resolvable, - ) -+from ipapython.admintool import admin_cleanup_global_argv - from ipapython.ipa_log_manager import standard_logging_setup - from ipapython.dn import DN - from ipapython.config import IPAOptionParser -@@ -84,7 +85,8 @@ class NoRUVsFound(Exception): - def parse_options(): - parser = IPAOptionParser(version=version.VERSION) - parser.add_option("-H", "--host", dest="host", help="starting host") -- parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") -+ parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True, -+ help="Directory Manager password") - parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, - help="provide additional information") - parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, -@@ -95,7 +97,7 @@ def parse_options(): - help="DANGER: clean up references to a ghost master") - parser.add_option("--binddn", dest="binddn", default=None, type="dn", - help="Bind DN to use with remote server") -- parser.add_option("--bindpw", dest="bindpw", default=None, -+ parser.add_option("--bindpw", dest="bindpw", default=None, sensitive=True, - help="Password for Bind DN to use with remote server") - parser.add_option("--winsync", dest="winsync", action="store_true", default=False, - help="This is a Windows Sync Agreement") -@@ -103,13 +105,15 @@ def parse_options(): - help="Full path and filename of CA certificate to use with TLS/SSL to the remote server") - parser.add_option("--win-subtree", dest="win_subtree", default=None, - help="DN of Windows subtree containing the users you want to sync (default cn=Users, ...', add two args -+ _argc = len(argv) + 2 -+ all_options = [] -+ if '_get_all_options' in dir(option_parser): -+ # OptParse parser -+ all_options = option_parser._get_all_options() -+ elif '_actions' in dir(option_parser): -+ # ArgParse parser -+ all_options = option_parser._actions -+ -+ for opt in all_options: -+ if getattr(opt, 'sensitive', False): -+ v = getattr(options, opt.dest) -+ for i in range(0, _argc): -+ vi = ctypes.cast(_argv[i], -+ ctypes.c_char_p -+ ).value.decode('utf-8') -+ if vi == v: -+ ctypes.memset(_argv[i], ord('X'), len(v)) -+ except Exception: -+ pass -+ -+ - class ScriptError(Exception): - """An exception that records an error message and a return value - """ -@@ -148,6 +187,7 @@ class AdminTool: - cls._option_parsers[cls] = cls.option_parser - - options, args = cls.option_parser.parse_args(argv[1:]) -+ admin_cleanup_global_argv(cls.option_parser, options, argv) - - command_class = cls.get_command_class(options, args) - command = command_class(options, args) -diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py -index 17cc859db..ffee6b11c 100644 ---- a/ipaserver/install/ipa_migrate.py -+++ b/ipaserver/install/ipa_migrate.py -@@ -28,6 +28,7 @@ from ipaplatform.paths import paths - from ipapython.dn import DN - from ipapython.ipaldap import LDAPClient, LDAPEntry, realm_to_ldapi_uri - from ipapython.ipa_log_manager import standard_logging_setup -+from ipapython.admintool import admin_cleanup_global_argv - from ipaserver.install.ipa_migrate_constants import ( - DS_CONFIG, DB_OBJECTS, DS_INDEXES, BIND_DN, LOG_FILE_NAME, - STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS, -@@ -284,6 +285,18 @@ class LDIFParser(ldif.LDIFParser): - self.mc.process_db_entry(entry_dn=dn, entry_attrs=entry_attrs) - - -+class SensitiveStoreAction(argparse._StoreAction): -+ def __init__(self, *, sensitive, **options): -+ super(SensitiveStoreAction, self).__init__(**options) -+ self.sensitive = sensitive -+ -+ def _get_kwargs(self): -+ names = super(SensitiveStoreAction, self)._get_kwargs() -+ sensitive_name = 'sensitive' -+ names.extend((sensitive_name, getattr(self, sensitive_name))) -+ return names -+ -+ - # - # Migrate IPA to IPA Class - # -@@ -344,7 +357,8 @@ class IPAMigrate(): - help='Password for the Bind DN. If a password ' - 'is not provided then the user will be ' - 'prompted to enter it', -- default=None) -+ default=None, sensitive=True, -+ action=SensitiveStoreAction) - parser.add_argument('-j', '--bind-pw-file', - help='A text file containing the clear text ' - 'password for the Bind DN', default=None) -@@ -2105,6 +2119,7 @@ class IPAMigrate(): - parser = argparse.ArgumentParser(description=desc, allow_abbrev=True) - self.add_options(parser) - self.validate_options() -+ admin_cleanup_global_argv(parser, self.args, sys.argv) - - # Check for dryrun mode - if self.args.dryrun or self.args.dryrun_record is not None: -diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py -index 8d75a0e6b..539501ab4 100644 ---- a/ipaserver/install/ipa_restore.py -+++ b/ipaserver/install/ipa_restore.py -@@ -185,7 +185,7 @@ class Restore(admintool.AdminTool): - super(Restore, cls).add_options(parser, debug_option=True) - - parser.add_option( -- "-p", "--password", dest="password", -+ "-p", "--password", dest="password", sensitive=True, - help="Directory Manager password") - parser.add_option( - "--gpg-keyring", dest="gpg_keyring", -diff --git a/ipaserver/install/ipa_server_certinstall.py b/ipaserver/install/ipa_server_certinstall.py -index e9f680b1d..76ad37ca7 100644 ---- a/ipaserver/install/ipa_server_certinstall.py -+++ b/ipaserver/install/ipa_server_certinstall.py -@@ -72,7 +72,7 @@ class ServerCertInstall(admintool.AdminTool): - help="Name of the certificate to install") - parser.add_option( - "-p", "--dirman-password", -- dest="dirman_password", -+ dest="dirman_password", sensitive=True, - help="Directory Manager password") - - def validate_options(self): --- -2.47.1 - - -From d7f750fdc1ad28967b2a0df89d4ebf24f63a03f2 Mon Sep 17 00:00:00 2001 -From: Sumit Bose -Date: Wed, 27 Nov 2024 12:16:09 +0100 -Subject: [PATCH 3/3] ipa-otpd: use oidc_child's --client-secret-stdin option - -To remove the client secret from the command line where it would be -visible e.g. when calling ps it is now passed via stdin to oidc_child. - -Fixes: CVE-2024-11029 - -Signed-off-by: Sumit Bose ---- - daemons/ipa-otpd/oauth2.c | 24 ++++++++++++++---------- - 1 file changed, 14 insertions(+), 10 deletions(-) - -diff --git a/daemons/ipa-otpd/oauth2.c b/daemons/ipa-otpd/oauth2.c -index a33cf5171..52d7d7c9c 100644 ---- a/daemons/ipa-otpd/oauth2.c -+++ b/daemons/ipa-otpd/oauth2.c -@@ -31,6 +31,7 @@ - #include - #include - #include -+#include - - #include "internal.h" - -@@ -93,6 +94,7 @@ static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev) - (void)vctx; /* Unused */ - ssize_t io; - struct child_ctx *child_ctx; -+ struct iovec iov[3]; - - child_ctx = verto_get_private(ev); - if (child_ctx == NULL) { -@@ -102,15 +104,18 @@ static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev) - } - - if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) { -- /* no input needed */ -- verto_del(ev); -- return; -- } -- -+ io = write(verto_get_fd(ev), child_ctx->item->idp.ipaidpClientSecret, -+ strlen(child_ctx->item->idp.ipaidpClientSecret)); -+ } else { -+ iov[0].iov_base = child_ctx->item->idp.ipaidpClientSecret; -+ iov[0].iov_len = strlen(child_ctx->item->idp.ipaidpClientSecret); -+ iov[1].iov_base = "\n"; -+ iov[1].iov_len = 1; -+ iov[2].iov_base = child_ctx->saved_item->oauth2.device_code_reply; -+ iov[2].iov_len = strlen(child_ctx->saved_item->oauth2.device_code_reply); - -- io = write(verto_get_fd(ev), -- child_ctx->saved_item->oauth2.device_code_reply, -- strlen(child_ctx->saved_item->oauth2.device_code_reply)); -+ io = writev(verto_get_fd(ev), iov, 3); -+ } - otpd_queue_item_free(child_ctx->saved_item); - - if (io < 0) { -@@ -429,8 +434,7 @@ int oauth2(struct otpd_queue_item **item, enum oauth2_state oauth2_state) - args[args_idx++] = (*item)->idp.ipaidpClientID; - - if ((*item)->idp.ipaidpClientSecret) { -- args[args_idx++] = "--client-secret"; -- args[args_idx++] = (*item)->idp.ipaidpClientSecret; -+ args[args_idx++] = "--client-secret-stdin"; - } - - if ((*item)->idp.ipaidpScope) { --- -2.47.1 - +From d26ce5cccc211f83b3cce3fc5e548b5cb955bb81 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Fri, 13 Dec 2024 13:42:36 +0200 +Subject: [PATCH] Unify use of option parsers + +Do not use direct optparse references, instead import IPAOptionParser + +Signed-off-by: Alexander Bokovoy +--- + install/tools/ipa-adtrust-install.in | 4 +--- + install/tools/ipa-managed-entries.in | 3 ++- + ipaclient/install/ipa_client_automount.py | 4 ++-- + ipaclient/install/ipa_client_samba.py | 4 ++-- + ipalib/cli.py | 21 ++++++++++++--------- + ipalib/plugable.py | 8 ++++---- + ipapython/admintool.py | 3 +-- + ipapython/config.py | 18 +++++++++++------- + ipapython/install/cli.py | 9 ++++----- + ipaserver/install/ipa_acme_manage.py | 6 ++---- + ipaserver/install/ipa_backup.py | 5 ++--- + ipaserver/install/ipa_cacert_manage.py | 9 ++++----- + ipaserver/install/ipa_kra_install.py | 5 ++--- + ipaserver/install/ipa_restore.py | 5 ++--- + ipaserver/install/ipa_server_certinstall.py | 7 +++---- + ipatests/i18n.py | 8 ++++---- + makeapi.in | 5 ++--- + 17 files changed, 60 insertions(+), 64 deletions(-) + +diff --git a/install/tools/ipa-adtrust-install.in b/install/tools/ipa-adtrust-install.in +index cb2b78e504896b96cdc378fd879cc1f00c60904f..e7b0e369259da5d28d703558d9293ccfaf68f3ed 100644 +--- a/install/tools/ipa-adtrust-install.in ++++ b/install/tools/ipa-adtrust-install.in +@@ -29,8 +29,6 @@ import sys + + import six + +-from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module +- + from ipalib.install import sysrestore + from ipaserver.install import adtrust, service + from ipaserver.install.installutils import ( +@@ -41,7 +39,7 @@ from ipapython.admintool import ScriptError + from ipapython import version + from ipapython import ipautil + from ipalib import api, errors, krb_utils +-from ipapython.config import IPAOptionParser ++from ipapython.config import IPAOptionParser, SUPPRESS_HELP + from ipaplatform.paths import paths + from ipapython.ipa_log_manager import standard_logging_setup + +diff --git a/install/tools/ipa-managed-entries.in b/install/tools/ipa-managed-entries.in +index e9be41b7a34ed62e65ab40bf544a6e4cea7c598a..e3f121943eb3b18ca8f7f8bfeae7813cbc9bd753 100644 +--- a/install/tools/ipa-managed-entries.in ++++ b/install/tools/ipa-managed-entries.in +@@ -39,7 +39,8 @@ logger = logging.getLogger(os.path.basename(__file__)) + def parse_options(): + usage = "%prog [options] \n" + usage += "%prog [options]\n" +- parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) ++ parser = config.IPAOptionParser(usage=usage, ++ formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information about the update(s)") +diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py +index 4439932bd723c40c429dac2c08e97d326e414d24..9f49ff9edeee2648d2be1dea6b09806ba0b5e041 100644 +--- a/ipaclient/install/ipa_client_automount.py ++++ b/ipaclient/install/ipa_client_automount.py +@@ -34,7 +34,6 @@ import SSSDConfig + + from six.moves.urllib.parse import urlsplit + +-from optparse import OptionParser # pylint: disable=deprecated-module + from ipapython import ipachangeconf + from ipaclient import discovery + from ipaclient.install.client import ( +@@ -52,6 +51,7 @@ from ipaplatform.tasks import tasks + from ipaplatform import services + from ipaplatform.paths import paths + from ipapython.admintool import ScriptError ++from ipapython.config import IPAOptionParser + + + logger = logging.getLogger(os.path.basename(__file__)) +@@ -59,7 +59,7 @@ logger = logging.getLogger(os.path.basename(__file__)) + + def parse_options(): + usage = "%prog [options]\n" +- parser = OptionParser(usage=usage) ++ parser = IPAOptionParser(usage=usage) + parser.add_option("--server", dest="server", help="FQDN of IPA server") + parser.add_option( + "--location", +diff --git a/ipaclient/install/ipa_client_samba.py b/ipaclient/install/ipa_client_samba.py +index 81d670c34cae0f91a436a2a2d1e0d525057b6cc7..5c33abb4cabe47ff311b7d769fefa37991f7d453 100755 +--- a/ipaclient/install/ipa_client_samba.py ++++ b/ipaclient/install/ipa_client_samba.py +@@ -9,7 +9,6 @@ import logging + import os + import gssapi + from urllib.parse import urlsplit +-from optparse import OptionParser # pylint: disable=deprecated-module + from contextlib import contextmanager + + from ipaclient import discovery +@@ -31,6 +30,7 @@ from ipaplatform.constants import constants + from ipaplatform import services + from ipapython.admintool import ScriptError + from samba import generate_random_password ++from ipapython.config import IPAOptionParser + + logger = logging.getLogger(os.path.basename(__file__)) + logger.setLevel(logging.DEBUG) +@@ -68,7 +68,7 @@ def use_api_as_principal(principal, keytab): + + def parse_options(): + usage = "%prog [options]\n" +- parser = OptionParser(usage=usage) ++ parser = IPAOptionParser(usage=usage) + parser.add_option( + "--server", + dest="server", +diff --git a/ipalib/cli.py b/ipalib/cli.py +index d9c2ac16513101098c8f9a2258ae930b3b2cebf0..667b213fd4e4706e6eecd4d2b4737fb3fede4c93 100644 +--- a/ipalib/cli.py ++++ b/ipalib/cli.py +@@ -30,7 +30,6 @@ import textwrap + import sys + import getpass + import code +-import optparse # pylint: disable=deprecated-module + import os + import pprint + import fcntl +@@ -71,6 +70,8 @@ from ipalib.text import _ + from ipalib import api + from ipapython.dnsutil import DNSName + from ipapython.admintool import ScriptError ++from ipapython.config import (IPAOptionParser, IPAFormatter, ++ OptionGroup, make_option) + + import datetime + +@@ -1121,7 +1122,8 @@ class Collector: + def __todict__(self): + return dict(self.__options) + +-class CLIOptionParserFormatter(optparse.IndentedHelpFormatter): ++ ++class CLIOptionParserFormatter(IPAFormatter): + def format_argument(self, name, help_string): + result = [] + opt_width = self.help_position - self.current_indent - 2 +@@ -1141,7 +1143,8 @@ class CLIOptionParserFormatter(optparse.IndentedHelpFormatter): + result.append("\n") + return "".join(result) + +-class CLIOptionParser(optparse.OptionParser): ++ ++class CLIOptionParser(IPAOptionParser): + """ + This OptionParser subclass adds an ability to print positional + arguments in CLI help. Custom formatter is used to format the argument +@@ -1151,13 +1154,13 @@ class CLIOptionParser(optparse.OptionParser): + self._arguments = [] + if 'formatter' not in kwargs: + kwargs['formatter'] = CLIOptionParserFormatter() +- optparse.OptionParser.__init__(self, *args, **kwargs) ++ IPAOptionParser.__init__(self, *args, **kwargs) + + def format_option_help(self, formatter=None): + """ + Prepend argument help to standard OptionParser's option help + """ +- option_help = optparse.OptionParser.format_option_help(self, formatter) ++ option_help = IPAOptionParser.format_option_help(self, formatter) + + if isinstance(formatter, CLIOptionParserFormatter): + heading = unicode(_("Positional arguments")) +@@ -1272,7 +1275,7 @@ class cli(backend.Executioner): + """Get or create an option group for the given name""" + option_group = option_groups.get(group_name) + if option_group is None: +- option_group = optparse.OptionGroup(parser, group_name) ++ option_group = OptionGroup(parser, group_name) + parser.add_option_group(option_group) + option_groups[group_name] = option_group + return option_group +@@ -1298,7 +1301,7 @@ class cli(backend.Executioner): + option_names = ['--%s' % cli_name] + if option.cli_short_name: + option_names.append('-%s' % option.cli_short_name) +- opt = optparse.make_option(*option_names, **kw) ++ opt = make_option(*option_names, **kw) + if option.option_group is None: + parser.add_option(opt) + else: +@@ -1312,7 +1315,7 @@ class cli(backend.Executioner): + group = _get_option_group(unicode(_('Deprecated options'))) + for alias in option.deprecated_cli_aliases: + name = '--%s' % alias +- group.add_option(optparse.make_option(name, **new_kw)) ++ group.add_option(make_option(name, **new_kw)) + + for arg in cmd.args(): + name = self.__get_arg_name(arg, format_name=False) +@@ -1442,7 +1445,7 @@ class cli(backend.Executioner): + ) + + +-class IPAHelpFormatter(optparse.IndentedHelpFormatter): ++class IPAHelpFormatter(IPAFormatter): + """Formatter suitable for printing IPA command help + + The default help formatter reflows text to fit the terminal, but it +diff --git a/ipalib/plugable.py b/ipalib/plugable.py +index 2e2861df054fa7ed3533d0f82a23b65322ab591b..a87e6e8914fa5579920aad8190c7f6a86cb3dfea 100644 +--- a/ipalib/plugable.py ++++ b/ipalib/plugable.py +@@ -33,7 +33,6 @@ import sys + import threading + import os + from os import path +-import optparse # pylint: disable=deprecated-module + import textwrap + import collections + import importlib +@@ -47,6 +46,7 @@ from ipalib.util import classproperty + from ipalib.base import ReadOnly, lock, islocked + from ipalib.constants import DEFAULT_CONFIG + from ipapython import ipa_log_manager, ipautil ++from ipapython.config import IPAOptionParser, IPAFormatter + from ipapython.ipa_log_manager import ( + LOGGING_FORMAT_FILE, + LOGGING_FORMAT_STDERR) +@@ -526,7 +526,7 @@ class API(ReadOnly): + + def build_global_parser(self, parser=None, context=None): + """ +- Add global options to an optparse.OptionParser instance. ++ Add global options to an IPAOptionParser instance. + """ + def config_file_callback(option, opt, value, parser): + if not os.path.isfile(value): +@@ -536,7 +536,7 @@ class API(ReadOnly): + parser.values.conf = value + + if parser is None: +- parser = optparse.OptionParser( ++ parser = IPAOptionParser( + add_help_option=False, + formatter=IPAHelpFormatter(), + usage='%prog [global-options] COMMAND [command-options]', +@@ -821,7 +821,7 @@ class API(ReadOnly): + return self.__next[plugin] + + +-class IPAHelpFormatter(optparse.IndentedHelpFormatter): ++class IPAHelpFormatter(IPAFormatter): + def format_epilog(self, epilog): + text_width = self.width - self.current_indent + indent = " " * self.current_indent +diff --git a/ipapython/admintool.py b/ipapython/admintool.py +index fdb4400d879165612405b3ba159a220773bd8d69..dff9112eba4009d2fc1a6202756c6671ba4d909d 100644 +--- a/ipapython/admintool.py ++++ b/ipapython/admintool.py +@@ -26,7 +26,6 @@ import logging + import sys + import os + import traceback +-from optparse import OptionGroup # pylint: disable=deprecated-module + + from ipaplatform.osinfo import osinfo + from ipapython import version +@@ -113,7 +112,7 @@ class AdminTool: + :param parser: The parser to add options to + :param debug_option: Add a --debug option as an alias to --verbose + """ +- group = OptionGroup(parser, "Logging and output options") ++ group = config.OptionGroup(parser, "Logging and output options") + group.add_option("-v", "--verbose", dest="verbose", default=False, + action="store_true", help="print debugging information") + if debug_option: +diff --git a/ipapython/config.py b/ipapython/config.py +index f53d0f998aaedf47715764577dd7f5048f4ca841..7af4dfdeb0047586de9694f233be8bf543190b9c 100644 +--- a/ipapython/config.py ++++ b/ipapython/config.py +@@ -18,9 +18,9 @@ + # + from __future__ import absolute_import + +-# pylint: disable=deprecated-module +-from optparse import ( +- Option, Values, OptionParser, IndentedHelpFormatter, OptionValueError) ++# pylint: disable=deprecated-module, disable=unused-import ++from optparse import (Option, Values, OptionGroup, OptionParser, SUPPRESS_HELP, ++ IndentedHelpFormatter, OptionValueError, make_option) + # pylint: enable=deprecated-module + from copy import copy + from configparser import ConfigParser as SafeConfigParser +@@ -113,10 +113,14 @@ class IPAOptionParser(OptionParser): + description=None, + formatter=None, + add_help_option=True, +- prog=None): +- OptionParser.__init__(self, usage, option_list, option_class, +- version, conflict_handler, description, +- formatter, add_help_option, prog) ++ prog=None, ++ epilog=None): ++ OptionParser.__init__(self, usage=usage, option_list=option_list, ++ option_class=option_class, version=version, ++ conflict_handler=conflict_handler, ++ description=description, formatter=formatter, ++ add_help_option=add_help_option, prog=prog, ++ epilog=epilog) + + def get_safe_opts(self, opts): + """ +diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py +index ab212be4e2c3f9c2aae97f7759672ef7c68c3347..a048b3c7c64bc22338e4517d64b33a44446c58a3 100644 +--- a/ipapython/install/cli.py ++++ b/ipapython/install/cli.py +@@ -9,12 +9,11 @@ Command line support. + import collections + import enum + import logging +-import optparse # pylint: disable=deprecated-module + import signal + + import six + +-from ipapython import admintool ++from ipapython import admintool, config + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.ipautil import (CheckedIPAddress, CheckedIPAddressLoopback, + private_ccache) +@@ -158,7 +157,7 @@ class ConfigureTool(admintool.AdminTool): + try: + opt_group = groups[group_cls] + except KeyError: +- opt_group = groups[group_cls] = optparse.OptionGroup( ++ opt_group = groups[group_cls] = config.OptionGroup( + parser, "{0} options".format(group_cls.description)) + parser.add_option_group(opt_group) + +@@ -232,7 +231,7 @@ class ConfigureTool(admintool.AdminTool): + if not hidden: + help = knob_cls.description + else: +- help = optparse.SUPPRESS_HELP ++ help = config.SUPPRESS_HELP + + opt_group.add_option( + *opt_strs, +@@ -256,7 +255,7 @@ class ConfigureTool(admintool.AdminTool): + + # fake option parser to parse positional arguments + # (because optparse does not support positional argument parsing) +- fake_option_parser = optparse.OptionParser() ++ fake_option_parser = config.IPAOptionParser() + self.add_options(fake_option_parser, True) + + fake_option_map = {option.dest: option +diff --git a/ipaserver/install/ipa_acme_manage.py b/ipaserver/install/ipa_acme_manage.py +index dc2359f49dfdd5c8f44ab96ee11a7240f8937e11..0decab394c1c18067fe0c194c040805a8d93d42d 100644 +--- a/ipaserver/install/ipa_acme_manage.py ++++ b/ipaserver/install/ipa_acme_manage.py +@@ -7,14 +7,12 @@ import enum + import pki.util + import logging + +-from optparse import OptionGroup # pylint: disable=deprecated-module +- + from ipalib import api, errors, x509 + from ipalib import _ + from ipalib.facts import is_ipa_configured + from ipaplatform.paths import paths + from ipapython.admintool import AdminTool +-from ipapython import cookie, dogtag ++from ipapython import cookie, dogtag, config + from ipapython.ipautil import run + from ipapython.certdb import NSSDatabase, EXTERNAL_CA_TRUST_FLAGS + from ipaserver.install import cainstance +@@ -143,7 +141,7 @@ class IPAACMEManage(AdminTool): + @classmethod + def add_options(cls, parser): + +- group = OptionGroup(parser, 'Pruning') ++ group = config.OptionGroup(parser, 'Pruning') + group.add_option( + "--enable", dest="enable", action="store_true", + default=False, help="Enable certificate pruning") +diff --git a/ipaserver/install/ipa_backup.py b/ipaserver/install/ipa_backup.py +index 982e5dfc4c0339aada88f936ab450b7fc16944f2..b6af63813fc4eaadab44ad95386d86ae4f1e21ee 100644 +--- a/ipaserver/install/ipa_backup.py ++++ b/ipaserver/install/ipa_backup.py +@@ -20,7 +20,6 @@ + from __future__ import absolute_import, print_function + + import logging +-import optparse # pylint: disable=deprecated-module + import os + import shutil + import sys +@@ -32,7 +31,7 @@ import six + from ipaplatform.paths import paths + from ipaplatform import services + from ipalib import api, errors +-from ipapython import version ++from ipapython import version, config + from ipapython.ipautil import run, write_tmp_file + from ipapython import admintool, certdb + from ipapython.dn import DN +@@ -245,7 +244,7 @@ class Backup(admintool.AdminTool): + + parser.add_option( + "--gpg-keyring", dest="gpg_keyring", +- help=optparse.SUPPRESS_HELP) ++ help=config.SUPPRESS_HELP) + parser.add_option( + "--gpg", dest="gpg", action="store_true", + default=False, help="Encrypt the backup") +diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py +index f6ab736fa985b00ba66a7001c4c4e2188841bcbe..048245237855212afe1f3ec4795b2253026ef864 100644 +--- a/ipaserver/install/ipa_cacert_manage.py ++++ b/ipaserver/install/ipa_cacert_manage.py +@@ -22,14 +22,13 @@ from __future__ import print_function, absolute_import + import datetime + import logging + import os +-from optparse import OptionGroup # pylint: disable=deprecated-module + import gssapi + + from ipalib.constants import ( + RENEWAL_CA_NAME, RENEWAL_REUSE_CA_NAME, RENEWAL_SELFSIGNED_CA_NAME, + IPA_CA_CN) + from ipalib.install import certmonger, certstore +-from ipapython import admintool, ipautil ++from ipapython import admintool, ipautil, config + from ipapython.certdb import (EMPTY_TRUST_FLAGS, + EXTERNAL_CA_TRUST_FLAGS, + TrustFlags, +@@ -61,7 +60,7 @@ class CACertManage(admintool.AdminTool): + "-p", "--password", dest='password', + help="Directory Manager password") + +- renew_group = OptionGroup(parser, "Renew options") ++ renew_group = config.OptionGroup(parser, "Renew options") + renew_group.add_option( + "--self-signed", dest='self_signed', + action='store_true', +@@ -89,7 +88,7 @@ class CACertManage(admintool.AdminTool): + "certificate chain") + parser.add_option_group(renew_group) + +- install_group = OptionGroup(parser, "Install options") ++ install_group = config.OptionGroup(parser, "Install options") + install_group.add_option( + "-n", "--nickname", dest='nickname', + help="Nickname for the certificate") +@@ -98,7 +97,7 @@ class CACertManage(admintool.AdminTool): + help="Trust flags for the certificate in certutil format") + parser.add_option_group(install_group) + +- delete_group = OptionGroup(parser, "Delete options") ++ delete_group = config.OptionGroup(parser, "Delete options") + delete_group.add_option( + "-f", "--force", action='store_true', + help="Force removing the CA even if chain validation fails") +diff --git a/ipaserver/install/ipa_kra_install.py b/ipaserver/install/ipa_kra_install.py +index 3e4cd67fa677db2534a639eb6beb14dfd78bf035..8a09179f7fa0c2ddad72d01e9c3eaf98575d0a88 100644 +--- a/ipaserver/install/ipa_kra_install.py ++++ b/ipaserver/install/ipa_kra_install.py +@@ -22,13 +22,12 @@ from __future__ import print_function, absolute_import + import logging + import sys + import tempfile +-from optparse import SUPPRESS_HELP # pylint: disable=deprecated-module + + from textwrap import dedent + from ipalib import api + from ipalib.constants import DOMAIN_LEVEL_1 + from ipaplatform.paths import paths +-from ipapython import admintool ++from ipapython import admintool, config + from ipaserver.install import service + from ipaserver.install import cainstance + from ipaserver.install import custodiainstance +@@ -73,7 +72,7 @@ class KRAInstall(admintool.AdminTool): + parser.add_option( + "--uninstall", + dest="uninstall", action="store_true", default=False, +- help=SUPPRESS_HELP) ++ help=config.SUPPRESS_HELP) + + parser.add_option( + "--pki-config-override", dest="pki_config_override", +diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py +index 57ad8dd05206132d3458fa84e7fb996b135f7f71..8d75a0e6beba09086bc2ce8c73f3bcfe81e52113 100644 +--- a/ipaserver/install/ipa_restore.py ++++ b/ipaserver/install/ipa_restore.py +@@ -20,7 +20,6 @@ + from __future__ import absolute_import, print_function + + import logging +-import optparse # pylint: disable=deprecated-module + import os + import shutil + import sys +@@ -34,7 +33,7 @@ import six + from ipaclient.install.client import update_ipa_nssdb + from ipalib import api, errors + from ipalib.constants import FQDN +-from ipapython import version, ipautil ++from ipapython import version, ipautil, config + from ipapython.ipautil import run, user_input + from ipapython import admintool, certdb + from ipapython.dn import DN +@@ -190,7 +189,7 @@ class Restore(admintool.AdminTool): + help="Directory Manager password") + parser.add_option( + "--gpg-keyring", dest="gpg_keyring", +- help=optparse.SUPPRESS_HELP) ++ help=config.SUPPRESS_HELP) + parser.add_option( + "--data", dest="data_only", action="store_true", + default=False, help="Restore only the data") +diff --git a/ipaserver/install/ipa_server_certinstall.py b/ipaserver/install/ipa_server_certinstall.py +index e29f00ec37779aeb10255c5df6e10d6ecc0a6d11..e9f680b1d1e505f89fc1611b8dbb3f84768f6781 100644 +--- a/ipaserver/install/ipa_server_certinstall.py ++++ b/ipaserver/install/ipa_server_certinstall.py +@@ -22,12 +22,11 @@ from __future__ import print_function, absolute_import + import os + import os.path + import tempfile +-import optparse # pylint: disable=deprecated-module + + from ipalib import x509 + from ipalib.install import certmonger + from ipaplatform.paths import paths +-from ipapython import admintool, dogtag ++from ipapython import admintool, dogtag, config + from ipapython.certdb import NSSDatabase, get_ca_nickname + from ipapython.dn import DN + from ipapython import ipaldap +@@ -65,8 +64,8 @@ class ServerCertInstall(admintool.AdminTool): + help="The password of the PKCS#12 file") + parser.add_option( + "--dirsrv_pin", "--http_pin", +- dest="pin", +- help=optparse.SUPPRESS_HELP) ++ dest="pin", sensitive=True, ++ help=config.SUPPRESS_HELP) + parser.add_option( + "--cert-name", + dest="cert_name", metavar="NAME", +diff --git a/ipatests/i18n.py b/ipatests/i18n.py +index 49f5c4c3232346db3147bd7a5ba8056344ac907f..57915c286be72124fa23380f97f3922496f00c22 100644 +--- a/ipatests/i18n.py ++++ b/ipatests/i18n.py +@@ -22,7 +22,6 @@ from __future__ import print_function + + # WARNING: Do not import ipa modules, this is also used as a + # stand-alone script (invoked from po Makefile). +-import optparse # pylint: disable=deprecated-module + import sys + import gettext + import re +@@ -30,6 +29,7 @@ import os + import traceback + import polib + from collections import namedtuple ++from ipapython import config + + import six + +@@ -722,9 +722,9 @@ usage =''' + def main(): + global verbose, print_traceback, pedantic, show_strings + +- parser = optparse.OptionParser(usage=usage) ++ parser = config.IPAOptionParser(usage=usage) + +- mode_group = optparse.OptionGroup(parser, 'Operational Mode', ++ mode_group = config.OptionGroup(parser, 'Operational Mode', + 'You must select one these modes to run in') + + mode_group.add_option('-g', '--test-gettext', action='store_const', const='test_gettext', dest='mode', +@@ -748,7 +748,7 @@ def main(): + parser.add_option('--traceback', action='store_true', dest='print_traceback', default=False, + help='print the traceback when an exception occurs') + +- param_group = optparse.OptionGroup(parser, 'Run Time Parameters', ++ param_group = config.OptionGroup(parser, 'Run Time Parameters', + 'These may be used to modify the run time defaults') + + param_group.add_option('--test-lang', action='store', dest='test_lang', default='test', +diff --git a/makeapi.in b/makeapi.in +index a801b9253db9500e0510fe591074ccf2e6d752c1..8fc87d23de743a6d661112ee616dce129a785a36 100644 +--- a/makeapi.in ++++ b/makeapi.in +@@ -38,6 +38,7 @@ from ipalib.parameters import Param + from ipalib.output import Output + from ipalib.text import Gettext, NGettext, ConcatenatedLazyText + from ipalib.capabilities import capabilities ++from ipapython import config + + API_FILE='API.txt' + +@@ -84,9 +85,7 @@ OUTPUT_IGNORED_ATTRIBUTES = ( + ) + + def parse_options(): +- from optparse import OptionParser # pylint: disable=deprecated-module +- +- parser = OptionParser() ++ parser = config.IPAOptionParser() + parser.add_option("--validate", dest="validate", action="store_true", + default=False, help="Validate the API vs the stored API") + +-- +2.47.1 + diff --git a/SOURCES/0044-ipa-tools-remove-sensitive-material-from-the-command.patch b/SOURCES/0044-ipa-tools-remove-sensitive-material-from-the-command.patch new file mode 100644 index 0000000..5556395 --- /dev/null +++ b/SOURCES/0044-ipa-tools-remove-sensitive-material-from-the-command.patch @@ -0,0 +1,422 @@ +From f9314562aaae03619eb89ae762bc24182174ad28 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Fri, 8 Nov 2024 14:59:20 +0200 +Subject: [PATCH] ipa tools: remove sensitive material from the commandline + +When command line tools accept passwords, remove them from the command +line so that they don't get visible in '/proc/pid/commandline'. + +There is no common method to access the original ARGV vector and modify +it from Python. Since this mostly affects Linux systems where IPA +services run, we expect use of GNU libc and thus can rely on internal +glibc symbols. If they aren't available, the code will skip removing +passwords. + +Fixes: CVE-2024-11029 + +Signed-off-by: Alexander Bokovoy +--- + .../com.redhat.idm.trust-fetch-domains.in | 5 ++- + install/tools/ipa-adtrust-install.in | 3 +- + install/tools/ipa-ca-install.in | 2 + + install/tools/ipa-compat-manage.in | 6 ++- + install/tools/ipa-csreplica-manage.in | 12 +++--- + install/tools/ipa-managed-entries.in | 5 ++- + install/tools/ipa-replica-conncheck.in | 7 ++-- + install/tools/ipa-replica-manage.in | 10 +++-- + ipapython/admintool.py | 40 +++++++++++++++++++ + ipaserver/install/ipa_migrate.py | 17 +++++++- + ipaserver/install/ipa_restore.py | 2 +- + ipaserver/install/ipa_server_certinstall.py | 2 +- + 12 files changed, 90 insertions(+), 21 deletions(-) + +diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in +index 45c1f1463d5849d753c2913aa0b86b845cfce784..b86be0212c462e82d9379f55d5d3198c1017d2e7 100644 +--- a/install/oddjob/com.redhat.idm.trust-fetch-domains.in ++++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in +@@ -15,6 +15,7 @@ import six + import gssapi + + from ipalib.install.kinit import kinit_keytab, kinit_password ++from ipapython.admintool import admin_cleanup_global_argv + + if six.PY3: + unicode = str +@@ -52,11 +53,13 @@ def parse_options(): + "--password", + action="store", + dest="password", +- help="Display debugging information", ++ help="Password for Active Directory administrator", ++ sensitive=True + ) + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) ++ admin_cleanup_global_argv(parser, options, sys.argv) + + # We only use first argument of the passed args but as D-BUS interface + # in oddjobd cannot expose optional, we fill in empty slots from IPA side +diff --git a/install/tools/ipa-adtrust-install.in b/install/tools/ipa-adtrust-install.in +index e7b0e369259da5d28d703558d9293ccfaf68f3ed..1efccdb678d17410667b1721670bf6bf79152109 100644 +--- a/install/tools/ipa-adtrust-install.in ++++ b/install/tools/ipa-adtrust-install.in +@@ -35,7 +35,7 @@ from ipaserver.install.installutils import ( + read_password, + check_server_configuration, + run_script) +-from ipapython.admintool import ScriptError ++from ipapython.admintool import ScriptError, admin_cleanup_global_argv + from ipapython import version + from ipapython import ipautil + from ipalib import api, errors, krb_utils +@@ -93,6 +93,7 @@ def parse_options(): + + options, _args = parser.parse_args() + safe_options = parser.get_safe_opts(options) ++ admin_cleanup_global_argv(parser, options, sys.argv) + + return safe_options, options + +diff --git a/install/tools/ipa-ca-install.in b/install/tools/ipa-ca-install.in +index 9f3d16669679a245b73e044622ff52321524fcde..b437e761f760ab715c27bc330ac0f2e66e72ef5b 100644 +--- a/install/tools/ipa-ca-install.in ++++ b/install/tools/ipa-ca-install.in +@@ -42,6 +42,7 @@ from ipalib.constants import DOMAIN_LEVEL_1 + from ipapython.config import IPAOptionParser + from ipapython.ipa_log_manager import standard_logging_setup + from ipaplatform.paths import paths ++from ipapython.admintool import admin_cleanup_global_argv + + logger = logging.getLogger(os.path.basename(__file__)) + +@@ -132,6 +133,7 @@ def parse_options(): + + options, args = parser.parse_args() + safe_options = parser.get_safe_opts(options) ++ admin_cleanup_global_argv(parser, options, sys.argv) + + if args: + parser.error("Too many arguments provided") +diff --git a/install/tools/ipa-compat-manage.in b/install/tools/ipa-compat-manage.in +index 459f39fc826bbe6becd8be3517235af343d4b0d9..9650abd6f83ebc0a8ef347fee83989d4e9f13f09 100644 +--- a/install/tools/ipa-compat-manage.in ++++ b/install/tools/ipa-compat-manage.in +@@ -24,13 +24,13 @@ from __future__ import print_function + import sys + from ipaplatform.paths import paths + try: +- from optparse import OptionParser # pylint: disable=deprecated-module + from ipapython import ipautil, config + from ipaserver.install import installutils + from ipaserver.install.ldapupdate import LDAPUpdate + from ipalib import api, errors + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.dn import DN ++ from ipapython.admintool import admin_cleanup_global_argv + except ImportError as e: + print("""\ + There was a problem importing one of the required Python modules. The +@@ -46,7 +46,8 @@ nis_config_dn = DN(('cn', 'NIS Server'), ('cn', 'plugins'), ('cn', 'config')) + def parse_options(): + usage = "%prog [options] \n" + usage += "%prog [options]\n" +- parser = OptionParser(usage=usage, formatter=config.IPAFormatter()) ++ parser = config.IPAOptionParser(usage=usage, ++ formatter=config.IPAFormatter()) + + parser.add_option("-d", "--debug", action="store_true", dest="debug", + help="Display debugging information about the update(s)") +@@ -55,6 +56,7 @@ def parse_options(): + + config.add_standard_options(parser) + options, args = parser.parse_args() ++ admin_cleanup_global_argv(parser, options, sys.argv) + + return options, args + +diff --git a/install/tools/ipa-csreplica-manage.in b/install/tools/ipa-csreplica-manage.in +index 6f248cc50f7f81b2014d629355f6f32d1d6ab7be..2fab27a94cfdbef8faadca82bc222bf96d212159 100644 +--- a/install/tools/ipa-csreplica-manage.in ++++ b/install/tools/ipa-csreplica-manage.in +@@ -32,8 +32,8 @@ from ipaserver.install import (replication, installutils, bindinstance, + from ipalib import api, errors + from ipalib.constants import FQDN + from ipalib.util import has_managed_topology, print_replication_status +-from ipapython import ipautil, ipaldap, version +-from ipapython.admintool import ScriptError ++from ipapython import ipautil, ipaldap, version, config ++from ipapython.admintool import admin_cleanup_global_argv, ScriptError + from ipapython.dn import DN + + logger = logging.getLogger(os.path.basename(__file__)) +@@ -54,11 +54,10 @@ commands = { + + + def parse_options(): +- from optparse import OptionParser # pylint: disable=deprecated-module +- +- parser = OptionParser(version=version.VERSION) ++ parser = config.IPAOptionParser(version=version.VERSION) + parser.add_option("-H", "--host", dest="host", help="starting host") +- parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") ++ parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True, ++ help="Directory Manager password") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="provide additional information") + parser.add_option("-f", "--force", dest="force", action="store_true", default=False, +@@ -66,6 +65,7 @@ def parse_options(): + parser.add_option("--from", dest="fromhost", help="Host to get data from") + + options, args = parser.parse_args() ++ admin_cleanup_global_argv(parser, options, sys.argv) + + valid_syntax = False + +diff --git a/install/tools/ipa-managed-entries.in b/install/tools/ipa-managed-entries.in +index e3f121943eb3b18ca8f7f8bfeae7813cbc9bd753..ff2fd6a58ccb5b322f75008578fea81f561666fa 100644 +--- a/install/tools/ipa-managed-entries.in ++++ b/install/tools/ipa-managed-entries.in +@@ -24,7 +24,6 @@ import logging + import os + import re + import sys +-from optparse import OptionParser # pylint: disable=deprecated-module + + from ipaplatform.paths import paths + from ipapython import config +@@ -32,6 +31,7 @@ from ipaserver.install import installutils + from ipalib import api, errors + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.dn import DN ++from ipapython.admintool import admin_cleanup_global_argv + + logger = logging.getLogger(os.path.basename(__file__)) + +@@ -51,9 +51,10 @@ def parse_options(): + action="store_true", + help="List available Managed Entries") + parser.add_option("-p", "--password", dest="dirman_password", +- help="Directory Manager password") ++ sensitive=True, help="Directory Manager password") + + options, args = parser.parse_args() ++ admin_cleanup_global_argv(parser, options, sys.argv) + + return options, args + +diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in +index 8eee82483abc275cb37ad96ff272b6ee8192403f..81b7d13ac900031fa81356b894dc3eaaaee0f744 100644 +--- a/install/tools/ipa-replica-conncheck.in ++++ b/install/tools/ipa-replica-conncheck.in +@@ -23,15 +23,15 @@ from __future__ import print_function + import logging + + from ipapython import ipachangeconf +-from ipapython.config import IPAOptionParser ++from ipapython.config import (IPAOptionParser, OptionGroup, ++ OptionValueError) ++from ipapython.admintool import admin_cleanup_global_argv + from ipapython.dn import DN + from ipapython import version + from ipapython import ipautil, certdb + from ipalib import api, errors, x509 + from ipalib.constants import FQDN + from ipaserver.install import installutils +-# pylint: disable=deprecated-module +-from optparse import OptionGroup, OptionValueError + # pylint: enable=deprecated-module + from ipapython.ipa_log_manager import standard_logging_setup + import copy +@@ -189,6 +189,7 @@ def parse_options(): + + options, _args = parser.parse_args() + safe_options = parser.get_safe_opts(options) ++ admin_cleanup_global_argv(parser, options, sys.argv) + + if options.master and options.replica: + parser.error("on-master and on-replica options are mutually exclusive!") +diff --git a/install/tools/ipa-replica-manage.in b/install/tools/ipa-replica-manage.in +index d6e6ef57c39af70f164d41662227af3dc2535f9c..7e5b31a598537f57c471729f22fdec4a5bedc03d 100644 +--- a/install/tools/ipa-replica-manage.in ++++ b/install/tools/ipa-replica-manage.in +@@ -43,6 +43,7 @@ from ipalib.util import ( + print_replication_status, + verify_host_resolvable, + ) ++from ipapython.admintool import admin_cleanup_global_argv + from ipapython.ipa_log_manager import standard_logging_setup + from ipapython.dn import DN + from ipapython.config import IPAOptionParser +@@ -84,7 +85,8 @@ class NoRUVsFound(Exception): + def parse_options(): + parser = IPAOptionParser(version=version.VERSION) + parser.add_option("-H", "--host", dest="host", help="starting host") +- parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") ++ parser.add_option("-p", "--password", dest="dirman_passwd", sensitive=True, ++ help="Directory Manager password") + parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, + help="provide additional information") + parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, +@@ -95,7 +97,7 @@ def parse_options(): + help="DANGER: clean up references to a ghost master") + parser.add_option("--binddn", dest="binddn", default=None, type="dn", + help="Bind DN to use with remote server") +- parser.add_option("--bindpw", dest="bindpw", default=None, ++ parser.add_option("--bindpw", dest="bindpw", default=None, sensitive=True, + help="Password for Bind DN to use with remote server") + parser.add_option("--winsync", dest="winsync", action="store_true", default=False, + help="This is a Windows Sync Agreement") +@@ -103,13 +105,15 @@ def parse_options(): + help="Full path and filename of CA certificate to use with TLS/SSL to the remote server") + parser.add_option("--win-subtree", dest="win_subtree", default=None, + help="DN of Windows subtree containing the users you want to sync (default cn=Users, ...', add two args ++ _argc = len(argv) + 2 ++ all_options = [] ++ if '_get_all_options' in dir(option_parser): ++ # OptParse parser ++ all_options = option_parser._get_all_options() ++ elif '_actions' in dir(option_parser): ++ # ArgParse parser ++ all_options = option_parser._actions ++ ++ for opt in all_options: ++ if getattr(opt, 'sensitive', False): ++ v = getattr(options, opt.dest) ++ for i in range(0, _argc): ++ vi = ctypes.cast(_argv[i], ++ ctypes.c_char_p ++ ).value.decode('utf-8') ++ if vi == v: ++ ctypes.memset(_argv[i], ord('X'), len(v)) ++ except Exception: ++ pass ++ ++ + class ScriptError(Exception): + """An exception that records an error message and a return value + """ +@@ -148,6 +187,7 @@ class AdminTool: + cls._option_parsers[cls] = cls.option_parser + + options, args = cls.option_parser.parse_args(argv[1:]) ++ admin_cleanup_global_argv(cls.option_parser, options, argv) + + command_class = cls.get_command_class(options, args) + command = command_class(options, args) +diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py +index f35629378490d3d45ca97f2aa5b4390c67d660ed..ece473bc8cb525e2d563356b5b274502d6b703e8 100644 +--- a/ipaserver/install/ipa_migrate.py ++++ b/ipaserver/install/ipa_migrate.py +@@ -28,6 +28,7 @@ from ipaplatform.paths import paths + from ipapython.dn import DN + from ipapython.ipaldap import LDAPClient, LDAPEntry, realm_to_ldapi_uri + from ipapython.ipa_log_manager import standard_logging_setup ++from ipapython.admintool import admin_cleanup_global_argv + from ipaserver.install.ipa_migrate_constants import ( + DS_CONFIG, DB_OBJECTS, DS_INDEXES, BIND_DN, LOG_FILE_NAME, + STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS, +@@ -284,6 +285,18 @@ class LDIFParser(ldif.LDIFParser): + self.mc.process_db_entry(entry_dn=dn, entry_attrs=entry_attrs) + + ++class SensitiveStoreAction(argparse._StoreAction): ++ def __init__(self, *, sensitive, **options): ++ super(SensitiveStoreAction, self).__init__(**options) ++ self.sensitive = sensitive ++ ++ def _get_kwargs(self): ++ names = super(SensitiveStoreAction, self)._get_kwargs() ++ sensitive_name = 'sensitive' ++ names.extend((sensitive_name, getattr(self, sensitive_name))) ++ return names ++ ++ + # + # Migrate IPA to IPA Class + # +@@ -344,7 +357,8 @@ class IPAMigrate(): + help='Password for the Bind DN. If a password ' + 'is not provided then the user will be ' + 'prompted to enter it', +- default=None) ++ default=None, sensitive=True, ++ action=SensitiveStoreAction) + parser.add_argument('-j', '--bind-pw-file', + help='A text file containing the clear text ' + 'password for the Bind DN', default=None) +@@ -2128,6 +2142,7 @@ class IPAMigrate(): + parser = argparse.ArgumentParser(description=desc, allow_abbrev=True) + self.add_options(parser) + self.validate_options() ++ admin_cleanup_global_argv(parser, self.args, sys.argv) + + # Check for dryrun mode + if self.args.dryrun or self.args.dryrun_record is not None: +diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py +index 8d75a0e6beba09086bc2ce8c73f3bcfe81e52113..539501ab4f0c73571b680aeccf3854b511fd29dc 100644 +--- a/ipaserver/install/ipa_restore.py ++++ b/ipaserver/install/ipa_restore.py +@@ -185,7 +185,7 @@ class Restore(admintool.AdminTool): + super(Restore, cls).add_options(parser, debug_option=True) + + parser.add_option( +- "-p", "--password", dest="password", ++ "-p", "--password", dest="password", sensitive=True, + help="Directory Manager password") + parser.add_option( + "--gpg-keyring", dest="gpg_keyring", +diff --git a/ipaserver/install/ipa_server_certinstall.py b/ipaserver/install/ipa_server_certinstall.py +index e9f680b1d1e505f89fc1611b8dbb3f84768f6781..76ad37ca7bcc62364379d56b21ead43c5248f5f1 100644 +--- a/ipaserver/install/ipa_server_certinstall.py ++++ b/ipaserver/install/ipa_server_certinstall.py +@@ -72,7 +72,7 @@ class ServerCertInstall(admintool.AdminTool): + help="Name of the certificate to install") + parser.add_option( + "-p", "--dirman-password", +- dest="dirman_password", ++ dest="dirman_password", sensitive=True, + help="Directory Manager password") + + def validate_options(self): +-- +2.47.1 + diff --git a/SOURCES/0045-ipa-otpd-use-oidc_child-s-client-secret-stdin-option.patch b/SOURCES/0045-ipa-otpd-use-oidc_child-s-client-secret-stdin-option.patch new file mode 100644 index 0000000..a42c3e8 --- /dev/null +++ b/SOURCES/0045-ipa-otpd-use-oidc_child-s-client-secret-stdin-option.patch @@ -0,0 +1,75 @@ +From d857fcfcc21481cdf06b8cce1685e141921d2fbf Mon Sep 17 00:00:00 2001 +From: Sumit Bose +Date: Wed, 27 Nov 2024 12:16:09 +0100 +Subject: [PATCH] ipa-otpd: use oidc_child's --client-secret-stdin option + +To remove the client secret from the command line where it would be +visible e.g. when calling ps it is now passed via stdin to oidc_child. + +Fixes: CVE-2024-11029 + +Signed-off-by: Sumit Bose +--- + daemons/ipa-otpd/oauth2.c | 24 ++++++++++++++---------- + 1 file changed, 14 insertions(+), 10 deletions(-) + +diff --git a/daemons/ipa-otpd/oauth2.c b/daemons/ipa-otpd/oauth2.c +index a33cf51715ca2a3e7a0cef871aed5cfbbd037598..52d7d7c9cb6c410bdbaa2e5eddccfea2204d3e69 100644 +--- a/daemons/ipa-otpd/oauth2.c ++++ b/daemons/ipa-otpd/oauth2.c +@@ -31,6 +31,7 @@ + #include + #include + #include ++#include + + #include "internal.h" + +@@ -93,6 +94,7 @@ static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev) + (void)vctx; /* Unused */ + ssize_t io; + struct child_ctx *child_ctx; ++ struct iovec iov[3]; + + child_ctx = verto_get_private(ev); + if (child_ctx == NULL) { +@@ -102,15 +104,18 @@ static void oauth2_on_child_writable(verto_ctx *vctx, verto_ev *ev) + } + + if (child_ctx->oauth2_state == OAUTH2_GET_DEVICE_CODE) { +- /* no input needed */ +- verto_del(ev); +- return; +- } +- ++ io = write(verto_get_fd(ev), child_ctx->item->idp.ipaidpClientSecret, ++ strlen(child_ctx->item->idp.ipaidpClientSecret)); ++ } else { ++ iov[0].iov_base = child_ctx->item->idp.ipaidpClientSecret; ++ iov[0].iov_len = strlen(child_ctx->item->idp.ipaidpClientSecret); ++ iov[1].iov_base = "\n"; ++ iov[1].iov_len = 1; ++ iov[2].iov_base = child_ctx->saved_item->oauth2.device_code_reply; ++ iov[2].iov_len = strlen(child_ctx->saved_item->oauth2.device_code_reply); + +- io = write(verto_get_fd(ev), +- child_ctx->saved_item->oauth2.device_code_reply, +- strlen(child_ctx->saved_item->oauth2.device_code_reply)); ++ io = writev(verto_get_fd(ev), iov, 3); ++ } + otpd_queue_item_free(child_ctx->saved_item); + + if (io < 0) { +@@ -429,8 +434,7 @@ int oauth2(struct otpd_queue_item **item, enum oauth2_state oauth2_state) + args[args_idx++] = (*item)->idp.ipaidpClientID; + + if ((*item)->idp.ipaidpClientSecret) { +- args[args_idx++] = "--client-secret"; +- args[args_idx++] = (*item)->idp.ipaidpClientSecret; ++ args[args_idx++] = "--client-secret-stdin"; + } + + if ((*item)->idp.ipaidpScope) { +-- +2.47.1 + diff --git a/SOURCES/0046-Fix-pylint-issue-in-ipatests-i18n.py.patch b/SOURCES/0046-Fix-pylint-issue-in-ipatests-i18n.py.patch new file mode 100644 index 0000000..3e3b822 --- /dev/null +++ b/SOURCES/0046-Fix-pylint-issue-in-ipatests-i18n.py.patch @@ -0,0 +1,69 @@ +From eef544c1d331bbe80852ebe8b5fc9bad0539b6fa Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Wed, 15 Jan 2025 15:39:20 +0100 +Subject: [PATCH] Fix pylint issue in ipatests/i18n.py + +This file should not import ipa modules + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rafael Guterres Jeffman +--- + ipatests/i18n.py | 8 ++++---- + pylintrc | 3 ++- + 2 files changed, 6 insertions(+), 5 deletions(-) + +diff --git a/ipatests/i18n.py b/ipatests/i18n.py +index 57915c286be72124fa23380f97f3922496f00c22..49f5c4c3232346db3147bd7a5ba8056344ac907f 100644 +--- a/ipatests/i18n.py ++++ b/ipatests/i18n.py +@@ -22,6 +22,7 @@ from __future__ import print_function + + # WARNING: Do not import ipa modules, this is also used as a + # stand-alone script (invoked from po Makefile). ++import optparse # pylint: disable=deprecated-module + import sys + import gettext + import re +@@ -29,7 +30,6 @@ import os + import traceback + import polib + from collections import namedtuple +-from ipapython import config + + import six + +@@ -722,9 +722,9 @@ usage =''' + def main(): + global verbose, print_traceback, pedantic, show_strings + +- parser = config.IPAOptionParser(usage=usage) ++ parser = optparse.OptionParser(usage=usage) + +- mode_group = config.OptionGroup(parser, 'Operational Mode', ++ mode_group = optparse.OptionGroup(parser, 'Operational Mode', + 'You must select one these modes to run in') + + mode_group.add_option('-g', '--test-gettext', action='store_const', const='test_gettext', dest='mode', +@@ -748,7 +748,7 @@ def main(): + parser.add_option('--traceback', action='store_true', dest='print_traceback', default=False, + help='print the traceback when an exception occurs') + +- param_group = config.OptionGroup(parser, 'Run Time Parameters', ++ param_group = optparse.OptionGroup(parser, 'Run Time Parameters', + 'These may be used to modify the run time defaults') + + param_group.add_option('--test-lang', action='store', dest='test_lang', default='test', +diff --git a/pylintrc b/pylintrc +index 50278cc76031af68959a138f6ea185c4288c7155..8fadeffbdfdb8013135a17b3493ecc4dc7e5e001 100644 +--- a/pylintrc ++++ b/pylintrc +@@ -153,4 +153,5 @@ forbidden-imports= + ipaplatform/:ipaclient:ipalib:ipaserver, + ipapython/:ipaclient:ipalib:ipaserver + ipatests/pytest_ipa:ipaserver:ipaclient.install:ipalib.install +- ipatests/test_integration:ipaserver ++ ipatests/test_integration:ipaserver, ++ ipatests/i18n.py:ipapython +-- +2.47.1 + diff --git a/SOURCES/0047-ipatests-skip-test_ipahealthcheck_ds_configcheck-for.patch b/SOURCES/0047-ipatests-skip-test_ipahealthcheck_ds_configcheck-for.patch new file mode 100644 index 0000000..8a0c5aa --- /dev/null +++ b/SOURCES/0047-ipatests-skip-test_ipahealthcheck_ds_configcheck-for.patch @@ -0,0 +1,51 @@ +From 45f96a0f978dfda0e2faa8360182a1dfd3122b94 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Fri, 10 Jan 2025 13:22:29 +0100 +Subject: [PATCH] ipatests: skip test_ipahealthcheck_ds_configcheck for recent + versions + +389-ds removed the parameter nsslapd-logging-hr-timestamps-enabled +in 2.5.3 and above. Skip the test that exercises the corresponding +healthcheck. + +Fixes: https://pagure.io/freeipa/issue/9730 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_ipahealthcheck.py | 12 +++++++++--- + 1 file changed, 9 insertions(+), 3 deletions(-) + +diff --git a/ipatests/test_integration/test_ipahealthcheck.py b/ipatests/test_integration/test_ipahealthcheck.py +index cc51a5a6a62fbc50927fc2fc51f129a069e70b69..6b6f15aa433a423fe599118d2226e4c4ec62b13b 100644 +--- a/ipatests/test_integration/test_ipahealthcheck.py ++++ b/ipatests/test_integration/test_ipahealthcheck.py +@@ -18,7 +18,7 @@ import uuid + + import pytest + +-from ipalib import x509 ++from ipalib import errors, x509 + from ipapython.dn import DN + from ipapython.ipaldap import realm_to_serverid + from ipapython.certdb import NSS_SQL_FILES +@@ -1146,8 +1146,14 @@ class TestIpaHealthCheck(IntegrationTest): + ) + entry = ldap.get_entry(dn) + entry.single_value["nsslapd-logging-hr-timestamps-enabled"] = 'off' +- ldap.update_entry(entry) +- ++ try: ++ ldap.update_entry(entry) ++ except errors.DatabaseError as e: ++ expected_msg = "Unknown attribute " \ ++ "nsslapd-logging-hr-timestamps-enabled" ++ if expected_msg in e.message: ++ pytest.skip( ++ "389-ds removed nsslapd-logging-hr-timestamps-enabled") + yield + + entry = ldap.get_entry(dn) +-- +2.47.1 + diff --git a/SOURCES/0048-ipatests-restart-dirsrv-after-time-jumps.patch b/SOURCES/0048-ipatests-restart-dirsrv-after-time-jumps.patch new file mode 100644 index 0000000..d5c410b --- /dev/null +++ b/SOURCES/0048-ipatests-restart-dirsrv-after-time-jumps.patch @@ -0,0 +1,35 @@ +From ec94ee72714296c86ba1227a5a945a7ed0bc7fac Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Thu, 16 Jan 2025 15:43:17 +0100 +Subject: [PATCH] ipatests: restart dirsrv after time jumps + +The test for ipa-healthcheck is moving the date in the future. +Restart the dirsrv instance because the LDAP server is +sensitive to large time jumps. + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_ipahealthcheck.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/ipatests/test_integration/test_ipahealthcheck.py b/ipatests/test_integration/test_ipahealthcheck.py +index 6b6f15aa433a423fe599118d2226e4c4ec62b13b..7c3f5857a477070d8a9b52c04d41f35ac580c97f 100644 +--- a/ipatests/test_integration/test_ipahealthcheck.py ++++ b/ipatests/test_integration/test_ipahealthcheck.py +@@ -1634,6 +1634,11 @@ class TestIpaHealthCheck(IntegrationTest): + grace_date = datetime.strftime(grace_date, "%Y-%m-%d 00:00:01 Z") + self.master.run_command(['date', '-s', grace_date]) + ++ # Restart dirsrv as it doesn't like time jumps ++ instance = realm_to_serverid(self.master.domain.realm) ++ cmd = ["systemctl", "restart", "dirsrv@{}".format(instance)] ++ self.master.run_command(cmd) ++ + for check in ("IPACertmongerExpirationCheck", + "IPACertfileExpirationCheck",): + execute_expiring_check(check) +-- +2.47.1 + diff --git a/SOURCES/0012-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch b/SOURCES/0049-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch similarity index 97% rename from SOURCES/0012-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch rename to SOURCES/0049-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch index 02c2253..5ebb167 100644 --- a/SOURCES/0012-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch +++ b/SOURCES/0049-ipa-otpd-do-not-pass-OIDC-client-secret-if-there-is-.patch @@ -1,4 +1,4 @@ -From d86db9d2c107c66372f422f1d628624b1a55ad45 Mon Sep 17 00:00:00 2001 +From f12c4ed600e9b35c930d386b37e36064fbf83968 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Fri, 17 Jan 2025 09:44:22 +0200 Subject: [PATCH] ipa-otpd: do not pass OIDC client secret if there is none to @@ -63,5 +63,5 @@ index 52d7d7c9cb6c410bdbaa2e5eddccfea2204d3e69..0eb43b2372701d47b9ef62cbbdb32b97 otpd_queue_item_free(child_ctx->saved_item); -- -2.48.1 +2.47.1 diff --git a/SOURCES/0013-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch b/SOURCES/0050-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch similarity index 99% rename from SOURCES/0013-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch rename to SOURCES/0050-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch index 82dcebc..0dc4f20 100644 --- a/SOURCES/0013-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch +++ b/SOURCES/0050-Migrate-Keycloak-tests-to-JDK-21-and-Keycloak-26.patch @@ -1,4 +1,4 @@ -From 431a5804949417257b204125ff0a898b98dd2a90 Mon Sep 17 00:00:00 2001 +From 4e43dd7cd30042588a2264fca98b6e6b9d4d25bb Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Fri, 17 Jan 2025 12:33:54 +0200 Subject: [PATCH] Migrate Keycloak tests to JDK 21 and Keycloak 26 @@ -161,5 +161,5 @@ index 9708e9fa05a75cb2657c657b39b015249f3fd208..57c5a96bae986ee9721fc540d2be2cdc "--password", kcadm_pass] -- -2.48.1 +2.47.1 diff --git a/SOURCES/0051-Apply-certmonger_timeout-to-start_tracking-and-reque.patch b/SOURCES/0051-Apply-certmonger_timeout-to-start_tracking-and-reque.patch new file mode 100644 index 0000000..dc7a2f6 --- /dev/null +++ b/SOURCES/0051-Apply-certmonger_timeout-to-start_tracking-and-reque.patch @@ -0,0 +1,162 @@ +From 9f30edef463237ba48efe45406626eb325bf6c39 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Wed, 22 Jan 2025 13:19:43 -0500 +Subject: [PATCH] Apply certmonger_timeout to start_tracking and request_cert + +We've seen that with some slow HSMs the default DBus timeout +the HSM doesn't respond quickly enough to certmonger start +tracking requests which fails the entire installation. + +A first attempt was made to bump up the default to 30 seconds +which turned out to not be long enough. + +There is already a certmonger timeout defined in the API but it +is 300 seconds so I was hesitant to use it. It could delay the +actual failure of a blown install by 5 minutes. But it also gives +the end user the flexibility to be able to control success over +an installation so we'll go ahead and use it. + +Fixes: https://pagure.io/freeipa/issue/9725 + +Signed-off-by: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Mohammad Rizwan Yusuf +Reviewed-By: Florence Blanc-Renaud +--- + client/man/default.conf.5 | 10 ++++++++-- + ipalib/install/certmonger.py | 7 +++++-- + ipaserver/install/cainstance.py | 5 ++++- + ipaserver/install/dogtaginstance.py | 18 ++++++++++++++++++ + ipaserver/install/service.py | 4 +++- + 5 files changed, 38 insertions(+), 6 deletions(-) + +diff --git a/client/man/default.conf.5 b/client/man/default.conf.5 +index 3846de50c5d851471ea3ceed9fc38cb687c719e4..e0aec21f725d88ce2ba3cf52901fb15575892cde 100644 +--- a/client/man/default.conf.5 ++++ b/client/man/default.conf.5 +@@ -20,7 +20,7 @@ + .SH "NAME" + default.conf \- IPA configuration file + .SH "SYNOPSIS" +-/etc/ipa/default.conf, ~/.ipa/default.conf, /etc/ipa/server.conf, /etc/ipa/cli.conf ++/etc/ipa/default.conf, ~/.ipa/default.conf, /etc/ipa/server.conf, /etc/ipa/cli.conf, /etc/ipa/installer.conf, /etc/ipa/cli_installer.conf + .SH "DESCRIPTION" + The \fIdefault.conf \fRconfiguration file is used to set system\-wide defaults to be applied when running IPA clients and servers. + +@@ -75,7 +75,7 @@ Specifies the hostname of the dogtag CA server. The default is the hostname of t + Specifies the insecure CA end user port. The default is 8080. + .TP + .B certmonger_wait_timeout +-The time to wait for a certmonger request to complete during installation. The default value is 300 seconds. ++The time to wait for a certmonger request to complete during installation. The default value is 300 seconds. To tune create/add to /etc/ipa/installer.conf. + .TP + .B context + Specifies the context that IPA is being executed in. IPA may operate differently depending on the context. The current defined contexts are cli, server and dns. Additionally this value is used to load /etc/ipa/\fBcontext\fR.conf to provide context\-specific configuration. For example, if you want to always perform client requests in verbose mode but do not want to have verbose enabled on the server, add the verbose option to \fI/etc/ipa/cli.conf\fR. +@@ -263,6 +263,12 @@ system\-wide IPA client configuration file + .TP + .I /etc/ipa/server.conf + system\-wide IPA server configuration file ++.TP ++.I /etc/ipa/installer.conf ++IPA configuration used while installing an IPA server or replica ++.TP ++.I /etc/ipa/cli_installer.conf ++IPA configuration used while installing an IPA client + .SH "EXAMPLES" + .TP + An example of a context-specific configuration file is \fB/etc/ipa/dns.conf\fR to be used to increase debug output of the IPA DNSSEC daemons. +diff --git a/ipalib/install/certmonger.py b/ipalib/install/certmonger.py +index efc1ba4f42eab98df5fac51bafa3acc83ae91831..675b2c96ce8ebe4f06822ad587a4bca734a1be09 100644 +--- a/ipalib/install/certmonger.py ++++ b/ipalib/install/certmonger.py +@@ -477,7 +477,8 @@ def request_cert( + request_parameters['cert-perms'] = perms[0] + request_parameters['key-perms'] = perms[1] + +- result = cm.obj_if.add_request(request_parameters, timeout=30) ++ result = cm.obj_if.add_request(request_parameters, ++ timeout=api.env.certmonger_wait_timeout) + try: + if result[0]: + request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF, +@@ -581,7 +582,9 @@ def start_tracking( + if nss_user: + params['nss-user'] = nss_user + +- result = cm.obj_if.add_request(params, timeout=30) ++ logger.debug("start tracking %s", params) ++ result = cm.obj_if.add_request(params, ++ timeout=api.env.certmonger_wait_timeout) + try: + if result[0]: + request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF, +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index e03a8c863e14782679e19c6887f5e220131e4234..76718036dbd317651edc98ce631405e42bf814d7 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -513,7 +513,10 @@ class CAInstance(DogtagInstance): + if ra_only: + runtime = None + else: +- runtime = 180 ++ if self.tokenname: ++ runtime = "HSM dependent" ++ else: ++ runtime = 180 + + try: + self.start_creation(runtime=runtime) +diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py +index 32a52dbedaa34b24c2658460f0ae889e7a37aa64..002053ed797beec829d324e80fc55b57cabf04be 100644 +--- a/ipaserver/install/dogtaginstance.py ++++ b/ipaserver/install/dogtaginstance.py +@@ -575,6 +575,24 @@ class DogtagInstance(service.Service): + except RuntimeError as e: + logger.error( + "certmonger failed to start tracking certificate: %s", e) ++ except dbus.exceptions.DBusException as e: ++ if e._dbus_error_name == "org.freedesktop.DBus.Error.NoReply": ++ logger.error( ++ "Timeout encountered starting tracking of '%s'." ++ "This timeout can be tuned using " ++ "certmonger_wait_timeout in /etc/ipa/installer.conf.", ++ nickname ++ ) ++ if self.hsm_enabled: ++ logger.error( ++ "On an initial install failure this may leave " ++ "certificates and keys on the HSM token. These " ++ "need to be manually cleaned per your HSM-specific " ++ "documentation before installing IPA again. On a " ++ "replica install no clean-up should be done (it will " ++ "destroy your installation." ++ ) ++ raise + + def stop_tracking_certificates(self): + """ +diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py +index cf0f64ab9794111761adf735bc488269bd1814fc..7755a4f2ff5e33e61f85dc24b71fd05a1837cd5a 100644 +--- a/ipaserver/install/service.py ++++ b/ipaserver/install/service.py +@@ -59,6 +59,8 @@ def print_msg(message, output_fd=sys.stdout): + + def format_seconds(seconds): + """Format a number of seconds as an English minutes+seconds message""" ++ if type(seconds) is not int: ++ return seconds + parts = [] + minutes, seconds = divmod(seconds, 60) + if minutes: +@@ -660,7 +662,7 @@ class Service: + else: + end_message = "Done configuring %s." % self.service_desc + +- if runtime is not None and runtime > 0: ++ if runtime is not None or (type(runtime) is int and runtime > 0): + self.print_msg('%s. Estimated time: %s' % (start_message, + format_seconds(runtime))) + else: +-- +2.48.1 + diff --git a/SOURCES/0052-Add-DNS-over-TLS-support.patch b/SOURCES/0052-Add-DNS-over-TLS-support.patch new file mode 100644 index 0000000..6264f5c --- /dev/null +++ b/SOURCES/0052-Add-DNS-over-TLS-support.patch @@ -0,0 +1,1447 @@ +From 46ef43b2a68139c991883633137c0061f20222a7 Mon Sep 17 00:00:00 2001 +From: Antonio Torres +Date: Mon, 29 Apr 2024 11:36:52 +0200 +Subject: [PATCH 1/4] Add DNS over TLS support + +Add DNS over TLS support using Unbound as a local resolver. This +includes new options on both server and client side. + +* `--dns-over-tls`: enable DNS over TLS support. This option is present + on both client and server. It deploys Unbound and configures BIND on +the server to receive DoT requests. +* `--dot-forwarder`: the upstream DNS server with DoT support. It must + be specified in the format `1.2.3.4#dns.server.test` +* `--dns-over-tls-key` and `--dns-over-tls-cert`: in case user prefers + to have the DoT certificate in BIND generated by themselves. If these + are empty, IPA CA is used instead to request a new certificate. + +Signed-off-by: Antonio Torres +Reviewed-By: Francisco Trivino +Reviewed-By: Alexander Bokovoy +Reviewed-By: Varun Mylaraiah +Reviewed-By: Pavel Brezina +--- + client/man/ipa-client-install.1 | 3 + + client/share/Makefile.am | 1 + + client/share/unbound.conf.template | 10 ++ + install/share/bind.named.conf.template | 4 + + install/tools/ipa-dns-install.in | 50 +++++- + install/tools/man/ipa-dns-install.1 | 16 ++ + install/tools/man/ipa-replica-install.1 | 16 ++ + install/tools/man/ipa-server-install.1 | 16 ++ + ipaclient/install/client.py | 89 +++++++++- + ipaplatform/base/paths.py | 4 + + ipaplatform/base/services.py | 2 +- + ipapython/ipautil.py | 13 ++ + ipaserver/install/bindinstance.py | 48 +++++- + ipaserver/install/dns.py | 182 ++++++++++++++++++++- + ipaserver/install/server/__init__.py | 60 ++++++- + ipaserver/install/server/install.py | 24 ++- + ipaserver/install/server/replicainstall.py | 2 + + 17 files changed, 507 insertions(+), 33 deletions(-) + create mode 100644 client/share/unbound.conf.template + +diff --git a/client/man/ipa-client-install.1 b/client/man/ipa-client-install.1 +index 725b11422..e6f641254 100644 +--- a/client/man/ipa-client-install.1 ++++ b/client/man/ipa-client-install.1 +@@ -201,6 +201,9 @@ Use \fIIP_ADDRESS\fR in DNS A/AAAA record for this host. May be specified multip + .TP + \fB\-\-all\-ip\-addresses\fR + Create DNS A/AAAA record for each IP address on this host. ++.TP ++\fB\-\-dns\-over\-tls\fR ++Configure DNS over TLS. + + .SS "SSSD OPTIONS" + .TP +diff --git a/client/share/Makefile.am b/client/share/Makefile.am +index bf631a22f..52f3c4dd4 100644 +--- a/client/share/Makefile.am ++++ b/client/share/Makefile.am +@@ -5,6 +5,7 @@ dist_app_DATA = \ + freeipa.template \ + sshd_ipa.conf.template \ + ssh_ipa.conf.template \ ++ unbound.conf.template \ + $(NULL) + + epnconfdir = $(IPA_SYSCONF_DIR) +diff --git a/client/share/unbound.conf.template b/client/share/unbound.conf.template +new file mode 100644 +index 000000000..166036f65 +--- /dev/null ++++ b/client/share/unbound.conf.template +@@ -0,0 +1,10 @@ ++server: ++ tls-cert-bundle: $TLS_CERT_BUNDLE_PATH ++ tls-upstream: yes ++ interface: 127.0.0.55 ++ log-servfail: yes ++forward-zone: ++ name: "." ++ forward-tls-upstream: yes ++ forward-first: no ++ $FORWARD_ADDRS +diff --git a/install/share/bind.named.conf.template b/install/share/bind.named.conf.template +index 01b77c5ae..b64a6a4b0 100644 +--- a/install/share/bind.named.conf.template ++++ b/install/share/bind.named.conf.template +@@ -21,6 +21,8 @@ options { + + managed-keys-directory "$MANAGED_KEYS_DIR"; + ++ $NAMED_DNS_OVER_TLS_OPTIONS_CONF ++ + /* user customizations of options */ + include "$NAMED_CUSTOM_OPTIONS_CONF"; + +@@ -49,6 +51,8 @@ ${NAMED_ZONE_COMMENT}}; + include "$RFC1912_ZONES"; + include "$ROOT_KEY"; + ++$NAMED_DNS_OVER_TLS_CONF ++ + /* user customization */ + include "$NAMED_CUSTOM_CONF"; + +diff --git a/install/tools/ipa-dns-install.in b/install/tools/ipa-dns-install.in +index f1a90e7ac..0b0cd2be5 100644 +--- a/install/tools/ipa-dns-install.in ++++ b/install/tools/ipa-dns-install.in +@@ -27,6 +27,7 @@ import sys + + from ipaserver.install import bindinstance + from ipaserver.install import installutils ++from ipaplatform import services + from ipapython import version + from ipalib import api + from ipaplatform.paths import paths +@@ -56,6 +57,24 @@ def parse_options(): + parser.add_option("--auto-forwarders", dest="auto_forwarders", + action="store_true", default=False, + help="Use DNS forwarders configured in /etc/resolv.conf") ++ parser.add_option("--dns-over-tls", dest="dns_over_tls", ++ help="Configure DNS over TLS", default=False, ++ action="store_true") ++ parser.add_option("--dot-forwarder", dest="dot_forwarders", ++ action="append", ++ default=[], ++ help="Add a DNS over TLS forwarder. " ++ "This option can be used multiple times") ++ parser.add_option("--dns-over-tls-cert", dest="dns_over_tls_cert", ++ help="Certificate to use for DNS over TLS. " ++ "If empty, a new certificate will be requested " ++ "from IPA CA") ++ parser.add_option("--dns-over-tls-key", dest="dns_over_tls_key", ++ help="Key for certificate specified " ++ "in --dns-over-tls-cert") ++ parser.add_option("--dns-policy", dest="dns_policy", ++ choices=("relaxed", "enforced"), default="relaxed", ++ help="Policy for encrypted DNS enforcement") + parser.add_option("--forward-policy", dest="forward_policy", + choices=("first", "only"), default=None, + help="DNS forwarding policy for global forwarders") +@@ -102,12 +121,37 @@ def parse_options(): + elif options.auto_reverse and options.no_reverse: + parser.error("You cannot specify a --auto-reverse option together with --no-reverse") + ++ if options.dns_policy == "enforced" and not options.dns_over_tls: ++ parser.error("If encrypted DNS policy is enforced, " ++ "--dns-over-tls must be specified.") ++ ++ unbound = services.knownservices["unbound"] ++ if options.dns_over_tls and not unbound.is_installed(): ++ parser.error( ++ "To enable DNS over TLS, package ipa-server-encrypted-dns " ++ "must be installed." ++ ) ++ ++ if not options.dns_over_tls and options.dot_forwarders: ++ parser.error("You cannot specify a " ++ "--dot-forwarder option without --dns-over-tls") ++ elif options.dns_over_tls and not options.dot_forwarders: ++ parser.error( ++ "You must specify --dot-forwarder " ++ "when enabling DNS over TLS") ++ elif bool(options.dns_over_tls_key) != bool(options.dns_over_tls_cert): ++ parser.error( ++ "You cannot specify a --dns-over-tls-key option " ++ "without the --dns-over-tls-cert option and vice versa") ++ + if options.unattended: + if (not options.forwarders +- and not options.no_forwarders +- and not options.auto_forwarders): ++ and not options.no_forwarders ++ and not options.auto_forwarders ++ and not options.dot_forwarders): + parser.error("You must specify at least one option: " +- "--forwarder or --no-forwarders or --auto-forwarders") ++ "--forwarder or --no-forwarders or --auto-forwarders" ++ " or --dot-forwarder") + + if options.kasp_db_file and not os.path.isfile(options.kasp_db_file): + parser.error("File %s does not exist" % options.kasp_db_file) +diff --git a/install/tools/man/ipa-dns-install.1 b/install/tools/man/ipa-dns-install.1 +index 029001eca..6008d2028 100644 +--- a/install/tools/man/ipa-dns-install.1 ++++ b/install/tools/man/ipa-dns-install.1 +@@ -69,6 +69,22 @@ Allow creatin of (reverse) zone even if the zone is already resolvable. Using th + .TP + \fB\-U\fR, \fB\-\-unattended\fR + An unattended installation that will never prompt for user input ++.TP ++\fB\-\-dns\-over\-tls\fR ++Configure DNS over TLS. ++.TP ++\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR ++Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. ++.TP ++\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR ++Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. ++.TP ++\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR ++Key for the certificate specified in --dns-over-tls-key. ++.TP ++\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR ++Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, ++unencrypted DNS queries will be allowed. + .SH "DEPRECATED OPTIONS" + .TP + \fB\-p\fR \fIDM_PASSWORD\fR, \fB\-\-ds\-password\fR=\fIDM_PASSWORD\fR +diff --git a/install/tools/man/ipa-replica-install.1 b/install/tools/man/ipa-replica-install.1 +index 3b1d25ba6..c55d21253 100644 +--- a/install/tools/man/ipa-replica-install.1 ++++ b/install/tools/man/ipa-replica-install.1 +@@ -223,6 +223,22 @@ Do not automatically create DNS SSHFP records. + .TP + \fB\-\-no\-dnssec\-validation\fR + Disable DNSSEC validation on this server. ++.TP ++\fB\-\-dns\-over\-tls\fR ++Configure DNS over TLS. ++.TP ++\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR ++Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. ++.TP ++\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR ++Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. ++.TP ++\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR ++Key for the certificate specified in --dns-over-tls-key. ++.TP ++\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR ++Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, ++unencrypted DNS queries will be allowed. + + .SS "SID GENERATION OPTIONS" + .TP +diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1 +index 215a77d6b..84d82531c 100644 +--- a/install/tools/man/ipa-server-install.1 ++++ b/install/tools/man/ipa-server-install.1 +@@ -252,6 +252,22 @@ Disable DNSSEC validation on this server. + .TP + \fB\-\-allow\-zone\-overlap\fR + Allow creation of (reverse) zone even if the zone is already resolvable. Using this option is discouraged as it result in later problems with domain name resolution. ++.TP ++\fB\-\-dns\-over\-tls\fR ++Configure DNS over TLS. ++.TP ++\fB\-\-dot\-forwarder\fR=\fIIP_ADDRESS#HOSTNAME\fR ++Add a DNS-over-TLS-enabled forwarder in the format of ip#hostname, e.g.: dns.example.com#1.2.3.4. This option can be used multiple times. ++.TP ++\fB\-\-dns\-over\-tls\-cert\fR=\fIFILE\fR ++Certificate to use for DNS over TLS. If empty, a new certificate will be requested from IPA CA. ++.TP ++\fB\-\-dns\-over\-tls\-key\fR=\fIFILE\fR ++Key for the certificate specified in --dns-over-tls-key. ++.TP ++\fB\-\-dns\-policy\fR=\fIrelaxed|enforced\fR ++Encrypted DNS policy. If enforced, DNS communications will only be allowed through the configured encrypted DNS methods. If relaxed, ++unencrypted DNS queries will be allowed. + + .SS "SID GENERATION OPTIONS" + .TP +diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py +index 47a371f62..9e4d3bbe7 100644 +--- a/ipaclient/install/client.py ++++ b/ipaclient/install/client.py +@@ -1002,6 +1002,11 @@ def configure_sssd_conf( + + if options.dns_updates: + domain.set_option('dyndns_update', True) ++ if options.dns_over_tls: ++ server_ip = str(list(dnsutil.resolve_ip_addresses( ++ cli_server[0]))[0]) ++ domain.set_option('dyndns_server', 'dns+tls://{}:853#{}' ++ .format(server_ip, cli_server[0])) + if options.all_ip_addresses: + domain.set_option('dyndns_iface', '*') + else: +@@ -1409,8 +1414,9 @@ def get_local_ipaddresses(iface=None): + return ips + + +-def do_nsupdate(update_txt): ++def do_nsupdate(update_txt, options, server): + logger.debug("Writing nsupdate commands to %s:", UPDATE_FILE) ++ + logger.debug("%s", update_txt) + + with open(UPDATE_FILE, "w") as f: +@@ -1418,12 +1424,20 @@ def do_nsupdate(update_txt): + + result = False + try: +- ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE]) ++ if options.dns_over_tls: ++ ipautil.run([paths.NSUPDATE, '-p', '853', '-S', ++ '-H', server, '-g', UPDATE_FILE]) ++ else: ++ ipautil.run([paths.NSUPDATE, '-g', UPDATE_FILE]) + result = True + except CalledProcessError as e: + logger.debug('nsupdate (GSS-TSIG) failed: %s', str(e)) + try: +- ipautil.run([paths.NSUPDATE, UPDATE_FILE]) ++ if options.dns_over_tls: ++ ipautil.run([paths.NSUPDATE, '-p', '853', '-S', ++ '-H', server, '-g', UPDATE_FILE]) ++ else: ++ ipautil.run([paths.NSUPDATE, UPDATE_FILE]) + try: + sssdconfig = SSSDConfig.SSSDConfig() + sssdconfig.import_config() +@@ -1525,6 +1539,8 @@ def update_dns(server, hostname, options): + no_matching_interface_for_ip_address_warning(update_ips) + + update_txt = "debug\n" ++ if options.dns_over_tls: ++ update_txt += "server %s 853" % server + update_txt += ipautil.template_str(DELETE_TEMPLATE_A, + dict(HOSTNAME=hostname)) + update_txt += ipautil.template_str(DELETE_TEMPLATE_AAAA, +@@ -1538,7 +1554,7 @@ def update_dns(server, hostname, options): + template = ADD_TEMPLATE_AAAA + update_txt += ipautil.template_str(template, sub_dict) + +- if not do_nsupdate(update_txt): ++ if not do_nsupdate(update_txt, options, server): + logger.error("Failed to update DNS records.") + verify_dns_update(hostname, update_ips) + +@@ -1654,6 +1670,54 @@ def client_dns(server, hostname, options): + hostname, ex) + dns_ok = False + ++ # Setup DNS over TLS ++ if options.dns_over_tls: ++ # setup and enable Unbound as resolver ++ server_ip = str(list(dnsutil.resolve_ip_addresses(server))[0]) ++ forward_addr = "forward-addr: %s#%s" % (server_ip, server) ++ ipautil.copy_template_file( ++ paths.UNBOUND_CONF_SRC, ++ paths.UNBOUND_CONF, ++ dict( ++ TLS_CERT_BUNDLE_PATH=os.path.join( ++ paths.OPENSSL_CERTS_DIR, "ca-bundle.crt"), ++ FORWARD_ADDRS=forward_addr ++ ) ++ ) ++ sr = services.knownservices["systemd-resolved"] ++ if sr.is_running(): ++ sr.stop() ++ sr.disable() ++ ++ nm = services.knownservices["NetworkManager"] ++ if nm.is_enabled(): ++ with open(paths.NETWORK_MANAGER_IPA_CONF, "w") as f: ++ dns_none = [ ++ "# auto-generated by IPA installer", ++ "[main]", ++ "dns=none\n" ++ ] ++ f.write("\n".join(dns_none)) ++ nm.reload_or_restart() ++ ++ # Overwrite resolv.conf to point to Unbound ++ cfg = [ ++ "# auto-generated by IPA installer", ++ "search .", ++ "nameserver 127.0.0.55\n" ++ ] ++ fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) ++ fstore.backup_file(paths.RESOLV_CONF) ++ with open(paths.RESOLV_CONF, 'w') as f: ++ f.write('\n'.join(cfg)) ++ os.chmod(paths.RESOLV_CONF, 0o644) ++ ++ services.knownservices.unbound.enable() ++ services.knownservices.unbound.restart() ++ logger.info("DNS encryption support was enabled. " ++ "Unbound is configured to listen on 127.0.0.55:53 and " ++ "forward to upstream DoT servers.") ++ + if ( + options.dns_updates or options.all_ip_addresses or + options.ip_addresses or not dns_ok +@@ -1661,6 +1725,7 @@ def client_dns(server, hostname, options): + update_dns(server, hostname, options) + + ++ + def check_ip_addresses(options): + if options.ip_addresses: + for ip in options.ip_addresses: +@@ -1672,7 +1737,7 @@ def check_ip_addresses(options): + return True + + +-def update_ssh_keys(hostname, ssh_dir, create_sshfp): ++def update_ssh_keys(hostname, ssh_dir, options, server): + if not os.path.isdir(ssh_dir): + return + +@@ -1718,10 +1783,12 @@ def update_ssh_keys(hostname, ssh_dir, create_sshfp): + logger.warning("Failed to upload host SSH public keys.") + return + +- if create_sshfp: ++ if options.create_sshfp: + ttl = 1200 + + update_txt = 'debug\n' ++ if options.dns_over_tls: ++ update_txt += "server %s 853" % server + update_txt += 'update delete %s. IN SSHFP\nshow\nsend\n' % hostname + for pubkey in pubkeys: + sshfp = pubkey.fingerprint_dns_sha1() +@@ -1734,7 +1801,7 @@ def update_ssh_keys(hostname, ssh_dir, create_sshfp): + hostname, ttl, sshfp) + update_txt += 'show\nsend\n' + +- if not do_nsupdate(update_txt): ++ if not do_nsupdate(update_txt, options, server): + logger.warning("Could not update DNS SSHFP records.") + + +@@ -3163,7 +3230,7 @@ def _install(options, tdict): + if not options.on_master: + client_dns(cli_server[0], hostname, options) + +- update_ssh_keys(hostname, paths.SSH_CONFIG_DIR, options.create_sshfp) ++ update_ssh_keys(hostname, paths.SSH_CONFIG_DIR, options, cli_server[0]) + + try: + os.remove(CCACHE_FILE) +@@ -3988,6 +4055,12 @@ class ClientInstallInterface(hostname_.HostNameInstallInterface, + if value < 1: + raise ValueError("expects an integer greater than 0.") + ++ dns_over_tls = knob( ++ None, ++ description="Configure DNS over TLS", ++ ) ++ dns_over_tls = enroll_only(dns_over_tls) ++ + request_cert = knob( + None, + deprecated=True, +diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py +index b339d2202..b2da94992 100644 +--- a/ipaplatform/base/paths.py ++++ b/ipaplatform/base/paths.py +@@ -102,6 +102,8 @@ class BasePathNamespace: + NAMED_ROOT_KEY = "/etc/named.root.key" + NAMED_MANAGED_KEYS_DIR = "/var/named/dynamic" + NAMED_CRYPTO_POLICY_FILE = None ++ UNBOUND_CONF_SRC = '/usr/share/ipa/client/unbound.conf.template' ++ UNBOUND_CONF = "/etc/unbound/conf.d/zzz-ipa.conf" + NSLCD_CONF = "/etc/nslcd.conf" + NSS_LDAP_CONF = "/etc/nss_ldap.conf" + NSSWITCH_CONF = "/etc/nsswitch.conf" +@@ -225,6 +227,8 @@ class BasePathNamespace: + OPENSSL_DIR = "/etc/pki/tls" + OPENSSL_CERTS_DIR = "/etc/pki/tls/certs" + OPENSSL_PRIVATE_DIR = "/etc/pki/tls/private" ++ BIND_DNS_OVER_TLS_CRT = "/etc/pki/tls/certs/bind_dot.crt" ++ BIND_DNS_OVER_TLS_KEY = "/etc/pki/tls/private/bind_dot.key" + PK12UTIL = "/usr/bin/pk12util" + SOFTHSM2_UTIL = "/usr/bin/softhsm2-util" + SSLGET = "/usr/bin/sslget" +diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py +index 275422e30..fcb626aa3 100644 +--- a/ipaplatform/base/services.py ++++ b/ipaplatform/base/services.py +@@ -53,7 +53,7 @@ wellknownservices = [ + 'named', 'ods_enforcerd', 'ods_signerd', 'gssproxy', + 'nfs-utils', 'sssd', 'NetworkManager', 'ipa-custodia', + 'ipa-dnskeysyncd', 'ipa-otpd', 'ipa-ods-exporter', +- 'systemd-resolved', ++ 'systemd-resolved', 'unbound', + ] + + # The common ports for these services. This is used to wait for the +diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py +index c237d59fb..681f60655 100644 +--- a/ipapython/ipautil.py ++++ b/ipapython/ipautil.py +@@ -266,6 +266,19 @@ class CheckedIPAddressLoopback(CheckedIPAddress): + file=sys.stderr) + + ++class IPAddressDoTForwarder(str): ++ """IPv4 or IPv6 address with added hostname as needed for DNS over TLS ++ configuration. Example: 1.2.3.4#dns.hostname.test ++ """ ++ def __init__(self, addr): ++ addr_split = addr.split("#") ++ if len(addr_split) != 2 or not valid_ip(addr_split[0]): ++ raise ValueError( ++ "DoT forwarder must be in the format " ++ "of '1.2.3.4#dns.example.test'." ++ ) ++ ++ + def valid_ip(addr): + return netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr) + +diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py +index 939f5f21f..4f4ab9bbc 100644 +--- a/ipaserver/install/bindinstance.py ++++ b/ipaserver/install/bindinstance.py +@@ -28,6 +28,7 @@ import re + import shutil + import sys + import time ++import textwrap + + import ldap + import six +@@ -50,7 +51,7 @@ from ipapython.admintool import ScriptError + import ipalib + from ipalib import api, errors + from ipalib.constants import IPA_CA_RECORD +-from ipalib.install import dnsforwarders ++from ipalib.install import dnsforwarders, certmonger + from ipaplatform import services + from ipaplatform.tasks import tasks + from ipaplatform.constants import constants +@@ -668,14 +669,20 @@ class BindInstance(service.Service): + + def setup(self, fqdn, ip_addresses, realm_name, domain_name, forwarders, + forward_policy, reverse_zones, zonemgr=None, +- no_dnssec_validation=False): ++ no_dnssec_validation=False, dns_over_tls=False, ++ dns_over_tls_cert=None, dns_over_tls_key=None, ++ dns_policy=None): + """Setup bindinstance for installation + """ + self.setup_templating( + fqdn=fqdn, + realm_name=realm_name, + domain_name=domain_name, +- no_dnssec_validation=no_dnssec_validation ++ no_dnssec_validation=no_dnssec_validation, ++ dns_over_tls=dns_over_tls, ++ dns_over_tls_cert=dns_over_tls_cert, ++ dns_over_tls_key=dns_over_tls_key, ++ dns_policy=dns_policy + ) + self.ip_addresses = ip_addresses + self.forwarders = forwarders +@@ -688,7 +695,9 @@ class BindInstance(service.Service): + self.zonemgr = normalize_zonemgr(zonemgr) + + def setup_templating( +- self, fqdn, realm_name, domain_name, no_dnssec_validation=None ++ self, fqdn, realm_name, domain_name, no_dnssec_validation=None, ++ dns_over_tls=None, dns_over_tls_cert=None, dns_over_tls_key=None, ++ dns_policy=None + ): + """Setup bindinstance for templating + """ +@@ -698,6 +707,10 @@ class BindInstance(service.Service): + self.host = fqdn.split(".")[0] + self.suffix = ipautil.realm_to_suffix(self.realm) + self.no_dnssec_validation = no_dnssec_validation ++ self.dns_over_tls = dns_over_tls ++ self.dns_over_tls_cert = dns_over_tls_cert ++ self.dns_over_tls_key = dns_over_tls_key ++ self.dns_policy = dns_policy + self._setup_sub_dict() + + @property +@@ -872,6 +885,24 @@ class BindInstance(service.Service): + else: + crypto_policy = "// not available" + ++ if self.dns_over_tls: ++ named_tls_conf = textwrap.dedent("""\ ++ tls local-tls {{ ++ \tkey-file "{}"; ++ \tcert-file "{}"; ++ }}; ++ """).format(self.dns_over_tls_key, self.dns_over_tls_cert) ++ unencrypted_iface = ("127.0.0.1" if self.dns_policy == "enforced" ++ else "any") ++ named_tls_options = textwrap.dedent("""\ ++ \tlisten-on { %s; }; ++ \tlisten-on tls local-tls { any; }; ++ \tlisten-on-v6 tls local-tls { any; }; ++ """ % unencrypted_iface) ++ else: ++ named_tls_options = "" ++ named_tls_conf = "" ++ + self.sub_dict = dict( + FQDN=self.fqdn, + SERVER_ID=ipaldap.realm_to_serverid(self.realm), +@@ -891,6 +922,8 @@ class BindInstance(service.Service): + NAMED_DATA_DIR=constants.NAMED_DATA_DIR, + NAMED_ZONE_COMMENT=constants.NAMED_ZONE_COMMENT, + NAMED_DNSSEC_VALIDATION=self._get_dnssec_validation(), ++ NAMED_DNS_OVER_TLS_OPTIONS_CONF=named_tls_options, ++ NAMED_DNS_OVER_TLS_CONF=named_tls_conf, + ) + + def __setup_dns_container(self): +@@ -1344,6 +1377,11 @@ class BindInstance(service.Service): + + self.named_conflict.unmask() + ++ certmonger.stop_tracking(certfile=paths.BIND_DNS_OVER_TLS_CRT) ++ certmonger.stop_tracking(certfile=paths.BIND_DNS_OVER_TLS_KEY) ++ services.knownservices.unbound.disable() ++ services.knownservices.unbound.stop() ++ + ipautil.remove_file(paths.NAMED_CONF_BAK) + ipautil.remove_file(paths.NAMED_CUSTOM_CONF) + ipautil.remove_file(paths.NAMED_CUSTOM_OPTIONS_CONF) +@@ -1357,6 +1395,8 @@ class BindInstance(service.Service): + pass + except ValueError: + pass ++ ipautil.remove_file(paths.BIND_DNS_OVER_TLS_CRT) ++ ipautil.remove_file(paths.BIND_DNS_OVER_TLS_KEY) + ipautil.remove_keytab(self.keytab) + + ipautil.remove_ccache(run_as=self.service_user) +diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py +index 47d79af9c..29ca0d2ff 100644 +--- a/ipaserver/install/dns.py ++++ b/ipaserver/install/dns.py +@@ -20,14 +20,18 @@ from subprocess import CalledProcessError + from ipalib import api + from ipalib import errors + from ipalib import util +-from ipalib.install import hostname, sysrestore ++from ipalib import x509 ++from ipalib.install import hostname, sysrestore, certmonger + from ipalib.install.service import enroll_only, prepare_only + from ipalib.install import dnsforwarders ++from ipalib.constants import FQDN + from ipaplatform.paths import paths + from ipaplatform.constants import constants + from ipaplatform import services ++from ipapython import admintool + from ipapython import ipautil + from ipapython import dnsutil ++from ipapython.certdb import EXTERNAL_CA_TRUST_FLAGS + from ipapython.dn import DN + from ipapython.dnsutil import check_zone_overlap + from ipapython.install import typing +@@ -37,7 +41,9 @@ from ipapython.ipautil import user_input + from ipaserver.install.installutils import get_server_ip_address + from ipaserver.install.installutils import read_dns_forwarders + from ipaserver.install.installutils import update_hosts_file ++from ipaserver.install.installutils import default_subject_base + from ipaserver.install import bindinstance ++from ipaserver.install import certs + from ipaserver.install import dnskeysyncinstance + from ipaserver.install import odsexporterinstance + from ipaserver.install import opendnssecinstance +@@ -108,6 +114,73 @@ def _disable_dnssec(): + conn.update_entry(entry) + + ++def _setup_dns_over_tls(options): ++ if os.path.isfile(paths.IPA_CA_CRT) and not options.dns_over_tls_cert: ++ # request certificate for DNS over TLS, using IPA CA ++ cert = paths.BIND_DNS_OVER_TLS_CRT ++ key = paths.BIND_DNS_OVER_TLS_KEY ++ certmonger.request_and_wait_for_cert( ++ certpath=(cert, key), ++ principal='DNS/%s@%s' % (FQDN, api.env.realm), ++ subject=str(DN(('CN', FQDN), default_subject_base(api.env.realm))), ++ storage="FILE" ++ ) ++ constants.NAMED_USER.chown(cert, gid=constants.NAMED_GROUP.gid) ++ constants.NAMED_USER.chown(key, gid=constants.NAMED_GROUP.gid) ++ ++ # setup and enable Unbound as resolver ++ forward_addrs = ["# forward-addr: specify here forwarders"] ++ if options.dot_forwarders: ++ forward_addrs = ["forward-addr: %s" % fw ++ for fw in options.dot_forwarders] ++ ipautil.copy_template_file( ++ paths.UNBOUND_CONF_SRC, ++ paths.UNBOUND_CONF, ++ dict( ++ TLS_CERT_BUNDLE_PATH=os.path.join( ++ paths.OPENSSL_CERTS_DIR, "ca-bundle.crt"), ++ FORWARD_ADDRS="\n".join(forward_addrs) ++ ) ++ ) ++ ++ sr = services.knownservices["systemd-resolved"] ++ if sr.is_running(): ++ sr.stop() ++ sr.disable() ++ ++ api.Command.dnsserver_mod( ++ FQDN, ++ idnsforwarders="127.0.0.55", ++ idnsforwardpolicy="first" ++ ) ++ ++ nm = services.knownservices["NetworkManager"] ++ if nm.is_enabled(): ++ with open(paths.NETWORK_MANAGER_IPA_CONF, "w") as f: ++ dns_none = [ ++ "# auto-generated by IPA installer", ++ "[main]", ++ "dns=none\n" ++ ] ++ f.write("\n".join(dns_none)) ++ nm.reload_or_restart() ++ ++ # Overwrite resolv.conf to point to IPA ++ cfg = [ ++ "# auto-generated by IPA installer", ++ "search .", ++ "nameserver 127.0.0.1\n" ++ ] ++ fstore = sysrestore.FileStore(paths.SYSRESTORE) ++ fstore.backup_file(paths.RESOLV_CONF) ++ with open(paths.RESOLV_CONF, 'w') as f: ++ f.write('\n'.join(cfg)) ++ os.chmod(paths.RESOLV_CONF, 0o644) ++ ++ services.knownservices.unbound.enable() ++ services.knownservices.unbound.restart() ++ ++ + def package_check(exception): + if not os.path.isfile(paths.IPA_DNS_INSTALL): + raise exception( +@@ -287,9 +360,14 @@ def install_check(standalone, api, replica, options, hostname): + + if options.no_forwarders: + options.forwarders = [] +- elif options.forwarders or options.auto_forwarders: ++ elif (options.forwarders ++ or options.dot_forwarders or options.auto_forwarders): + if not options.forwarders: +- options.forwarders = [] ++ if options.dot_forwarders: ++ options.forwarders = [fw.split("#")[0] ++ for fw in options.dot_forwarders] ++ else: ++ options.forwarders = [] + if options.auto_forwarders: + options.forwarders.extend(dnsforwarders.get_nameservers()) + elif standalone or not replica: +@@ -330,11 +408,46 @@ def install(standalone, replica, options, api=api): + # otherwise this is done by server/replica installer + update_hosts_file(ip_addresses, api.env.host, fstore) + ++ if os.path.isfile(paths.IPA_CA_CRT) and not options.dns_over_tls_cert: ++ dot_cert = paths.BIND_DNS_OVER_TLS_CRT ++ dot_key = paths.BIND_DNS_OVER_TLS_KEY ++ elif options.dns_over_tls_cert and options.dns_over_tls_key: ++ # Check certificate validity first ++ with certs.NSSDatabase() as tmpdb: ++ tmpdb.create_db() ++ ca_certs = x509.load_certificate_list_from_file( ++ options.dns_over_tls_cert) ++ nicknames = [] ++ for ca_cert in ca_certs: ++ nicknames.append(str(DN(ca_cert.subject))) ++ tmpdb.add_cert( ++ ca_cert, str(DN(ca_cert.subject)), EXTERNAL_CA_TRUST_FLAGS) ++ try: ++ for nick in nicknames: ++ tmpdb.verify_ca_cert_validity(nick) ++ except ValueError as e: ++ raise admintool.ScriptError( ++ "Not a valid CA certificate: %s" % e) ++ dot_cert = options.dns_over_tls_cert ++ dot_key = options.dns_over_tls_key ++ else: ++ raise RuntimeError( ++ "Certificate for DNS over TLS not specified " ++ "and IPA CA is not present." ++ ) ++ ++ if not options.forwarders and options.dot_forwarders: ++ options.forwaders = [fw.split("#")[0] for fw in options.dot_forwarders] ++ + bind = bindinstance.BindInstance(fstore, api=api) + bind.setup(api.env.host, ip_addresses, api.env.realm, api.env.domain, + options.forwarders, options.forward_policy, + reverse_zones, zonemgr=options.zonemgr, +- no_dnssec_validation=options.no_dnssec_validation) ++ no_dnssec_validation=options.no_dnssec_validation, ++ dns_over_tls=options.dns_over_tls, ++ dns_over_tls_cert=dot_cert, ++ dns_over_tls_key=dot_key, ++ dns_policy=options.dns_policy) + + if standalone and not options.unattended: + print("") +@@ -343,6 +456,11 @@ def install(standalone, replica, options, api=api): + print("") + + bind.create_instance() ++ ++ if options.dns_over_tls: ++ print("Setting up DNS over TLS") ++ _setup_dns_over_tls(options) ++ + print("Restarting the web server to pick up resolv.conf changes") + services.knownservices.httpd.restart(capture_output=True) + +@@ -370,6 +488,12 @@ def install(standalone, replica, options, api=api): + bind.update_system_records() + + if standalone: ++ if options.dns_over_tls and options.dns_policy == "enforced": ++ dns_port = "853" ++ elif options.dns_over_tls: ++ dns_port = "53, 853" ++ else: ++ dns_port = "53" + print("==============================================================================") + print("Setup complete") + print("") +@@ -378,14 +502,22 @@ def install(standalone, replica, options, api=api): + print("") + print("\tYou must make sure these network ports are open:") + print("\t\tTCP Ports:") +- print("\t\t * 53: bind") ++ print(f"\t\t * {dns_port}: bind") + print("\t\tUDP Ports:") +- print("\t\t * 53: bind") ++ print(f"\t\t * {dns_port}: bind") + elif not standalone and replica: + print("") + bind.check_global_configuration() + print("") + ++ if options.dns_over_tls: ++ policy = "enforced" if options.dns_policy == "enforced" else "relaxed" ++ print("") ++ print(("DNS encryption support was enabled " ++ "with policy '{}'.".format(policy))) ++ print(("Unbound is configured to listen on 127.0.0.55:53 and " ++ "forward to upstream DoT servers.")) ++ + + def uninstall_check(options): + # test if server is DNSSEC key master +@@ -424,6 +556,10 @@ class DNSForwardPolicy(enum.Enum): + FIRST = 'first' + + ++class EncryptedDNSPolicy(enum.Enum): ++ RELAXED = 'relaxed' ++ ENFORCED = 'enforced' ++ + @group + class DNSInstallInterface(hostname.HostNameInstallInterface): + """ +@@ -536,6 +672,40 @@ class DNSInstallInterface(hostname.HostNameInstallInterface): + ) + no_dnssec_validation = enroll_only(no_dnssec_validation) + ++ dns_over_tls = knob( ++ None, ++ description="Configure DNS over TLS", ++ ) ++ dns_over_tls = enroll_only(dns_over_tls) ++ ++ dot_forwarders = knob( ++ typing.List[ipautil.IPAddressDoTForwarder], None, ++ description=("Add a DNS over TLS forwarder. " ++ "This option can be used multiple times"), ++ cli_names='--dot-forwarder', ++ ) ++ dot_forwarders = enroll_only(dot_forwarders) ++ ++ dns_over_tls_cert = knob( ++ str, None, ++ description=("Certificate to use for DNS over TLS. " ++ "If empty, a new certificate will be " ++ "requested from IPA CA"), ++ ) ++ dns_over_tls_cert = enroll_only(dns_over_tls_cert) ++ ++ dns_over_tls_key = knob( ++ str, None, ++ description="Key for certificate specified in --dns-over-tls-cert", ++ ) ++ dns_over_tls_key = enroll_only(dns_over_tls_key) ++ ++ dns_policy = knob( ++ EncryptedDNSPolicy, 'relaxed', ++ description=("Encrypted DNS policy"), ++ ) ++ dns_policy = enroll_only(dns_policy) ++ + dnssec_master = False + disable_dnssec_master = False + kasp_db_file = None +diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py +index 857b08f9f..c6a88585a 100644 +--- a/ipaserver/install/server/__init__.py ++++ b/ipaserver/install/server/__init__.py +@@ -21,6 +21,7 @@ from ipalib.install.service import (enroll_only, + from ipapython.install import typing + from ipapython.install.core import group, knob, extend_knob + from ipapython.install.common import step ++from ipaplatform import services + + from .install import validate_admin_password, validate_dm_password + from .install import get_min_idstart +@@ -442,6 +443,18 @@ class ServerInstallInterface(ServerCertificateInstallInterface, + raise RuntimeError( + "You cannot specify a --no-dnssec-validation option " + "without the --setup-dns option") ++ if self.dot_forwarders: ++ raise RuntimeError( ++ "You cannot specify a --dot-forwarder option " ++ "without the --setup-dns option") ++ if self.dns_over_tls_cert: ++ raise RuntimeError( ++ "You cannot specify a --dns-over-tls-cert option " ++ "without the --setup-dns option") ++ if self.dns_over_tls_key: ++ raise RuntimeError( ++ "You cannot specify a --dns-over-tls-key option " ++ "without the --setup-dns option") + elif self.forwarders and self.no_forwarders: + raise RuntimeError( + "You cannot specify a --forwarder option together with " +@@ -458,7 +471,32 @@ class ServerInstallInterface(ServerCertificateInstallInterface, + raise RuntimeError( + "You cannot specify a --auto-reverse option together with " + "--no-reverse") +- ++ elif self.dot_forwarders and not self.dns_over_tls: ++ raise RuntimeError( ++ "You cannot specify a --dot-forwarder option " ++ "without the --dns-over-tls option") ++ elif (self.dns_over_tls ++ and not services.knownservices["unbound"].is_installed()): ++ raise RuntimeError( ++ "To enable DNS over TLS, package ipa-server-encrypted-dns " ++ "must be installed." ++ ) ++ elif self.dns_policy == "enforced" and not self.dns_over_tls: ++ raise RuntimeError( ++ "You cannot specify a --dns-policy option " ++ "without the --dns-over-tls option") ++ elif self.dns_over_tls_cert and not self.dns_over_tls: ++ raise RuntimeError( ++ "You cannot specify a --dns-over-tls-cert option " ++ "without the --dns-over-tls option") ++ elif self.dns_over_tls_key and not self.dns_over_tls: ++ raise RuntimeError( ++ "You cannot specify a --dns-over-tls-key option " ++ "without the --dns-over-tls option") ++ elif bool(self.dns_over_tls_key) != bool(self.dns_over_tls_cert): ++ raise RuntimeError( ++ "You cannot specify a --dns-over-tls-key option " ++ "without the --dns-over-tls-cert option and vice versa") + if not self.setup_adtrust: + if self.add_agents: + raise RuntimeError( +@@ -504,12 +542,18 @@ class ServerInstallInterface(ServerCertificateInstallInterface, + "In unattended mode you need to provide at least -r, " + "-p and -a options") + if self.setup_dns: +- if (not self.forwarders and +- not self.no_forwarders and +- not self.auto_forwarders): ++ if (not self.forwarders ++ and not self.no_forwarders ++ and not self.auto_forwarders ++ and not self.dot_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " +- "--auto-forwarders, or --no-forwarders options") ++ "--auto-forwarders, --dot-forwarder or " ++ "--no-forwarders options") ++ elif self.dns_over_tls and not self.dot_forwarders: ++ raise RuntimeError( ++ "You must specify --dot-forwarder " ++ "when enabling DNS over TLS") + + any_ignore_option_true = any( + [self.ignore_topology_disconnect, self.ignore_last_of_role]) +@@ -541,10 +585,12 @@ class ServerInstallInterface(ServerCertificateInstallInterface, + if self.setup_dns: + if (not self.forwarders and + not self.no_forwarders and +- not self.auto_forwarders): ++ not self.auto_forwarders ++ and not self.dot_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " +- "--auto-forwarders, or --no-forwarders options") ++ "--auto-forwarders, --dot-forwarder, " ++ "or --no-forwarders options") + + + ServerMasterInstallInterface = installs_master(ServerInstallInterface) +diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py +index c39c807a9..4354683f1 100644 +--- a/ipaserver/install/server/install.py ++++ b/ipaserver/install/server/install.py +@@ -1018,6 +1018,10 @@ def install(installer): + + if options.setup_dns: + dns.install(False, False, options) ++ elif options.dns_over_tls: ++ service.print_msg("Warning: --dns-over-tls option " ++ "specified without --setup-dns, ignoring") ++ options.dns_over_tls = False + + # Always call adtrust installer to configure SID generation + # if --setup-adtrust is not specified, only the SID part is executed +@@ -1089,12 +1093,16 @@ def install(installer): + print("\t\t * 80, 443: HTTP/HTTPS") + print("\t\t * 389, 636: LDAP/LDAPS") + print("\t\t * 88, 464: kerberos") +- if options.setup_dns: +- print("\t\t * 53: bind") ++ if options.dns_over_tls and options.dns_policy == "enforced": ++ dns_port = "853" ++ elif options.dns_over_tls: ++ dns_port = "53, 853" ++ else: ++ dns_port = "53" ++ print(f"\t\t * {dns_port}: bind") + print("\t\tUDP Ports:") + print("\t\t * 88, 464: kerberos") +- if options.setup_dns: +- print("\t\t * 53: bind") ++ print(f"\t\t * {dns_port}: bind") + if not options.no_ntp: + print("\t\t * 123: ntp") + print("") +@@ -1109,6 +1117,14 @@ def install(installer): + print("\t and servers for correct operation. You should consider " + "enabling chronyd.") + ++ if options.dns_over_tls: ++ policy = "enforced" if options.dns_policy == "enforced" else "relaxed" ++ print("") ++ print(("DNS encryption support was enabled " ++ "with policy '{}'.".format(policy))) ++ print(("Unbound is configured to listen on 127.0.0.55:53 and " ++ "forward to upstream DoT servers.")) ++ + print("") + if setup_ca and not options.token_name: + print(("Be sure to back up the CA certificates stored in " + +diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py +index eeaaacb65..1f2c81f85 100644 +--- a/ipaserver/install/server/replicainstall.py ++++ b/ipaserver/install/server/replicainstall.py +@@ -722,6 +722,8 @@ def ensure_enrolled(installer): + args.extend(("--ntp-server", server)) + if installer.ntp_pool: + args.extend(("--ntp-pool", installer.ntp_pool)) ++ if installer.dns_over_tls and not installer.setup_dns: ++ args.append("--dns-over-tls") + + try: + # Call client install script +-- +2.48.1 + +From 186d5f65dc57dba3bb027fa4c5c4cb1603ce305a Mon Sep 17 00:00:00 2001 +From: Antonio Torres +Date: Wed, 11 Dec 2024 13:38:28 +0100 +Subject: [PATCH 2/4] ipatests: add tests for DNS over TLS + +Signed-off-by: Antonio Torres +Reviewed-By: Francisco Trivino +Reviewed-By: Alexander Bokovoy +Reviewed-By: Varun Mylaraiah +Reviewed-By: Pavel Brezina +--- + ipatests/test_integration/test_edns.py | 257 +++++++++++++++++++++++++ + 1 file changed, 257 insertions(+) + create mode 100644 ipatests/test_integration/test_edns.py + +diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py +new file mode 100644 +index 000000000..b42570ffa +--- /dev/null ++++ b/ipatests/test_integration/test_edns.py +@@ -0,0 +1,257 @@ ++# ++# Copyright (C) 2024 FreeIPA Contributors see COPYING for license ++# ++"""This covers tests for DNS over TLS related feature""" ++ ++from __future__ import absolute_import ++import textwrap ++ ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++from ipatests.test_integration.test_dns import TestDNS ++from ipatests.pytest_ipa.integration.firewall import Firewall ++from ipaplatform.paths import paths ++ ++ ++class TestDNSOverTLS(IntegrationTest): ++ """Tests for DNS over TLS feature.""" ++ ++ topology = 'line' ++ num_replicas = 1 ++ num_clients = 1 ++ ++ @classmethod ++ def install(cls, mh): ++ Firewall(cls.master).enable_service("dns-over-tls") ++ Firewall(cls.replicas[0]).enable_service("dns-over-tls") ++ tasks.install_packages(cls.master, ['*ipa-server-encrypted-dns']) ++ tasks.install_packages(cls.replicas[0], ['*ipa-server-encrypted-dns']) ++ tasks.install_packages(cls.clients[0], ['*ipa-client-encrypted-dns']) ++ ++ def test_install_dnsovertls_invalid_ca(self): ++ """ ++ This test checks that the installers throws an error ++ when invalid cert is specified. ++ """ ++ bad_ca_cnf = textwrap.dedent(""" ++ [ req ] ++ x509_extensions = v3_ca ++ [ v3_ca ] ++ basicConstraints = critical,CA:false ++ """) ++ self.master.put_file_contents("/bad_ca.cnf", bad_ca_cnf) ++ self.master.run_command(["openssl", "req", "-newkey", "rsa:2048", ++ "-nodes", "-keyout", ++ "/etc/pki/tls/certs/privkey-invalid.pem", ++ "-x509", "-days", "36500", "-out", ++ "/etc/pki/tls/certs/certificate-invalid.pem", ++ "-subj", ++ ("/C=ES/ST=Andalucia/L=Sevilla/O=CompanyName/" ++ "OU=IT/CN=www.example.com/" ++ "emailAddress=email@example.com"), ++ "-config", "/bad_ca.cnf"]) ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", ++ "--dns-over-tls-cert", ++ "/etc/pki/tls/certs/certificate-invalid.pem", ++ "--dns-over-tls-key", ++ "/etc/pki/tls/certs/privkey-invalid.pem" ++ ] ++ res = tasks.install_master(self.master, extra_args=args, ++ raiseonerr=False) ++ assert "Not a valid CA certificate: " in res.stderr_text ++ tasks.uninstall_master(self.master) ++ ++ def test_install_dnsovertls_without_setup_dns_master(self): ++ """ ++ This test installs an IPA server using the --dns-over-tls option ++ without using setup-dns option, and captures warnings that appear. ++ """ ++ self.master.run_command(["ipa-server-install", "--uninstall", "-U"]) ++ args = [ ++ "--dns-over-tls", ++ ] ++ res = tasks.install_master( ++ self.master, extra_args=args, setup_dns=False) ++ assert ("Warning: --dns-over-tls option specified without " ++ "--setup-dns, ignoring") in res.stdout_text ++ tasks.uninstall_master(self.master) ++ ++ def test_install_dnsovertls_master(self): ++ """ ++ This tests installs IPA server with --dns-over-tls option. ++ """ ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", ++ ] ++ return tasks.install_master(self.master, extra_args=args) ++ ++ def test_install_dnsovertls_client(self): ++ """ ++ This tests installs IPA client with --dns-over-tls option. ++ """ ++ self.clients[0].put_file_contents( ++ paths.RESOLV_CONF, ++ "nameserver %s" % self.master.ip ++ ) ++ args = [ ++ "--dns-over-tls" ++ ] ++ return tasks.install_client(self.master, ++ self.clients[0], ++ nameservers=None, ++ extra_args=args) ++ ++ def test_install_dnsovertls_replica(self): ++ """ ++ This tests installs IPA replica with --dns-over-tls option. ++ """ ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", ++ ] ++ return tasks.install_replica(self.master, self.replicas[0], ++ setup_dns=True, extra_args=args) ++ ++ def test_queries_encrypted(self): ++ """ ++ This test performs queries from each of the hosts ++ and ensures they were routed to 1.1.1.1#853 (eDNS). ++ """ ++ unbound_log_cfg = textwrap.dedent(""" ++ server: ++ verbosity: 3 ++ log-queries: yes ++ """) ++ # Test servers first (querying to local Unbound) ++ for server in [self.master, self.replicas[0]]: ++ server.put_file_contents("/etc/unbound/conf.d/log.conf", ++ unbound_log_cfg) ++ server.run_command(["systemctl", "restart", "unbound"]) ++ server.run_command(["journalctl", "--flush", "--rotate", ++ "--vacuum-time=1s"]) ++ server.run_command(["dig", "freeipa.org"]) ++ server.run_command(["journalctl", "-u", "unbound", ++ "--grep=1.1.1.1#853"]) ++ server.run_command(["journalctl", "--flush", "--rotate", ++ "--vacuum-time=1s"]) ++ # Now, test the client (redirects query to master) ++ self.clients[0].run_command(["dig", "redhat.com"]) ++ self.master.run_command(["journalctl", "-u", "unbound", ++ "--grep=1.1.1.1#853"]) ++ ++ def test_uninstall_all(self): ++ """ ++ This test ensures that all hosts can be uninstalled correctly. ++ """ ++ tasks.uninstall_client(self.clients[0]) ++ tasks.uninstall_replica(self.master, self.replicas[0]) ++ tasks.uninstall_master(self.master) ++ ++ def test_install_dnsovertls_master_external_ca(self): ++ """ ++ This test ensures that IPA server can be installed ++ with DoT using an external CA. ++ """ ++ self.master.run_command(["openssl", "req", "-newkey", "rsa:2048", ++ "-nodes", "-keyout", ++ "/etc/pki/tls/certs/privkey.pem", "-x509", ++ "-days", "36500", "-out", ++ "/etc/pki/tls/certs/certificate.pem", "-subj", ++ ("/C=ES/ST=Andalucia/L=Sevilla/O=CompanyName/" ++ "OU=IT/CN={}/" ++ "emailAddress=email@example.com") ++ .format(self.master.hostname)]) ++ self.master.run_command(["chown", "named:named", ++ "/etc/pki/tls/certs/privkey.pem", ++ "/etc/pki/tls/certs/certificate.pem"]) ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com", ++ "--dns-over-tls-cert", "/etc/pki/tls/certs/certificate.pem", ++ "--dns-over-tls-key", "/etc/pki/tls/certs/privkey.pem" ++ ] ++ return tasks.install_master(self.master, extra_args=args) ++ ++ def test_enrollments_external_ca(self): ++ """ ++ Test that replicas and clients can be deployed when the master ++ uses an external CA. ++ """ ++ tasks.copy_files(self.master, self.clients[0], ++ ["/etc/pki/tls/certs/certificate.pem"]) ++ self.clients[0].run_command(["mv", ++ "/etc/pki/tls/certs/certificate.pem", ++ "/etc/pki/ca-trust/source/anchors/"]) ++ self.clients[0].run_command(["update-ca-trust", "extract"]) ++ self.test_install_dnsovertls_client() ++ self.test_install_dnsovertls_replica() ++ self.test_queries_encrypted() ++ ++ def test_install_dnsovertls_with_invalid_ipaddress_master(self): ++ """ ++ This test installs an IPA server using the --dns-over-tls ++ option with an invalid IP address. ++ """ ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "198.168.0.0.1#example-dns.test", ++ ] ++ res = tasks.install_master(self.master, extra_args=args, ++ raiseonerr=False) ++ assert ("--dot-forwarder invalid: DoT forwarder must be in " ++ "the format of '1.2.3.4#dns.example.test'") in res.stderr_text ++ tasks.uninstall_master(self.master) ++ ++ def test_validate_DoT_options_master(self): ++ """ ++ Tests that DoT options are displayed correctly on master. ++ """ ++ cmdout = self.master.run_command( ++ ['ipa-server-install', '--help']) ++ assert '''--dot-forwarder=DOT_FORWARDERS ++ Add a DNS over TLS forwarder. This option can be used ++ multiple times''' in cmdout.stdout_text # noqa: E501 ++ assert '''--dns-over-tls-cert=DNS_OVER_TLS_CERT ++ Certificate to use for DNS over TLS. If empty, a new ++ certificate will be requested from IPA CA''' in cmdout.stdout_text # noqa: E501 ++ assert '''--dns-over-tls-key=DNS_OVER_TLS_KEY ++ Key for certificate specified in --dns-over-tls-cert''' in cmdout.stdout_text # noqa: E501 ++ assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 ++ ++ def test_validate_DoT_options_replica(self): ++ """ ++ Tests that DoT options are displayed correctly on replica. ++ """ ++ cmdout = self.replicas[0].run_command( ++ ['ipa-server-install', '--help']) ++ assert '''--dot-forwarder=DOT_FORWARDERS ++ Add a DNS over TLS forwarder. This option can be used ++ multiple times''' in cmdout.stdout_text ++ assert '''--dns-over-tls-cert=DNS_OVER_TLS_CERT ++ Certificate to use for DNS over TLS. If empty, a new ++ certificate will be requested from IPA CA''' in cmdout.stdout_text # noqa: E501 ++ assert '''--dns-over-tls-key=DNS_OVER_TLS_KEY ++ Key for certificate specified in --dns-over-tls-cert''' in cmdout.stdout_text # noqa: E501 ++ assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 ++ ++ def test_validate_DoT_options_client(self): ++ """ ++ Tests that DoT options are displayed correctly on client. ++ """ ++ cmdout = self.clients[0].run_command( ++ ['ipa-client-install', '--help']) ++ assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501 ++ ++ ++class TestDNS_DoT(TestDNS): ++ @classmethod ++ def install(cls, mh): ++ tasks.install_packages(cls.master, ['*ipa-server-encrypted-dns']) ++ args = [ ++ "--dns-over-tls", ++ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com" ++ ] ++ tasks.install_master(cls.master, extra_args=args) +-- +2.48.1 + +From a32b8fda893ae00bcd8efd91339dc8dbe35fc3cd Mon Sep 17 00:00:00 2001 +From: Antonio Torres +Date: Wed, 11 Dec 2024 13:35:29 +0100 +Subject: [PATCH 3/4] spec: add unbound requirement and template file + +Signed-off-by: Antonio Torres +Reviewed-By: Francisco Trivino +Reviewed-By: Alexander Bokovoy +Reviewed-By: Varun Mylaraiah +Reviewed-By: Pavel Brezina +--- + freeipa.spec.in | 27 +++++++++++++++++++++++++++ + 1 file changed, 27 insertions(+) + +diff --git a/freeipa.spec.in b/freeipa.spec.in +index 4b91aa96f..b539f51f8 100755 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -628,6 +628,7 @@ Requires: openssl-pkcs11 >= %{openssl_pkcs11_version} + # See https://bugzilla.redhat.com/show_bug.cgi?id=1825812 + # RHEL 8.3+ and Fedora 32+ have 2.1 + Requires: opendnssec >= 2.1.6-5 ++Recommends: %{name}-server-encrypted-dns + %{?systemd_requires} + + Provides: %{alt_name}-server-dns = %{version} +@@ -642,6 +643,15 @@ IPA integrated DNS server with support for automatic DNSSEC signing. + Integrated DNS server is BIND 9. OpenDNSSEC provides key management. + + ++%package server-encrypted-dns ++Summary: support for encrypted DNS in IPA integrated DNS server ++Requires: %{name}-client-encrypted-dns ++ ++%description server-encrypted-dns ++Provides support for enabling DNS over TLS in the IPA integrated DNS ++server. ++ ++ + %package server-trust-ad + Summary: Virtual package to install packages required for Active Directory trusts + Requires: %{name}-server = %{version}-%{release} +@@ -722,6 +732,7 @@ Requires: libnfsidmap + Requires: (nfs-utils or nfsv4-client-utils) + Requires: sssd-tools >= %{sssd_version} + Requires(post): policycoreutils ++Recommends: %{name}-client-encrypted-dns + + # https://pagure.io/freeipa/issue/8530 + Recommends: libsss_sudo +@@ -763,6 +774,14 @@ If your network uses IPA for authentication, this package should be + installed on every client machine. + This package provides command-line tools for IPA administrators. + ++%package client-encrypted-dns ++Summary: Enable encrypted DNS support for clients ++Requires: unbound ++ ++%description client-encrypted-dns ++This package enables support for installing clients with encrypted DNS ++via DNS over TLS. ++ + %package client-samba + Summary: Tools to configure Samba on IPA client + Group: System Environment/Base +@@ -1724,6 +1743,10 @@ fi + %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.socket + %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.service + ++%files server-encrypted-dns ++%doc README.md Contributors.txt ++%license COPYING ++ + %files server-trust-ad + %doc README.md Contributors.txt + %license COPYING +@@ -1783,6 +1806,10 @@ fi + %attr(600,root,root) %config(noreplace) %{_sysconfdir}/ipa/epn.conf + %attr(644,root,root) %config(noreplace) %{_sysconfdir}/ipa/epn/expire_msg.template + ++%files client-encrypted-dns ++%doc README.md Contributors.txt ++%license COPYING ++ + %files -n python3-ipaclient + %doc README.md Contributors.txt + %license COPYING +-- +2.48.1 + diff --git a/SOURCES/0053-Configure-the-pki-tomcatd-service-systemd-timeout.patch b/SOURCES/0053-Configure-the-pki-tomcatd-service-systemd-timeout.patch new file mode 100644 index 0000000..ca4bc30 --- /dev/null +++ b/SOURCES/0053-Configure-the-pki-tomcatd-service-systemd-timeout.patch @@ -0,0 +1,55 @@ +From 47ce0982249ee7ce12b38eae5ce3ee6a9b5df52e Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 4 Feb 2025 12:54:48 -0500 +Subject: [PATCH] Configure the pki-tomcatd service systemd timeout + +IPA defines a startup timeout that is primarily used +during installation to extend service start-up timeouts +on slower systems. + +This tends to work ok when runing pki-spawn but can fail when +systemd is starting the tomcat service. + +Use the value of startup_timeout to set TimeoutStartSec in +the pki-tomcat systemd override file ipa.conf. This will +preserve the necessary startup_timeout for all future restarts. + +This was seen with a very slow HSM where installation was successful +(pki-spawn) but pki-tomcatd startup timed out at the end of the +installation. + +To increase the value in installation one needs to create the file +/etc/ipa/installer.conf with contents: + +[global] +startup_timeout = 300 (or whatever) + +Fixes: https://pagure.io/freeipa/issue/9743 + +Signed-off-by: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + ipaserver/install/cainstance.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index 76718036dbd317651edc98ce631405e42bf814d7..c8ecde8f2e9649d57012fcda937ee5816105df4e 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -713,7 +713,12 @@ class CAInstance(DogtagInstance): + f.write('[Service]\n') + f.write('Environment=LC_ALL=C.UTF-8\n') + f.write('ExecStartPost={}\n'.format(paths.IPA_PKI_WAIT_RUNNING)) ++ f.write('TimeoutStartSec=%d\n' % api.env.startup_timeout) + tasks.systemd_daemon_reload() ++ logger.info( ++ "Set start up timeout of pki-tomcatd service to %d seconds", ++ api.env.startup_timeout ++ ) + + def safe_backup_config(self): + """ +-- +2.48.1 + diff --git a/SOURCES/0054-Align-startup_timeout-with-the-systemd-default-and-d.patch b/SOURCES/0054-Align-startup_timeout-with-the-systemd-default-and-d.patch new file mode 100644 index 0000000..c0d15af --- /dev/null +++ b/SOURCES/0054-Align-startup_timeout-with-the-systemd-default-and-d.patch @@ -0,0 +1,84 @@ +From 22cbc5ed4889d6c66e2916d5acde582b1868fbc9 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Mon, 10 Feb 2025 10:45:39 -0500 +Subject: [PATCH] Align startup_timeout with the systemd default and document + it + +We had it set to 120 seconds while the systemd default is 90. +They should be the same because the first one that times out "wins". + +Move where during the installation we create the systemd override +file so that the timeout will be applied across all subsequent +server starts during and post installation. + +Fixes: https://pagure.io/freeipa/issue/9743 + +Signed-off-by: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + client/man/default.conf.5 | 2 +- + ipalib/constants.py | 5 +++-- + ipaserver/install/cainstance.py | 3 ++- + 3 files changed, 6 insertions(+), 4 deletions(-) + +diff --git a/client/man/default.conf.5 b/client/man/default.conf.5 +index e0aec21f725d88ce2ba3cf52901fb15575892cde..461c60134124ed3e31e17ac350576487fda4c46e 100644 +--- a/client/man/default.conf.5 ++++ b/client/man/default.conf.5 +@@ -191,7 +191,7 @@ Specifies the IPA Server hostname. + Skip client vs. server API version checking. Can lead to errors/strange behavior when newer clients talk to older servers. Use with caution. + .TP + .B startup_timeout