From a4087a755bdf86f267cf4c58b60684205a4ef8ce Mon Sep 17 00:00:00 2001
From: Rafael Guterres Jeffman <rjeffman@redhat.com>
Date: Wed, 15 Mar 2023 12:20:30 -0300
Subject: [PATCH] roles/ipaserver: Allow deployments with random serial numbers

Since FreeIPA version 4.10 it is possible to deploy servers that use
Random Serial Number v3 support for certificates.

This patch exposes the 'random_serial_numbers' parameter, as
'ipaserver_random_serial_numbers', allowing a user to have random serial
numbers enabled for the domain.

The use of random serial numbers is allowed on new installations only.
---
 roles/ipaserver/README.md                     | 17 ++++
 roles/ipaserver/defaults/main.yml             |  1 +
 roles/ipaserver/library/ipaserver_test.py     | 91 +++++++++++--------
 .../module_utils/ansible_ipa_server.py        |  9 +-
 roles/ipaserver/tasks/install.yml             |  3 +-
 5 files changed, 82 insertions(+), 39 deletions(-)

diff --git a/roles/ipaserver/README.md b/roles/ipaserver/README.md
index 130be07c..b5dd9e62 100644
--- a/roles/ipaserver/README.md
+++ b/roles/ipaserver/README.md
@@ -168,6 +168,22 @@ Server installation step 2: Copy `<ipaserver hostname>-chain.crt` to the IPA ser
 
 The files can also be copied automatically: Set `ipaserver_copy_csr_to_controller` to true in the server installation step 1 and set `ipaserver_external_cert_files_from_controller` to point to the `chain.crt` file in the server installation step 2.
 
+Since version 4.10, FreeIPA supports creating certificates using random serial numbers. Random serial numbers is a global and permanent setting, that can only be activated while deploying the first server of the domain. Replicas will inherit this setting automatically. An example of an inventory file to deploy a server with random serial numbers enabled is:
+
+```ini
+[ipaserver]
+ipaserver.example.com
+
+[ipaserver:vars]
+ipaserver_domain=example.com
+ipaserver_realm=EXAMPLE.COM
+ipaadmin_password=MySecretPassword123
+ipadm_password=MySecretPassword234
+ipaserver_random_serial_number=true
+```
+
+By setting the variable in the inventory file, the same ipaserver deployment playbook, shown before, can be used.
+
 
 Example inventory file to remove a server from the domain:
 
@@ -263,6 +279,7 @@ Variable | Description | Required
 `ipaserver_no_ui_redirect` | Do not automatically redirect to the Web UI. (bool) | no
 `ipaserver_dirsrv_config_file` | The path to LDIF file that will be used to modify configuration of dse.ldif during installation. (string) | no
 `ipaserver_pki_config_override` | Path to ini file with config overrides. This is only usable with recent FreeIPA versions. (string) | no
+`ipaserver_random_serial_numbers` | Enable use of random serial numbers for certificates. Requires FreeIPA version 4.10 or later. (boolean) | no
 
 SSL certificate Variables
 -------------------------
diff --git a/roles/ipaserver/defaults/main.yml b/roles/ipaserver/defaults/main.yml
index 5af85c2d..0acecc57 100644
--- a/roles/ipaserver/defaults/main.yml
+++ b/roles/ipaserver/defaults/main.yml
@@ -11,6 +11,7 @@ ipaserver_no_hbac_allow: no
 ipaserver_no_pkinit: no
 ipaserver_no_ui_redirect: no
 ipaserver_mem_check: yes
+ipaserver_random_serial_numbers: true
 ### ssl certificate ###
 ### client ###
 ipaclient_mkhomedir: no
diff --git a/roles/ipaserver/library/ipaserver_test.py b/roles/ipaserver/library/ipaserver_test.py
index cf5b7c8f..45f65a2b 100644
--- a/roles/ipaserver/library/ipaserver_test.py
+++ b/roles/ipaserver/library/ipaserver_test.py
@@ -208,6 +208,10 @@ options:
     description: The installer ca_subject setting
     type: str
     required: no
