From d5f3f770776dc86c318cd371297e9c45a40cc923 Mon Sep 17 00:00:00 2001 From: Florence Blanc-Renaud Date: Mon, 6 Feb 2023 08:44:35 +0100 Subject: [PATCH] ipa-4.10.1-4 - Resolves: rhbz#2161284 'ERROR Could not remove /tmp/tmpbkw6hawo.ipabkp' can be seen prior to 'ipa-client-install' command was successful - Resolves: rhbz#2164403 ipa-trust-add with --range-type=ipa-ad-trust-posix fails while creating an ID range - Resolves: rhbz#2162677 RFE: Implement support for PKI certificate and request pruning - Resolves: rhbz#2167312 - Backport latest test fixes in python3-ipatests Signed-off-by: Florence Blanc-Renaud --- ...emove-error-log-about-missing-bkup-f.patch | 54 ++ ...NG-before-kinit-command-to-fix-issue.patch | 118 +++ ...d-handle-missing-msSFU30MaxGidNumber.patch | 48 ++ 0006-doc-Design-for-certificate-pruning.patch | 337 +++++++++ ...add-certificate-request-pruning-mana.patch | 684 ++++++++++++++++++ ...run-command-for-manual-job-execution.patch | 138 ++++ ...s-add-wrapper-around-ACME-RSNv3-test.patch | 43 ++ freeipa.spec | 57 +- 8 files changed, 1451 insertions(+), 28 deletions(-) create mode 100644 0003-server-install-remove-error-log-about-missing-bkup-f.patch create mode 100644 0004-ipa-tests-Add-LANG-before-kinit-command-to-fix-issue.patch create mode 100644 0005-trust-add-handle-missing-msSFU30MaxGidNumber.patch create mode 100644 0006-doc-Design-for-certificate-pruning.patch create mode 100644 0007-ipa-acme-manage-add-certificate-request-pruning-mana.patch create mode 100644 0008-doc-add-the-run-command-for-manual-job-execution.patch create mode 100644 0009-tests-add-wrapper-around-ACME-RSNv3-test.patch diff --git a/0003-server-install-remove-error-log-about-missing-bkup-f.patch b/0003-server-install-remove-error-log-about-missing-bkup-f.patch new file mode 100644 index 0000000..4bb3177 --- /dev/null +++ b/0003-server-install-remove-error-log-about-missing-bkup-f.patch @@ -0,0 +1,54 @@ +From 894dca12c120f0bfa705307a0609da47326b8fb2 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Thu, 12 Jan 2023 11:26:53 +0100 +Subject: [PATCH] server install: remove error log about missing bkup file + +The client installer code can be called in 3 different ways: +- from ipa-client-install CLI +- from ipa-replica-install CLI if the client is not already installed +- from ipa-server-install + +In the last case, the client installer is called with +options.on_master=True +As a result, it's skipping the part that is creating the krb5 +configuration: + if not options.on_master: + nolog = tuple() + configure_krb5_conf(...) + +The configure_krb5_conf method is the place where the krb5.conf file is +backup'ed with the extention ".ipabkp". For a master installation, this +code is not called and the ipabkp file does not exist => delete raises +an error. + +When delete fails because the file does not exist, no need to log an +error message. + +Fixes: https://pagure.io/freeipa/issue/9306 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaclient/install/client.py | 7 +++---- + 1 file changed, 3 insertions(+), 4 deletions(-) + +diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py +index e5d3e8223efa1ebb69a1b7e963c394f9e1f38816..6e7f17d5b69581320866627cb5747a050cb6e32e 100644 +--- a/ipaclient/install/client.py ++++ b/ipaclient/install/client.py +@@ -124,10 +124,9 @@ def cleanup(func): + os.rmdir(ccache_dir) + except OSError: + pass +- try: +- os.remove(krb_name + ".ipabkp") +- except OSError: +- logger.error("Could not remove %s.ipabkp", krb_name) ++ # During master installation, the .ipabkp file is not created ++ # Ignore the delete error if it is "file does not exist" ++ remove_file(krb_name + ".ipabkp") + + return inner + +-- +2.39.1 + diff --git a/0004-ipa-tests-Add-LANG-before-kinit-command-to-fix-issue.patch b/0004-ipa-tests-Add-LANG-before-kinit-command-to-fix-issue.patch new file mode 100644 index 0000000..02bca15 --- /dev/null +++ b/0004-ipa-tests-Add-LANG-before-kinit-command-to-fix-issue.patch @@ -0,0 +1,118 @@ +From 2520a7adff7a49ddcddaaf19f0e586425dc0d878 Mon Sep 17 00:00:00 2001 +From: Filip Dvorak +Date: Tue, 6 Dec 2022 15:51:27 +0100 +Subject: [PATCH] ipa tests: Add LANG before kinit command to fix issue with + locale settings + +Reviewed-By: Rob Crittenden +Reviewed-By: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Michal Polovka +--- + ipatests/test_integration/test_krbtpolicy.py | 20 ++++++++++---------- + 1 file changed, 10 insertions(+), 10 deletions(-) + +diff --git a/ipatests/test_integration/test_krbtpolicy.py b/ipatests/test_integration/test_krbtpolicy.py +index eae16247bdfb195c1d91209cf2d11eac4c25018f..269cfb0a191821c229aaeb5a3eda0181c6e3ae62 100644 +--- a/ipatests/test_integration/test_krbtpolicy.py ++++ b/ipatests/test_integration/test_krbtpolicy.py +@@ -23,7 +23,7 @@ PASSWORD = "Secret123" + USER1 = "testuser1" + USER2 = "testuser2" + MAXLIFE = 86400 +- ++LANG_PKG = ["langpacks-en"] + + def maxlife_within_policy(input, maxlife, slush=3600): + """Given klist output of the TGT verify that it is within policy +@@ -45,7 +45,6 @@ def maxlife_within_policy(input, maxlife, slush=3600): + + return maxlife >= diff >= maxlife - slush + +- + @pytest.fixture + def reset_to_default_policy(): + """Reset default user authentication and user authentication type""" +@@ -70,7 +69,7 @@ def reset_to_default_policy(): + def kinit_check_life(master, user): + """Acquire a TGT and check if it's within the lifetime window""" + master.run_command(["kinit", user], stdin_text=f"{PASSWORD}\n") +- result = master.run_command("klist | grep krbtgt") ++ result = master.run_command("LANG=en_US.utf-8 klist | grep krbtgt") + assert maxlife_within_policy(result.stdout_text, MAXLIFE) is True + + +@@ -81,6 +80,7 @@ class TestPWPolicy(IntegrationTest): + + @classmethod + def install(cls, mh): ++ tasks.install_packages(cls.master, LANG_PKG) + tasks.install_master(cls.master) + tasks.create_active_user(cls.master, USER1, PASSWORD) + tasks.create_active_user(cls.master, USER2, PASSWORD) +@@ -100,7 +100,7 @@ class TestPWPolicy(IntegrationTest): + + master.run_command(['kinit', USER1], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command("LANG=en_US.utf-8 klist | grep krbtgt") + assert maxlife_within_policy(result.stdout_text, MAXLIFE) is True + + def test_krbtpolicy_password_and_hardended(self): +@@ -122,7 +122,7 @@ class TestPWPolicy(IntegrationTest): + + master.run_command(['kinit', USER1], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, 600, + slush=600) is True + +@@ -131,7 +131,7 @@ class TestPWPolicy(IntegrationTest): + # Verify that the short policy only applies to USER1 + master.run_command(['kinit', USER2], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, MAXLIFE) is True + + def test_krbtpolicy_hardended(self): +@@ -151,7 +151,7 @@ class TestPWPolicy(IntegrationTest): + + master.run_command(['kinit', USER1], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, 1800, + slush=1800) is True + +@@ -160,7 +160,7 @@ class TestPWPolicy(IntegrationTest): + # Verify that the short policy only applies to USER1 + master.run_command(['kinit', USER2], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, MAXLIFE) is True + + def test_krbtpolicy_password(self): +@@ -173,7 +173,7 @@ class TestPWPolicy(IntegrationTest): + + master.run_command(['kinit', USER2], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, 1200, + slush=1200) is True + +@@ -183,7 +183,7 @@ class TestPWPolicy(IntegrationTest): + master.run_command(['ipa', 'krbtpolicy-reset', USER2]) + master.run_command(['kinit', USER2], + stdin_text=PASSWORD + '\n') +- result = master.run_command('klist | grep krbtgt') ++ result = master.run_command('LANG=en_US.utf-8 klist | grep krbtgt') + assert maxlife_within_policy(result.stdout_text, MAXLIFE) is True + + def test_krbtpolicy_otp(self, reset_to_default_policy): +-- +2.39.1 + diff --git a/0005-trust-add-handle-missing-msSFU30MaxGidNumber.patch b/0005-trust-add-handle-missing-msSFU30MaxGidNumber.patch new file mode 100644 index 0000000..4c702d0 --- /dev/null +++ b/0005-trust-add-handle-missing-msSFU30MaxGidNumber.patch @@ -0,0 +1,48 @@ +From 97fc368df2db3b559a9def236d3c3e0a12bcdd0a Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Mon, 23 Jan 2023 20:28:17 +0100 +Subject: [PATCH] trust-add: handle missing msSFU30MaxGidNumber + +When ipa trust-add is executed with --range-type ad-trust-posix, +the server tries to find the max uidnumber and max gidnumber +from AD domain controller. +The values are extracted from the entry +CN=,CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System, +in the msSFU30MaxUidNumber and msSFU30MaxGidNumber attributes. + +msSFU30MaxUidNumber is required but not msSFU30MaxGidNumber. +In case msSFU30MaxGidNumber is missing, the code is currently assigning +a "None" value and later on evaluates the max between this value and +msSFU30MaxUidNumber. The max function cannot compare None and a list +of string and triggers an exception. + +To avoid the exception, assign [b'0'] to max gid if msSFU30MaxGidNumber +is missing. This way, the comparison succeeds and max returns the +value from msSFU30MaxUidNumber. + +Fixes: https://pagure.io/freeipa/issue/9310 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/plugins/trust.py | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py +index c074f6d6e609476e416c95bcbe607654718ae9ce..79264b8d8a3b15dd4e5d0553e4ce42194b0ae044 100644 +--- a/ipaserver/plugins/trust.py ++++ b/ipaserver/plugins/trust.py +@@ -379,7 +379,10 @@ def add_range(myapi, trustinstance, range_name, dom_sid, *keys, **options): + range_type = u'ipa-ad-trust-posix' + + max_uid = info.get('msSFU30MaxUidNumber') +- max_gid = info.get('msSFU30MaxGidNumber', None) ++ # if max_gid is missing, assume 0 and the max will ++ # be obtained from max_uid. We just checked that ++ # msSFU30MaxUidNumber is defined ++ max_gid = info.get('msSFU30MaxGidNumber', [b'0']) + max_id = int(max(max_uid, max_gid)[0]) + + base_id = int(info.get('msSFU30OrderNumber')[0]) +-- +2.39.1 + diff --git a/0006-doc-Design-for-certificate-pruning.patch b/0006-doc-Design-for-certificate-pruning.patch new file mode 100644 index 0000000..07eb3c9 --- /dev/null +++ b/0006-doc-Design-for-certificate-pruning.patch @@ -0,0 +1,337 @@ +From 51b1c22d025bf40e9ef488bb0faf0c8dff303ccd Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Thu, 8 Dec 2022 16:18:07 -0500 +Subject: [PATCH] doc: Design for certificate pruning + +This describes how the certificate pruning capability of PKI +introduced in v11.3.0 will be integrated into IPA, primarily for +ACME. + +Related: https://pagure.io/freeipa/issue/9294 + +Signed-off-by: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + doc/designs/expired_certificate_pruning.md | 297 +++++++++++++++++++++ + doc/designs/index.rst | 1 + + 2 files changed, 298 insertions(+) + create mode 100644 doc/designs/expired_certificate_pruning.md + +diff --git a/doc/designs/expired_certificate_pruning.md b/doc/designs/expired_certificate_pruning.md +new file mode 100644 +index 0000000000000000000000000000000000000000..2c10d914020d3c12b6abb028323cd6796ec33e00 +--- /dev/null ++++ b/doc/designs/expired_certificate_pruning.md +@@ -0,0 +1,297 @@ ++# Expired Certificate Pruning ++ ++## Overview ++ ++https://pagure.io/dogtagpki/issue/1750 ++ ++When using short-lived certs and regular issuance, the expired certs can build up in the PKI database and cause issues with replication, performance and overall database size. ++ ++PKI has provided a new feature in 11.3.0, pruning, which is a job that can be executed on a schedule or manually to remove expired certificates and requests. ++ ++Random Serial Numbers v3 (RSNv3) is mandatory to enable pruning. ++ ++Both pruning and RSNv3 require PKI 11.3.0 or higher. ++ ++## Use Cases ++ ++ACME certificates in particular are generally short-lived and expired certificates can build up quickly in a dynamic environment. An example is a CI system that requests one or more certificates per run. These will build up infinitely without a way to remove the expired certificates. ++ ++Another case is simply a very long-lived installation. Over time as hosts come and go certificates build up. ++ ++## How to Use ++ ++https://github.com/dogtagpki/pki/wiki/Configuring-CA-Database-Pruning provides a thorough description of the capabilities of the pruning job. ++ ++The default configuration is to remove expired certificates and incomplete requests after 30 days. ++ ++Pruning is disabled by default. ++ ++Configuration is a four-step process: ++ ++1. Configure the expiration thresholds ++2. Enable the job ++3. Schedule the job ++4. Restart the CA ++ ++The job will be scheduled to use the PKI built-in cron-like timer. It is configured nearly identically to `crontab(5)`. On execution it will remove certificates and requests that fall outside the configured thresholds. LDAP search/time limits can be used to control how many are removed at once. ++ ++In addition to the automated schedule it is possible to manually run the pruning job. ++ ++The tool will not restart the CA. It will be left as an exercise for the user, who will be notified as needed. ++ ++### Where to use ++ ++The pruning configuration is not replicated. It should not be necessary to enable this task on all IPA servers, or more than one. ++ ++Running the task simultaneously on multiple servers has a few downsides: ++ ++* Additional stress on the LDAP server searching for expired certificates and requests ++* Unnecessary replication load deleting the same entries on multiple servers ++ ++While enabling this on a single server represents a single-point-of-failure there should be no catastrophic consequences other than expired certificates and requests potentially building up. This can be cleared by enabling pruning on a different server. Depending on the size of the backlog this could take a couple of executions to catch up. ++ ++## Design ++ ++There are several operations, most of which act locally and one of which uses the PKI REST API. ++ ++1. Updating the job configuration (enable, thresholds, etc). This will be done by running the `pki-server ca-config-set` command which modifies CS.cfg directly per the PKI wiki. A restart is required. ++ ++2. Retrieving the current configuration for display. The `pki-server ca-config-find` command returns the entire configuration so the results will need to be filtered. ++ ++3. Managing the job. This can be done using the REST API, https://github.com/dogtagpki/pki/wiki/PKI-REST-API . Operations include enabling the job and triggering it to run now. ++ ++Theoretically for operations 1 and 2 we could use existing code to manually update `CS.cfg` and retrieve values. For future-proofing purposes calling `pki-server` is probably the better long-term option given the limited number of times this will be used. Configuration is likely to be one and done. ++ ++There are four values each that can be managed for pruning certificates and requests: ++ ++* expired cert/incomplete request time ++* time unit ++* LDAP search size limit ++* LDAP search time limit ++ ++The first two configure when an expired certificate or incomplete request will be deleted. The unit can be one of: minute, hour, day, year. By default it is 30 days. ++ ++The LDAP limits control how many entries are returned and how long the search can take. By default it is 1000 entries and unlimited time. ++ ++### Configuration settings ++ ++The configuration values will be set by running `pki-server ca-config-set` This will ensure best forward compatibility. The options are case-sensitive and not validated by the CA until restart. The values are not applied until the CA is restarted. ++ ++### Configuring job execution time ++ ++The CA provides a cron-like interface for scheduling jobs. To configure the job to run at midnight on the first of every month the PKI equivalent command-line is: ++ ++``` ++pki-server ca-config-set jobsScheduler.job.pruning.cron `"0 0 1 * *"` ++``` ++ ++This will be the default when pruning is enabled. A separate configuration option will be available for fine-tuning execution time. ++ ++The format is defined https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9/html/administration_guide/setting_up_specific_jobs#Frequency_Settings_for_Automated_Jobs ++ ++### REST Authentication and Authorization ++ ++The REST API for pruning is documented at https://github.com/dogtagpki/pki/wiki/PKI-Start-Job-REST-API ++ ++A PKI job can define an owner that can manage the job over the REST API. We will automatically define the owner as `ipara` when pruning is enabled. ++ ++Manually running the job will be done using the PKI REST API. Authentication to this API for our purposes is done at the `/ca/rest/account/login` endpoint. A cookie is returned which will be used in any subsequent calls. The IPA RA agent certificate will be used for authentication and authorization. ++ ++### Commands ++ ++This will be implemented in the ipa-acme-manage command. While strictly not completely ACME-related this is the primary driver for pruning. ++ ++A new verb will be added, pruning, to be used for enabling and configuring pruning. ++ ++### Enabling pruning ++ ++`# ipa-acme-manage pruning --enable=TRUE` ++ ++Enabling the job will call ++ ++`# pki-server ca-config-set jobsScheduler.job.pruning.enabled true` ++ ++This will also set jobsScheduler.job.pruning.cron to `"0 0 1 * *"` if it has not already been set. ++ ++Additionally it will set the job owner to `ipara` with: ++ ++`# pki-server ca-config-set jobsScheduler.job.pruning.owner ipara` ++ ++Disabling the job will call ++ ++`# pki-server ca-config-unset jobsScheduler.job.pruning.enabled` ++ ++### Cron settings ++ ++To modify the cron settings: ++ ++`# ipa-acme-manage pruning --cron="Minute Hour Day_of_month Month_of_year Day_of_week"` ++ ++Validation of the value will be: ++* each of the options is an integer ++* minute is within 0-59 ++* hour is within 0-23 ++* day of month is within 0-31 ++* month of year is within 1-12 ++* day of week is within 0-6 ++ ++No validation of setting February 31st will be done. That will be left to PKI. Buyer beware. ++ ++### Disabling pruning ++ ++`$ ipa-acme-manage pruning --enable=FALSE` ++ ++This will remove the configuration option for `jobsScheduler.job.pruning.cron` just to be sure it no longer runs. ++ ++### Configuration ++ ++#### Pruning certificates ++ ++`$ ipa-acme-manage pruning --certretention=VALUE --certretentionunit=UNIT` ++ ++will be the equivalent of: ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.certRetentionTime 30` ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.certRetentionUnit day` ++ ++The unit will always be required when modifying the time. ++ ++`$ ipa-acme-manage pruning --certsearchsizelimit=VALUE --certsearchtimelimit=VALUE` ++ ++will be the equivalent of: ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.certSearchSizeLimit 1000` ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.certSearchTimeLimit 0` ++ ++A value of 0 for searchtimelimit is unlimited. ++ ++#### Pruning requests ++ ++`$ ipa-acme-manage pruning --requestretention=VALUE --requestretentionunit=UNIT` ++ ++will be the equivalent of: ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionTime 30` ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionUnit day` ++ ++The unit will always be required when modifying the time. ++ ++`$ ipa-acme-manage pruning --requestsearchsizelimit=VALUE --requestsearchtimelimit=VALUE` ++ ++ ++will be the equivalent of: ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.requestSearchSizeLimit 1000` ++ ++`$ pki-server ca-config-set jobsScheduler.job.pruning.requestSearchTimeLimit 0` ++ ++A value of 0 for searchtimelimit is unlimited. ++ ++These options set the client-side limits. The server imposes its own search size and look through limits. This can be tuned for the uid=pkidbuser,ou=people,o=ipaca user via https://access.redhat.com/documentation/en-us/red_hat_directory_server/11/html/administration_guide/ldapsearch-ex-complex-range ++ ++### Showing the Configuration ++ ++To display the current configuration run `pki-server ca-config-find` and filter the results to only those that contain `jobsScheduler.job.pruning`. ++ ++Default values are not included so will need to be set by `ipa-acme-manage` before displaying. ++ ++Output may look something like: ++ ++```console ++# ipa-acme-manage pruning --config-show ++Enabled: TRUE ++Certificate retention time: 30 days ++Certificate search size limit: 1000 ++Certificate search time limit: 0 ++Request retention time: 30 days ++Request search size limit: 1000 ++Request search time limit: 0 ++Cron: 0 0 1 * * ++``` ++ ++## Implementation ++ ++For online REST operations (login, run job) we will use the `ipaserver/plugins/dogtag.py::RestClient` class to manage the requests. This will take care of the authentication cookie, etc. ++ ++The class uses dogtag.https_request() will can take PEM cert and key files as arguments. These will be used for authentication. ++ ++For the non-REST operations (configuration, cron settings) the tool will fork out to pki-server ca-config-set. ++ ++### UI ++ ++This will only be configurable on the command-line. ++ ++### CLI ++ ++Overview of the CLI commands. Example: ++ ++ ++| Command | Options | ++| --- | ----- | ++| ipa-acme-manage pruning | --enable=TRUE | ++| ipa-acme-manage pruning | --enable=FALSE | ++| ipa-acme-manage pruning | --cron=`"0 0 1 * *"` | ++| ipa-acme-manage pruning | --certretention=30 --certretentionunit=day | ++| ipa-acme-manage pruning | --certsearchsizelimit=1000 --certsearchtimelimit=0 | ++| ipa-acme-manage pruning | --requestretention=30 --requestretentionunit=day | ++| ipa-acme-manage pruning | --requestsearchsizelimit=1000 --requestsearchtimelimit=0 | ++| ipa-acme-manage pruning | --config-show | ++ ++ipa-acme-manage can only be run as root. ++ ++### Configuration ++ ++Configuration changes will be made to /etc/pki/pki-tomcat/ca/CS.cfg ++ ++## Upgrade ++ ++No expected impact on upgrades. ++ ++## Test plan ++ ++Testing will consist of: ++ ++* Use the default configuration ++* enabling the pruning job ++* issue one or more certificates ++* move time forward +1 days after expiration ++* manually running the job ++* validating that the certificates are removed ++ ++For size/time limit testing, create a large number of certificates/requests and set the search limit to a low value, then ensure that the number of deleted certs is equal to the search limit. Testing timelimit in this way may be less predictable as it may require a massive number of entries to find to timeout on a non-busy server. ++ ++## Troubleshooting and debugging ++ ++The PKI debug log will contain job information. ++ ++``` ++2022-12-08 21:14:25 [https-jsse-nio-8443-exec-8] INFO: JobService: Starting job pruning ++2022-12-08 21:14:25 [https-jsse-nio-8443-exec-8] INFO: JobService: - principal: null ++2022-12-08 21:14:51 [https-jsse-nio-8443-exec-10] INFO: JobService: Starting job pruning 2022-12-08 21:14:51 [https-jsse-nio-8443-exec-10] INFO: JobService: - principal: null ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: Authenticating certificate chain: ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - CN=IPA RA,O=EXAMPLE.TEST ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - CN=Certificate Authority,O=EXAMPLE.TEST ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: LDAPSession: Retrieving cn=19072098145751813471503860299601579276,ou=certificateRepository, ou=ca,o=ipaca ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: CertUserDBAuthentication: UID ipara authenticated. ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: User ipara authenticated ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: UGSubsystem: Retrieving user uid=ipara,ou=People,o=ipaca ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: User DN: uid=ipara,ou=people,o=ipaca ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: Roles: ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - Certificate Manager Agents ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - Registration Manager Agents ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - Security Domain Administrators ++2022-12-08 21:15:11 [https-jsse-nio-8443-exec-11] INFO: PKIRealm: - Enterprise ACME Administrators ++2022-12-08 21:15:24 [https-jsse-nio-8443-exec-12] INFO: JobService: Starting job pruning ++2022-12-08 21:15:24 [https-jsse-nio-8443-exec-12] INFO: JobService: - principal: GenericPrincipal[ipara(Certificate Manager Agents,Enterprise ACME Administrators,Registration Manager Agents,Security Domain Administrators,)] ++2022-12-08 21:15:24 [https-jsse-nio-8443-exec-12] INFO: JobsScheduler: Starting job pruning ++2022-12-08 21:15:24 [pruning] INFO: PruningJob: Running pruning job at Thu Dec 08 21:15:24 UTC 2022 ++2022-12-08 21:15:24 [pruning] INFO: PruningJob: Pruning certs expired before Tue Nov 08 21:15:24 UTC 2022 ++2022-12-08 21:15:24 [pruning] INFO: PruningJob: - filter: (&(x509Cert.notAfter<=1667942124527)(!(x509Cert.notAfter=1667942124527))) ++2022-12-08 21:15:24 [pruning] INFO: LDAPSession: Searching ou=certificateRepository, ou=ca,o=ipaca for (&(notAfter<=20221108211524Z)(!(notAfter=20221108211524Z))) ++2022-12-08 21:15:24 [pruning] INFO: PruningJob: Pruning incomplete requests last modified before Tue Nov 08 21:15:24 UTC 2022 ++2022-12-08 21:15:24 [pruning] INFO: PruningJob: - filter: (&(!(requestState=complete))(requestModifyTime<=1667942124527)(!(requestModifyTime=1667942124527))) ++2022-12-08 21:15:24 [pruning] INFO: LDAPSession: Searching ou=ca, ou=requests,o=ipaca for (&(!(requestState=complete))(dateOfModify<=20221108211524Z)(!(dateOfModify=20221108211524Z))) ++``` +diff --git a/doc/designs/index.rst b/doc/designs/index.rst +index 570e526fe35d510feeac62a44dd59224289e0506..1d41c0f84f0d7d3d5f184a47e31b4e71a890805d 100644 +--- a/doc/designs/index.rst ++++ b/doc/designs/index.rst +@@ -14,6 +14,7 @@ FreeIPA design documentation + hsm.md + krb-ticket-policy.md + extdom-plugin-protocol.md ++ expired_certificate_pruning.md + expiring-password-notification.md + ldap_grace_period.md + ldap_pam_passthrough.md +-- +2.39.1 + diff --git a/0007-ipa-acme-manage-add-certificate-request-pruning-mana.patch b/0007-ipa-acme-manage-add-certificate-request-pruning-mana.patch new file mode 100644 index 0000000..cbc2095 --- /dev/null +++ b/0007-ipa-acme-manage-add-certificate-request-pruning-mana.patch @@ -0,0 +1,684 @@ +From 9246a8a003b2b0062e07c289cd7cde8fe902b16f Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Thu, 12 Jan 2023 15:06:27 -0500 +Subject: [PATCH] ipa-acme-manage: add certificate/request pruning management + +Configures PKI to remove expired certificates and non-resolved +requests on a schedule. + +This is geared towards ACME which can generate a lot of certificates +over a short period of time but is general purpose. It lives in +ipa-acme-manage because that is the primary reason for including it. + +Random Serial Numbers v3 must be enabled for this to work. + +Enabling pruning enables the job scheduler within CS and sets the +job user as the IPA RA user which has full rights to certificates +and requests. + +Disabling pruning does not disable the job scheduler because the +tool is stateless. Having the scheduler enabled should not be a +problem. + +A restart of PKI is required to apply any changes. This tool forks +out to pki-server which does direct writes to CS.cfg. It might +be easier to use our own tooling for this but this makes the +integration tighter so we pick up any improvements in PKI. + +The "cron" setting is quite limited, taking only integer values +and *. It does not accept ranges, either - or /. + +No error checking is done in PKI when setting a value, only when +attempting to use it, so some rudimentary validation is done. + +Fixes: https://pagure.io/freeipa/issue/9294 + +Signed-off-by: Rob Crittenden rcritten@redhat.com +Reviewed-By: Florence Blanc-Renaud +--- + install/tools/man/ipa-acme-manage.1 | 83 +++++++ + ipaserver/install/ipa_acme_manage.py | 303 ++++++++++++++++++++++++- + ipatests/test_integration/test_acme.py | 158 +++++++++++++ + 3 files changed, 534 insertions(+), 10 deletions(-) + +diff --git a/install/tools/man/ipa-acme-manage.1 b/install/tools/man/ipa-acme-manage.1 +index e15d25bd0017d8bd71e425fcb633827fa6f67693..e6cec4e4a7fd460c514a72456a2dc9a2e3682ebd 100644 +--- a/install/tools/man/ipa-acme-manage.1 ++++ b/install/tools/man/ipa-acme-manage.1 +@@ -27,6 +27,89 @@ Disable the ACME service on this host. + .TP + \fBstatus\fR + Display the status of the ACME service. ++.TP ++\fBpruning\fR ++Configure certificate and request pruning. ++ ++.SH "PRUNING" ++Pruning is a job that runs in the CA that can remove expired ++certificates and certificate requests which have not been issued. ++This is particularly important when using short-lived certificates ++like those issued with the ACME protocol. Pruning requires that ++the IPA server be installed with random serial numbers enabled. ++ ++The CA needs to be restarted after modifying the pruning configuration. ++ ++The job is a cron-like task within the CA that is controlled by a ++number of options which dictate how long after the certificate or ++request is considered no longer valid and removed from the LDAP ++database. ++ ++The cron time and date fields are: ++.IP ++.ta 1.5i ++field allowed values ++.br ++----- -------------- ++.br ++minute 0-59 ++.br ++hour 0-23 ++.br ++day of month 1-31 ++.br ++month 1-12 ++.br ++day of week 0-6 (0 is Sunday) ++.br ++.PP ++ ++The cron syntax is limited to * or specific numbers. Ranges are not supported. ++ ++.TP ++\fB\-\-enable\fR ++Enable certificate pruning. ++.TP ++\fB\-\-disable\fR ++Disable certificate pruning. ++.TP ++\fB\-\-cron=CRON\fR ++Configure the pruning cron job. The syntax is similar to crontab(5) syntax. ++For example, "0 0 1 * *" schedules the job to run at 12:00am on the first ++day of each month. ++.TP ++\fB\-\-certretention=CERTRETENTION\fR ++Certificate retention time. The default is 30. ++.TP ++\fB\-\-certretentionunit=CERTRETENTIONUNIT\fR ++Certificate retention units. Valid units are: minute, hour, day, year. ++The default is days. ++.TP ++\fB\-\-certsearchsizelimit=CERTSEARCHSIZELIMIT\fR ++LDAP search size limit searching for expired certificates. The default is 1000. This is a client-side limit. There may be additional server-side limitations. ++.TP ++\fB\-\-certsearchtimelimit=CERTSEARCHTIMELIMIT\fR ++LDAP search time limit searching for expired certificates. The default is 0, no limit. This is a client-side limit. There may be additional server-side limitations. ++.TP ++\fB\-\-requestretention=REQUESTRETENTION\fR ++Request retention time. The default is 30. ++.TP ++\fB\-\-requestretentionunit=REQUESTRETENTIONUNIT\fR ++Request retention units. Valid units are: minute, hour, day, year. ++The default is days. ++.TP ++\fB\-\-requestsearchsizelimit=REQUESTSEARCHSIZELIMIT\fR ++LDAP search size limit searching for unfulfilled requests. The default is 1000. There may be additional server-side limitations. ++.TP ++\fB\-\-requestsearchtimelimit=REQUESTSEARCHTIMELIMIT\fR ++LDAP search time limit searching for unfulfilled requests. The default is 0, no limit. There may be additional server-side limitations. ++.TP ++\fB\-\-config\-show\fR ++Show the current pruning configuration ++.TP ++\fB\-\-run\fR ++Run the pruning job now. The IPA RA certificate is used to authenticate to the PKI REST backend. ++ + + .SH "EXIT STATUS" + 0 if the command was successful +diff --git a/ipaserver/install/ipa_acme_manage.py b/ipaserver/install/ipa_acme_manage.py +index 0474b9f4a051063ac6df41a81877a2af9d4a2096..b7b2111d9edcec2580aa4a485d7a7340146ff065 100644 +--- a/ipaserver/install/ipa_acme_manage.py ++++ b/ipaserver/install/ipa_acme_manage.py +@@ -2,7 +2,12 @@ + # Copyright (C) 2020 FreeIPA Contributors see COPYING for license + # + ++ + import enum ++import pki.util ++import logging ++ ++from optparse import OptionGroup # pylint: disable=deprecated-module + + from ipalib import api, errors, x509 + from ipalib import _ +@@ -10,10 +15,64 @@ from ipalib.facts import is_ipa_configured + from ipaplatform.paths import paths + from ipapython.admintool import AdminTool + from ipapython import cookie, dogtag ++from ipapython.ipautil import run ++from ipapython.certdb import NSSDatabase, EXTERNAL_CA_TRUST_FLAGS + from ipaserver.install import cainstance ++from ipaserver.install.ca import lookup_random_serial_number_version + + from ipaserver.plugins.dogtag import RestClient + ++logger = logging.getLogger(__name__) ++ ++default_pruning_options = { ++ 'certRetentionTime': '30', ++ 'certRetentionUnit': 'day', ++ 'certSearchSizeLimit': '1000', ++ 'certSearchTimeLimit': '0', ++ 'requestRetentionTime': 'day', ++ 'requestRetentionUnit': '30', ++ 'requestSearchSizeLimit': '1000', ++ 'requestSearchTimeLimit': '0', ++ 'cron': '' ++} ++ ++pruning_labels = { ++ 'certRetentionTime': 'Certificate Retention Time', ++ 'certRetentionUnit': 'Certificate Retention Unit', ++ 'certSearchSizeLimit': 'Certificate Search Size Limit', ++ 'certSearchTimeLimit': 'Certificate Search Time Limit', ++ 'requestRetentionTime': 'Request Retention Time', ++ 'requestRetentionUnit': 'Request Retention Unit', ++ 'requestSearchSizeLimit': 'Request Search Size Limit', ++ 'requestSearchTimeLimit': 'Request Search Time Limit', ++ 'cron': 'cron Schedule' ++} ++ ++ ++def validate_range(val, min, max): ++ """dogtag appears to have no error checking in the cron ++ entry so do some minimum amount of validation. It is ++ left as an exercise for the user to do month/day ++ validation so requesting Feb 31 will be accepted. ++ ++ Only * and a number within a min/max range are allowed. ++ """ ++ if val == '*': ++ return ++ ++ if '-' in val or '/' in val: ++ raise ValueError(f"{val} ranges are not supported") ++ ++ try: ++ int(val) ++ except ValueError: ++ # raise a clearer error ++ raise ValueError(f"{val} is not a valid integer") ++ ++ if int(val) < min or int(val) > max: ++ raise ValueError(f"{val} not within the range {min}-{max}") ++ ++ + # Manages the FreeIPA ACME service on a per-server basis. + # + # This program is a stop-gap until the deployment-wide management of +@@ -66,32 +125,121 @@ class acme_state(RestClient): + status, unused, _unused = self._request('/acme/disable', + headers=headers) + if status != 200: +- raise RuntimeError('Failed to disble ACME') ++ raise RuntimeError('Failed to disable ACME') + + + class Command(enum.Enum): + ENABLE = 'enable' + DISABLE = 'disable' + STATUS = 'status' ++ PRUNE = 'pruning' + + + class IPAACMEManage(AdminTool): + command_name = "ipa-acme-manage" +- usage = "%prog [enable|disable|status]" ++ usage = "%prog [enable|disable|status|pruning]" + description = "Manage the IPA ACME service" + ++ @classmethod ++ def add_options(cls, parser): ++ ++ group = OptionGroup(parser, 'Pruning') ++ group.add_option( ++ "--enable", dest="enable", action="store_true", ++ default=False, help="Enable certificate pruning") ++ group.add_option( ++ "--disable", dest="disable", action="store_true", ++ default=False, help="Disable certificate pruning") ++ group.add_option( ++ "--cron", dest="cron", action="store", ++ default=None, help="Configure the pruning cron job") ++ group.add_option( ++ "--certretention", dest="certretention", action="store", ++ default=None, help="Certificate retention time", type=int) ++ group.add_option( ++ "--certretentionunit", dest="certretentionunit", action="store", ++ choices=['minute', 'hour', 'day', 'year'], ++ default=None, help="Certificate retention units") ++ group.add_option( ++ "--certsearchsizelimit", dest="certsearchsizelimit", ++ action="store", ++ default=None, help="LDAP search size limit", type=int) ++ group.add_option( ++ "--certsearchtimelimit", dest="certsearchtimelimit", action="store", ++ default=None, help="LDAP search time limit", type=int) ++ group.add_option( ++ "--requestretention", dest="requestretention", action="store", ++ default=None, help="Request retention time", type=int) ++ group.add_option( ++ "--requestretentionunit", dest="requestretentionunit", ++ choices=['minute', 'hour', 'day', 'year'], ++ action="store", default=None, help="Request retention units") ++ group.add_option( ++ "--requestsearchsizelimit", dest="requestsearchsizelimit", ++ action="store", ++ default=None, help="LDAP search size limit", type=int) ++ group.add_option( ++ "--requestsearchtimelimit", dest="requestsearchtimelimit", ++ action="store", ++ default=None, help="LDAP search time limit", type=int) ++ group.add_option( ++ "--config-show", dest="config_show", action="store_true", ++ default=False, help="Show the current pruning configuration") ++ group.add_option( ++ "--run", dest="run", action="store_true", ++ default=False, help="Run the pruning job now") ++ parser.add_option_group(group) ++ super(IPAACMEManage, cls).add_options(parser, debug_option=True) ++ ++ + def validate_options(self): +- # needs root now - if/when this program changes to an API +- # wrapper we will no longer need root. + super(IPAACMEManage, self).validate_options(needs_root=True) + + if len(self.args) < 1: + self.option_parser.error(f'missing command argument') +- else: +- try: +- self.command = Command(self.args[0]) +- except ValueError: +- self.option_parser.error(f'unknown command "{self.args[0]}"') ++ ++ if self.args[0] == "pruning": ++ if self.options.enable and self.options.disable: ++ self.option_parser.error("Cannot both enable and disable") ++ elif ( ++ any( ++ [ ++ self.options.enable, ++ self.options.disable, ++ self.options.cron, ++ self.options.certretention, ++ self.options.certretentionunit, ++ self.options.requestretention, ++ self.options.requestretentionunit, ++ self.options.certsearchsizelimit, ++ self.options.certsearchtimelimit, ++ self.options.requestsearchsizelimit, ++ self.options.requestsearchtimelimit, ++ ] ++ ) ++ and (self.options.config_show or self.options.run) ++ ): ++ ++ self.option_parser.error( ++ "Cannot change and show config or run at the same time" ++ ) ++ elif self.options.cron: ++ if len(self.options.cron.split()) != 5: ++ self.option_parser.error("Invalid format for --cron") ++ # dogtag does no validation when setting an option so ++ # do the minimum. The dogtag cron is limited compared to ++ # crontab(5). ++ opt = self.options.cron.split() ++ validate_range(opt[0], 0, 59) ++ validate_range(opt[1], 0, 23) ++ validate_range(opt[2], 1, 31) ++ validate_range(opt[3], 1, 12) ++ validate_range(opt[4], 0, 6) ++ ++ try: ++ self.command = Command(self.args[0]) ++ except ValueError: ++ self.option_parser.error(f'unknown command "{self.args[0]}"') + + def check_san_status(self): + """ +@@ -100,6 +248,140 @@ class IPAACMEManage(AdminTool): + cert = x509.load_certificate_from_file(paths.HTTPD_CERT_FILE) + cainstance.check_ipa_ca_san(cert) + ++ def pruning(self): ++ def run_pki_server(command, directive, prefix, value=None): ++ """Take a set of arguments to append to pki-server""" ++ args = [ ++ 'pki-server', command, ++ f'{prefix}.{directive}' ++ ] ++ if value: ++ args.extend([str(value)]) ++ logger.debug(args) ++ result = run(args, raiseonerr=False, capture_output=True, ++ capture_error=True) ++ if result.returncode != 0: ++ raise RuntimeError(result.error_output) ++ return result ++ ++ def ca_config_set(directive, value, ++ prefix='jobsScheduler.job.pruning'): ++ run_pki_server('ca-config-set', directive, prefix, value) ++ # ca-config-set always succeeds, even if the option is ++ # not supported. ++ newvalue = ca_config_show(directive) ++ if str(value) != newvalue.strip(): ++ raise RuntimeError('Updating %s failed' % directive) ++ ++ def ca_config_show(directive): ++ result = run_pki_server('ca-config-show', directive, ++ prefix='jobsScheduler.job.pruning') ++ return result.output.strip() ++ ++ def config_show(): ++ status = ca_config_show('enabled') ++ if status.strip() == 'true': ++ print("Status: enabled") ++ else: ++ print("Status: disabled") ++ for option in ( ++ 'certRetentionTime', 'certRetentionUnit', ++ 'certSearchSizeLimit', 'certSearchTimeLimit', ++ 'requestRetentionTime', 'requestRetentionUnit', ++ 'requestSearchSizeLimit', 'requestSearchTimeLimit', ++ 'cron', ++ ): ++ value = ca_config_show(option) ++ if value: ++ print("{}: {}".format(pruning_labels[option], value)) ++ else: ++ print("{}: {}".format(pruning_labels[option], ++ default_pruning_options[option])) ++ ++ def run_pruning(): ++ """Run the pruning job manually""" ++ ++ with NSSDatabase() as tmpdb: ++ print("Preparing...") ++ tmpdb.create_db() ++ tmpdb.import_files((paths.RA_AGENT_PEM, paths.RA_AGENT_KEY), ++ import_keys=True) ++ tmpdb.import_files((paths.IPA_CA_CRT,)) ++ for nickname, trust_flags in tmpdb.list_certs(): ++ if trust_flags.has_key: ++ ra_nickname = nickname ++ continue ++ # external is suffucient for our purposes: C,, ++ tmpdb.trust_root_cert(nickname, EXTERNAL_CA_TRUST_FLAGS) ++ print("Starting job...") ++ args = ['pki', '-C', tmpdb.pwd_file, '-d', tmpdb.secdir, ++ '-n', ra_nickname, ++ 'ca-job-start', 'pruning'] ++ logger.debug(args) ++ run(args, stdin='y') ++ ++ pki_version = pki.util.Version(pki.specification_version()) ++ if pki_version < pki.util.Version("11.3.0"): ++ raise RuntimeError( ++ 'Certificate pruning is not supported in PKI version %s' ++ % pki_version ++ ) ++ ++ if lookup_random_serial_number_version(api) == 0: ++ raise RuntimeError( ++ 'Certificate pruning requires random serial numbers' ++ ) ++ ++ if self.options.config_show: ++ config_show() ++ return ++ ++ if self.options.run: ++ run_pruning() ++ return ++ ++ # Don't play the enable/disable at the same time game ++ if self.options.enable: ++ ca_config_set('owner', 'ipara') ++ ca_config_set('enabled', 'true') ++ ca_config_set('enabled', 'true', 'jobsScheduler') ++ elif self.options.disable: ++ ca_config_set('enabled', 'false') ++ ++ # pki-server ca-config-set can only set one option at a time so ++ # loop through all the options and set what is there. ++ if self.options.certretention: ++ ca_config_set('certRetentionTime', ++ self.options.certretention) ++ if self.options.certretentionunit: ++ ca_config_set('certRetentionUnit', ++ self.options.certretentionunit) ++ if self.options.certsearchtimelimit: ++ ca_config_set('certSearchTimeLimit', ++ self.options.certsearchtimelimit) ++ if self.options.certsearchsizelimit: ++ ca_config_set('certSearchSizeLimit', ++ self.options.certsearchsizelimit) ++ if self.options.requestretention: ++ ca_config_set('requestRetentionTime', ++ self.options.requestretention) ++ if self.options.requestretentionunit: ++ ca_config_set('requestRetentionUnit', ++ self.options.requestretentionunit) ++ if self.options.requestsearchsizelimit: ++ ca_config_set('requestSearchSizeLimit', ++ self.options.requestsearchsizelimit) ++ if self.options.requestsearchtimelimit: ++ ca_config_set('requestSearchTimeLimit', ++ self.options.requestsearchtimelimit) ++ if self.options.cron: ++ ca_config_set('cron', self.options.cron) ++ ++ config_show() ++ ++ print("The CA service must be restarted for changes to take effect") ++ ++ + def run(self): + if not is_ipa_configured(): + print("IPA is not configured.") +@@ -123,7 +405,8 @@ class IPAACMEManage(AdminTool): + elif self.command == Command.STATUS: + status = "enabled" if dogtag.acme_status() else "disabled" + print("ACME is {}".format(status)) +- return 0 ++ elif self.command == Command.PRUNE: ++ self.pruning() + else: + raise RuntimeError('programmer error: unhandled enum case') + +diff --git a/ipatests/test_integration/test_acme.py b/ipatests/test_integration/test_acme.py +index 15d7543cfb0fa0fcb921166f7cd8f13d0535a41d..93e785d8febd9fa8d7b3ef87ecb3f2eb42ac5da2 100644 +--- a/ipatests/test_integration/test_acme.py ++++ b/ipatests/test_integration/test_acme.py +@@ -12,6 +12,9 @@ from ipalib.constants import IPA_CA_RECORD + from ipatests.test_integration.base import IntegrationTest + from ipatests.pytest_ipa.integration import tasks + from ipatests.test_integration.test_caless import CALessBase, ipa_certs_cleanup ++from ipatests.test_integration.test_random_serial_numbers import ( ++ pki_supports_RSNv3 ++) + from ipaplatform.osinfo import osinfo + from ipaplatform.paths import paths + from ipatests.test_integration.test_external_ca import ( +@@ -388,6 +391,16 @@ class TestACME(CALessBase): + status = check_acme_status(self.replicas[0], 'disabled') + assert status == 'disabled' + ++ def test_acme_pruning_no_random_serial(self): ++ """This ACME install is configured without random serial ++ numbers. Verify that we can't enable pruning on it.""" ++ self.master.run_command(['ipa-acme-manage', 'enable']) ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', '--enable'], ++ raiseonerr=False) ++ assert result.returncode == 1 ++ assert "requires random serial numbers" in result.stderr_text ++ + @server_install_teardown + def test_third_party_certs(self): + """Require ipa-ca SAN on replacement web certificates""" +@@ -630,3 +643,148 @@ class TestACMERenew(IntegrationTest): + renewed_expiry = cert.not_valid_after + + assert initial_expiry != renewed_expiry ++ ++ ++class TestACMEPrune(IntegrationTest): ++ """Validate that ipa-acme-manage configures dogtag for pruning""" ++ ++ random_serial = True ++ ++ @classmethod ++ def install(cls, mh): ++ if not pki_supports_RSNv3(mh.master): ++ raise pytest.skip("RNSv3 not supported") ++ tasks.install_master(cls.master, setup_dns=True, ++ random_serial=True) ++ ++ @classmethod ++ def uninstall(cls, mh): ++ if not pki_supports_RSNv3(mh.master): ++ raise pytest.skip("RSNv3 not supported") ++ super(TestACMEPrune, cls).uninstall(mh) ++ ++ def test_enable_pruning(self): ++ if (tasks.get_pki_version(self.master) ++ < tasks.parse_version('11.3.0')): ++ raise pytest.skip("Certificate pruning is not available") ++ cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) ++ assert "jobsScheduler.job.pruning.enabled=false".encode() in cs_cfg ++ ++ self.master.run_command(['ipa-acme-manage', 'pruning', '--enable']) ++ ++ cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) ++ assert "jobsScheduler.enabled=true".encode() in cs_cfg ++ assert "jobsScheduler.job.pruning.enabled=true".encode() in cs_cfg ++ assert "jobsScheduler.job.pruning.owner=ipara".encode() in cs_cfg ++ ++ def test_pruning_options(self): ++ if (tasks.get_pki_version(self.master) ++ < tasks.parse_version('11.3.0')): ++ raise pytest.skip("Certificate pruning is not available") ++ ++ self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--certretention=60', ++ '--certretentionunit=minute', ++ '--certsearchsizelimit=2000', ++ '--certsearchtimelimit=5',] ++ ) ++ cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) ++ assert ( ++ "jobsScheduler.job.pruning.certRetentionTime=60".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.certRetentionUnit=minute".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.certSearchSizeLimit=2000".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.certSearchTimeLimit=5".encode() ++ in cs_cfg ++ ) ++ ++ self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--requestretention=60', ++ '--requestretentionunit=minute', ++ '--requestresearchsizelimit=2000', ++ '--requestsearchtimelimit=5',] ++ ) ++ cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) ++ assert ( ++ "jobsScheduler.job.pruning.requestRetentionTime=60".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.requestRetentionUnit=minute".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.requestSearchSizeLimit=2000".encode() ++ in cs_cfg ++ ) ++ assert ( ++ "jobsScheduler.job.pruning.requestSearchTimeLimit=5".encode() ++ in cs_cfg ++ ) ++ ++ self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--cron="0 23 1 * *',] ++ ) ++ cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) ++ assert ( ++ "jobsScheduler.job.pruning.cron=0 23 1 * *".encode() ++ in cs_cfg ++ ) ++ ++ def test_pruning_negative_options(self): ++ """Negative option testing for things we directly cover""" ++ if (tasks.get_pki_version(self.master) ++ < tasks.parse_version('11.3.0')): ++ raise pytest.skip("Certificate pruning is not available") ++ ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--enable', '--disable'], ++ raiseonerr=False ++ ) ++ assert result.returncode == 1 ++ assert "Cannot both enable and disable" in result.stderr_text ++ ++ for cmd in ('--config-show', '--run'): ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ cmd, '--enable'], ++ raiseonerr=False ++ ) ++ assert result.returncode == 1 ++ assert "Cannot change and show config" in result.stderr_text ++ ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--cron="* *"'], ++ raiseonerr=False ++ ) ++ assert result.returncode == 1 ++ assert "Invalid format format --cron" in result.stderr_text ++ ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--cron="100 * * * *"'], ++ raiseonerr=False ++ ) ++ assert result.returncode == 1 ++ assert "100 not within the range 0-59" in result.stderr_text ++ ++ result = self.master.run_command( ++ ['ipa-acme-manage', 'pruning', ++ '--cron="10 1-5 * * *"'], ++ raiseonerr=False ++ ) ++ assert result.returncode == 1 ++ assert "1-5 ranges are not supported" in result.stderr_text +-- +2.39.1 + diff --git a/0008-doc-add-the-run-command-for-manual-job-execution.patch b/0008-doc-add-the-run-command-for-manual-job-execution.patch new file mode 100644 index 0000000..4dd7587 --- /dev/null +++ b/0008-doc-add-the-run-command-for-manual-job-execution.patch @@ -0,0 +1,138 @@ +From f10d1a0f84ed0f16ab4a1469f16ffadb3e79e59e Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Fri, 27 Jan 2023 14:05:37 -0500 +Subject: [PATCH] doc: add the --run command for manual job execution + +A manual method was mentioned with no specificity. Include +the --run command. Also update the troubleshooting section +to show what failure to restart the CA after configuration +looks like. + +Import the IPA CA chain for manual execution. + +Also fix up some $ -> # to indicate root is needed. + +Related: https://pagure.io/freeipa/issue/9294 + +Signed-off-by: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +--- + doc/designs/expired_certificate_pruning.md | 46 +++++++++++++++------- + 1 file changed, 32 insertions(+), 14 deletions(-) + +diff --git a/doc/designs/expired_certificate_pruning.md b/doc/designs/expired_certificate_pruning.md +index 2c10d914020d3c12b6abb028323cd6796ec33e00..a23e452696ba2a150c4ad5a3e57360ae0a16a338 100644 +--- a/doc/designs/expired_certificate_pruning.md ++++ b/doc/designs/expired_certificate_pruning.md +@@ -139,7 +139,7 @@ No validation of setting February 31st will be done. That will be left to PKI. B + + ### Disabling pruning + +-`$ ipa-acme-manage pruning --enable=FALSE` ++`# ipa-acme-manage pruning --enable=FALSE` + + This will remove the configuration option for `jobsScheduler.job.pruning.cron` just to be sure it no longer runs. + +@@ -147,46 +147,46 @@ This will remove the configuration option for `jobsScheduler.job.pruning.cron` j + + #### Pruning certificates + +-`$ ipa-acme-manage pruning --certretention=VALUE --certretentionunit=UNIT` ++`# ipa-acme-manage pruning --certretention=VALUE --certretentionunit=UNIT` + + will be the equivalent of: + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.certRetentionTime 30` ++`# pki-server ca-config-set jobsScheduler.job.pruning.certRetentionTime 30` + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.certRetentionUnit day` ++`# pki-server ca-config-set jobsScheduler.job.pruning.certRetentionUnit day` + + The unit will always be required when modifying the time. + +-`$ ipa-acme-manage pruning --certsearchsizelimit=VALUE --certsearchtimelimit=VALUE` ++`# ipa-acme-manage pruning --certsearchsizelimit=VALUE --certsearchtimelimit=VALUE` + + will be the equivalent of: + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.certSearchSizeLimit 1000` ++`# pki-server ca-config-set jobsScheduler.job.pruning.certSearchSizeLimit 1000` + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.certSearchTimeLimit 0` ++`# pki-server ca-config-set jobsScheduler.job.pruning.certSearchTimeLimit 0` + + A value of 0 for searchtimelimit is unlimited. + + #### Pruning requests + +-`$ ipa-acme-manage pruning --requestretention=VALUE --requestretentionunit=UNIT` ++`# ipa-acme-manage pruning --requestretention=VALUE --requestretentionunit=UNIT` + + will be the equivalent of: + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionTime 30` ++`# pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionTime 30` + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionUnit day` ++`# pki-server ca-config-set jobsScheduler.job.pruning.requestRetentionUnit day` + + The unit will always be required when modifying the time. + +-`$ ipa-acme-manage pruning --requestsearchsizelimit=VALUE --requestsearchtimelimit=VALUE` ++`# ipa-acme-manage pruning --requestsearchsizelimit=VALUE --requestsearchtimelimit=VALUE` + + + will be the equivalent of: + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.requestSearchSizeLimit 1000` ++`# pki-server ca-config-set jobsScheduler.job.pruning.requestSearchSizeLimit 1000` + +-`$ pki-server ca-config-set jobsScheduler.job.pruning.requestSearchTimeLimit 0` ++`# pki-server ca-config-set jobsScheduler.job.pruning.requestSearchTimeLimit 0` + + A value of 0 for searchtimelimit is unlimited. + +@@ -212,10 +212,15 @@ Request search time limit: 0 + Cron: 0 0 1 * * + ``` + ++### Manual pruning ++ ++`# ipa-acme-manage pruning --run` ++ ++This is useful for testing the configuration or if the user wants to use the system cron or systemd timers for handling automation. ++ + ## Implementation + + For online REST operations (login, run job) we will use the `ipaserver/plugins/dogtag.py::RestClient` class to manage the requests. This will take care of the authentication cookie, etc. +- + The class uses dogtag.https_request() will can take PEM cert and key files as arguments. These will be used for authentication. + + For the non-REST operations (configuration, cron settings) the tool will fork out to pki-server ca-config-set. +@@ -239,6 +244,7 @@ Overview of the CLI commands. Example: + | ipa-acme-manage pruning | --requestretention=30 --requestretentionunit=day | + | ipa-acme-manage pruning | --requestsearchsizelimit=1000 --requestsearchtimelimit=0 | + | ipa-acme-manage pruning | --config-show | ++| ipa-acme-manage pruning | --run | + + ipa-acme-manage can only be run as root. + +@@ -295,3 +301,15 @@ The PKI debug log will contain job information. + 2022-12-08 21:15:24 [pruning] INFO: PruningJob: - filter: (&(!(requestState=complete))(requestModifyTime<=1667942124527)(!(requestModifyTime=1667942124527))) + 2022-12-08 21:15:24 [pruning] INFO: LDAPSession: Searching ou=ca, ou=requests,o=ipaca for (&(!(requestState=complete))(dateOfModify<=20221108211524Z)(!(dateOfModify=20221108211524Z))) + ``` ++ ++### Manual execution fails with Forbidden ++ ++If manually running pruning fails with a message like: ++ ++```console ++# ipa-acme-manage pruning --run ++CalledProcessError(Command ['pki', '-C', '/tmp/tmppyyd3hfq/pwdfile.txt', '-d', '/tmp/tmppyyd3hfq', '-n', 'CN=IPA RA,O=EXAMPLE.TEST', 'ca-job-start', 'pruning'] returned non-zero exit status 255: 'PKIException: Forbidden\n') ++The ipa-acme-manage command failed. ++``` ++ ++You probably forgot to restart the CA after enabling pruning. +-- +2.39.1 + diff --git a/0009-tests-add-wrapper-around-ACME-RSNv3-test.patch b/0009-tests-add-wrapper-around-ACME-RSNv3-test.patch new file mode 100644 index 0000000..0137fbb --- /dev/null +++ b/0009-tests-add-wrapper-around-ACME-RSNv3-test.patch @@ -0,0 +1,43 @@ +From d24b69981d94fce7b1e1aa4a5c1ab88a123f96b5 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Fri, 3 Feb 2023 10:04:31 -0500 +Subject: [PATCH] tests: add wrapper around ACME RSNv3 test + +This test is located outside of the TestACMEPrune because +it enables RSNv3 while the server installed by TestACME doesn't. + +It still needs a wrapper to enforce a version of PKI that +supports pruning because that is checked first in the tool. +Re-ordering that wouldn't be a good user experience. + +https://pagure.io/freeipa/issue/9322 + +Signed-off-by: Rob Crittenden +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_acme.py | 9 ++++++++- + 1 file changed, 8 insertions(+), 1 deletion(-) + +diff --git a/ipatests/test_integration/test_acme.py b/ipatests/test_integration/test_acme.py +index 93e785d8febd9fa8d7b3ef87ecb3f2eb42ac5da2..5ceba05976059de69414a79634d98045c3ab68bb 100644 +--- a/ipatests/test_integration/test_acme.py ++++ b/ipatests/test_integration/test_acme.py +@@ -393,7 +393,14 @@ class TestACME(CALessBase): + + def test_acme_pruning_no_random_serial(self): + """This ACME install is configured without random serial +- numbers. Verify that we can't enable pruning on it.""" ++ numbers. Verify that we can't enable pruning on it. ++ ++ This test is located here because by default installs ++ don't enable RSNv3. ++ """ ++ if (tasks.get_pki_version(self.master) ++ < tasks.parse_version('11.3.0')): ++ raise pytest.skip("Certificate pruning is not available") + self.master.run_command(['ipa-acme-manage', 'enable']) + result = self.master.run_command( + ['ipa-acme-manage', 'pruning', '--enable'], +-- +2.39.1 + diff --git a/freeipa.spec b/freeipa.spec index 0dd680d..8b83e54 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -94,8 +94,6 @@ # Fedora %global package_name freeipa %global alt_name ipa -# Fix for CVE-2020-28196 -%global krb5_version 1.18.2-29 # 0.7.16: https://github.com/drkjam/netaddr/issues/71 %global python_netaddr_version 0.7.16 # Require 4.7.0 which brings Python 3 bindings @@ -111,7 +109,15 @@ %endif %global slapi_nis_version 0.56.5 +%if 0%{?fedora} < 38 +# Fix for CVE-2020-28196 +%global krb5_version 1.18.2-29 %global krb5_kdb_version 8.0 +%else +# Fix for CVE-2020-28196 +%global krb5_version 1.20.1-3 +%global krb5_kdb_version 9.0 +%endif # fix for segfault in python3-ldap, https://pagure.io/freeipa/issue/7324 %global python_ldap_version 3.1.0-1 @@ -217,7 +223,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 3%{?rc_version:.%rc_version}%{?dist} +Release: 4%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPLv3+ @@ -236,17 +242,25 @@ Source1: https://releases.pagure.org/freeipa/freeipa-%{version}%{?rc_vers # RHEL spec file only: START %if %{NON_DEVELOPER_BUILD} -%if 0%{?rhel} >= 8 +%if 0%{?rhel} == 8 +Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch +Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch +%endif +%if 0%{?rhel} == 9 Patch0001: 0001-updates-fix-memberManager-ACI-to-allow-managers-from.patch Patch0002: 0002-Spec-file-ipa-client-depends-on-krb5-pkinit-openssl.patch +Patch0003: 0003-server-install-remove-error-log-about-missing-bkup-f.patch +Patch0004: 0004-ipa-tests-Add-LANG-before-kinit-command-to-fix-issue.patch +Patch0005: 0005-trust-add-handle-missing-msSFU30MaxGidNumber.patch +Patch0006: 0006-doc-Design-for-certificate-pruning.patch +Patch0007: 0007-ipa-acme-manage-add-certificate-request-pruning-mana.patch +Patch0008: 0008-doc-add-the-run-command-for-manual-job-execution.patch +Patch0009: 0009-tests-add-wrapper-around-ACME-RSNv3-test.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif # RHEL spec file only: END -# For the timestamp trick in patch application -BuildRequires: diffstat - BuildRequires: openldap-devel # For KDB DAL version, make explicit dependency so that increase of version # will cause the build to fail due to unsatisfied dependencies. @@ -383,7 +397,6 @@ BuildRequires: python3-libsss_nss_idmap BuildRequires: python3-lxml BuildRequires: python3-netaddr >= %{python_netaddr_version} BuildRequires: python3-netifaces -BuildRequires: python3-paste BuildRequires: python3-pki >= %{pki_version} BuildRequires: python3-polib BuildRequires: python3-pyasn1 @@ -970,22 +983,7 @@ Custom SELinux policy module for FreeIPA %prep -# Update timestamps on the files touched by a patch, to avoid non-equal -# .pyc/.pyo files across the multilib peers within a build, where "Level" -# is the patch prefix option (e.g. -p1) -# Taken from specfile for sssd and python-simplejson -UpdateTimestamps() { - Level=$1 - PatchFile=$2 - - # Locate the affected files: - for f in $(diffstat $Level -l $PatchFile); do - # Set the files to have the same timestamp as that of the patch: - touch -c -r $PatchFile $f - done -} - -%setup -n freeipa-%{version}%{?rc_version} -q +%autosetup -n freeipa-%{version}%{?rc_version} -N -p1 # To allow proper application patches to the stripped po files, strip originals pushd po @@ -995,10 +993,7 @@ for i in *.po ; do done popd -for p in %patches ; do - %__patch -p1 -i $p - UpdateTimestamps -p1 $p -done +%autopatch -p1 %build # PATH is workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1005235 @@ -1748,6 +1743,12 @@ fi %endif %changelog +* Mon Feb 06 2023 Florence Blanc-Renaud - 4.10.1-4 +- Resolves: rhbz#2161284 'ERROR Could not remove /tmp/tmpbkw6hawo.ipabkp' can be seen prior to 'ipa-client-install' command was successful +- Resolves: rhbz#2164403 ipa-trust-add with --range-type=ipa-ad-trust-posix fails while creating an ID range +- Resolves: rhbz#2162677 RFE: Implement support for PKI certificate and request pruning +- Resolves: rhbz#2167312 - Backport latest test fixes in python3-ipatests + * Wed Dec 21 2022 Alexander Bokovoy - 4.10.1-3 - Rebuild against krb5 1.20.1 ABI - Resolves: rhbz#2155425