+  random_serial_numbers:
+    description: The installer random_serial_numbers setting
+    type: bool
+    required: no
   allow_zone_overlap:
     description: Create DNS zone even if it already exists
     type: bool
@@ -304,7 +308,7 @@ from ansible.module_utils.ansible_ipa_server import (
     check_dirsrv, ScriptError, get_fqdn, verify_fqdn, BadHostError,
     validate_domain_name, load_pkcs12, IPA_PYTHON_VERSION,
     encode_certificate, check_available_memory, getargspec, adtrustinstance,
-    get_min_idstart
+    get_min_idstart, SerialNumber
 )
 from ansible.module_utils import six
 
@@ -369,6 +373,8 @@ def main():
                                      elements='str', default=None),
             subject_base=dict(required=False, type='str'),
             ca_subject=dict(required=False, type='str'),
+            random_serial_numbers=dict(required=False, type='bool',
+                                       default=False),
             # ca_signing_algorithm
             # dns
             allow_zone_overlap=dict(required=False, type='bool',
@@ -456,6 +462,8 @@ def main():
         'external_cert_files')
     options.subject_base = ansible_module.params.get('subject_base')
     options.ca_subject = ansible_module.params.get('ca_subject')
+    options._random_serial_numbers = ansible_module.params.get(
+        'random_serial_numbers')
     # ca_signing_algorithm
     # dns
     options.allow_zone_overlap = ansible_module.params.get(
@@ -513,6 +521,12 @@ def main():
                 ansible_module.fail_json(
                     msg="pki_config_override: %s" % str(e))
 
+    # Check if Random Serial Numbers v3 is available
+    if options._random_serial_numbers and SerialNumber is None:
+        ansible_module.fail_json(
+            msg="Random Serial Numbers is not supported for this IPA version"
+        )
+
     # default values ########################################################
 
     # idstart and idmax
@@ -1147,42 +1161,45 @@ def main():
         pkinit_pkcs12_info = ("/etc/ipa/.tmp_pkcs12_pkinit", pkinit_pin)
         pkinit_ca_cert = encode_certificate(pkinit_ca_cert)
 
-    ansible_module.exit_json(changed=False,
-                             ipa_python_version=IPA_PYTHON_VERSION,
-                             # basic
-                             domain=options.domain_name,
-                             realm=realm_name,
-                             hostname=host_name,
-                             _hostname_overridden=bool(options.host_name),
-                             no_host_dns=options.no_host_dns,
-                             # server
-                             setup_adtrust=options.setup_adtrust,
-                             setup_kra=options.setup_kra,
-                             setup_ca=options.setup_ca,
-                             idstart=options.idstart,
-                             idmax=options.idmax,
-                             no_pkinit=options.no_pkinit,
-                             # ssl certificate
-                             _dirsrv_pkcs12_info=dirsrv_pkcs12_info,
-                             _dirsrv_ca_cert=dirsrv_ca_cert,
-                             _http_pkcs12_info=http_pkcs12_info,
-                             _http_ca_cert=http_ca_cert,
-                             _pkinit_pkcs12_info=pkinit_pkcs12_info,
-                             _pkinit_ca_cert=pkinit_ca_cert,
-                             # certificate system
-                             external_ca=options.external_ca,
-                             external_ca_type=options.external_ca_type,
-                             external_ca_profile=options.external_ca_profile,
-                             # ad trust
-                             rid_base=options.rid_base,
-                             secondary_rid_base=options.secondary_rid_base,
-                             # client
-                             ntp_servers=options.ntp_servers,
-                             ntp_pool=options.ntp_pool,
-                             # additional
-                             _installation_cleanup=_installation_cleanup,
-                             domainlevel=options.domainlevel,
-                             sid_generation_always=sid_generation_always)
+    ansible_module.exit_json(
+        changed=False,
+        ipa_python_version=IPA_PYTHON_VERSION,
+        # basic
+        domain=options.domain_name,
+        realm=realm_name,
+        hostname=host_name,
+        _hostname_overridden=bool(options.host_name),
+        no_host_dns=options.no_host_dns,
+        # server
+        setup_adtrust=options.setup_adtrust,
+        setup_kra=options.setup_kra,
+        setup_ca=options.setup_ca,
+        idstart=options.idstart,
+        idmax=options.idmax,
+        no_pkinit=options.no_pkinit,
+        # ssl certificate
+        _dirsrv_pkcs12_info=dirsrv_pkcs12_info,
+        _dirsrv_ca_cert=dirsrv_ca_cert,
+        _http_pkcs12_info=http_pkcs12_info,
+        _http_ca_cert=http_ca_cert,
+        _pkinit_pkcs12_info=pkinit_pkcs12_info,
+        _pkinit_ca_cert=pkinit_ca_cert,
+        # certificate system
+        external_ca=options.external_ca,
+        external_ca_type=options.external_ca_type,
+        external_ca_profile=options.external_ca_profile,
+        # ad trust
+        rid_base=options.rid_base,
+        secondary_rid_base=options.secondary_rid_base,
+        # client
+        ntp_servers=options.ntp_servers,
+        ntp_pool=options.ntp_pool,
+        # additional
+        _installation_cleanup=_installation_cleanup,
+        domainlevel=options.domainlevel,
+        sid_generation_always=sid_generation_always,
+        random_serial_numbers=options._random_serial_numbers,
+    )
 
 
 if __name__ == '__main__':
diff --git a/roles/ipaserver/module_utils/ansible_ipa_server.py b/roles/ipaserver/module_utils/ansible_ipa_server.py
index 6dec4bc2..80e2042c 100644
--- a/roles/ipaserver/module_utils/ansible_ipa_server.py
+++ b/roles/ipaserver/module_utils/ansible_ipa_server.py
@@ -44,7 +44,7 @@ __all__ = ["IPAChangeConf", "certmonger", "sysrestore", "root_logger",
            "check_available_memory", "getargspec", "get_min_idstart",
            "paths", "api", "ipautil", "adtrust_imported", "NUM_VERSION",
            "time_service", "kra_imported", "dsinstance", "IPA_PYTHON_VERSION",
-           "NUM_VERSION"]
+           "NUM_VERSION", "SerialNumber"]
 
 import sys
 import logging
@@ -203,6 +203,13 @@ try:
         except ImportError:
             get_min_idstart = None
 
+        # SerialNumber is defined in versions 4.10 and later and is
+        # used by Random Serian Number v3.
+        try:
+            from ipalib.parameters import SerialNumber
+        except ImportError:
+            SerialNumber = None
+
     else:
         # IPA version < 4.5
 
diff --git a/roles/ipaserver/tasks/install.yml b/roles/ipaserver/tasks/install.yml
index 50b4876f..9c55c30d 100644
--- a/roles/ipaserver/tasks/install.yml
+++ b/roles/ipaserver/tasks/install.yml
@@ -108,6 +108,7 @@
     external_cert_files: "{{ ipaserver_external_cert_files | default(omit) }}"
     subject_base: "{{ ipaserver_subject_base | default(omit) }}"
     ca_subject: "{{ ipaserver_ca_subject | default(omit) }}"
+    random_serial_numbers: "{{ ipaserver_random_serial_numbers | default(omit) }}"
     # ca_signing_algorithm
     ### dns ###
     allow_zone_overlap: "{{ ipaserver_allow_zone_overlap }}"
@@ -199,7 +200,7 @@
       ### additional ###
       setup_ca: "{{ result_ipaserver_test.setup_ca }}"
       sid_generation_always: "{{ result_ipaserver_test.sid_generation_always }}"
-      random_serial_numbers: no
+      random_serial_numbers: "{{ result_ipaserver_test.random_serial_numbers }}"
       _hostname_overridden: "{{ result_ipaserver_test._hostname_overridden }}"
     register: result_ipaserver_prepare
 
-- 
GitLab