diff --git a/docs/dictionary/en-custom.txt b/docs/dictionary/en-custom.txt index c4751c2a6d..568f01e951 100644 --- a/docs/dictionary/en-custom.txt +++ b/docs/dictionary/en-custom.txt @@ -75,6 +75,7 @@ cli clusterimageset clusterpool cmd +cn cni cniversion codeql @@ -387,6 +388,7 @@ osds osp osprh otz +ou overprovisionratio ovirt ovirtmgmt @@ -398,6 +400,7 @@ params passwd passwordless pastebin +pem pkgs pki png @@ -449,6 +452,7 @@ rebaser redfish redhat refspec +regexes repo repos rgw diff --git a/plugins/modules/pem_read.py b/plugins/modules/pem_read.py new file mode 100644 index 0000000000..5f89856894 --- /dev/null +++ b/plugins/modules/pem_read.py @@ -0,0 +1,131 @@ +#!/usr/bin/python + +# Copyright: (c) 2025, Pablo Rodriguez +# Apache License Version 2.0 (see LICENSE) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r""" +--- +module: pem_read +short_description: Reads a PEM file (that can list many certs) + +description: +- Reads a PEM file (that can list many certs) +- OU or CN regexes can be provided to filter out the list + +author: + - Pablo Rodriguez (@pablintino) + +options: + path: + description: + - The path to the certificate file to be read + required: True + type: str + ou_filter: + description: + - Regex that, if given, used to filter the list of certs by OU. + required: False + type: str + cn_filter: + description: + - Regex that, if given, used to filter the list of certs by CN. + required: False + type: str + +""" + +EXAMPLES = r""" +- name: Get pem certs from crt file + pem_read: + path: "/etc/ssl/certs/ca-certificates.crt" + register: _certs + +- name: Get pem certs from crt file by OU + pem_read: + path: "/etc/ssl/certs/ca-certificates.crt" + ou_filter: "Red Hat" + register: _certs2 +""" + +RETURN = r""" +certs: + description: The list of PEM files + type: list + returned: returned request +""" + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +import re +import typing + +try: + from cryptography import x509 + from cryptography.x509.oid import NameOID + from cryptography.hazmat.primitives import serialization + + python_cryptography_installed = True +except ImportError: + python_cryptography_installed = False + + +def filter_certs(input_certs, ou_filter=None, cn_filter=None): + certs = [] + for cert in input_certs: + ou = cert.subject.get_attributes_for_oid(NameOID.ORGANIZATIONAL_UNIT_NAME) + cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + if ( + (ou and ou_filter and re.search(ou_filter, ou[0].value)) + or (cn and cn_filter and re.search(cn_filter, cn[0].value)) + or (not ou_filter and not cn_filter) + ): + certs.append(cert) + return certs + + +def main(): + module_args = { + "path": {"type": "str", "required": True}, + "ou_filter": {"type": "str", "required": False}, + "cn_filter": {"type": "str", "required": False}, + } + + result = {"changed": False, "certs": []} + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=False) + if not python_cryptography_installed: + module.fail_json(msg=missing_required_lib("cryptography")) + + path = module.params["path"] + ou_filter = module.params.get("ou_filter", None) + cn_filter = module.params.get("cn_filter", None) + + try: + with open(path, "rb") as f: + certs = filter_certs( + x509.load_pem_x509_certificates(f.read()), + ou_filter=ou_filter, + cn_filter=cn_filter, + ) + result["certs"].extend( + [ + cert.public_bytes(encoding=serialization.Encoding.PEM).decode( + "utf8" + ) + for cert in certs + ] + ) + + except Exception as e: + module.fail_json(msg=f"Error fetching reading a PEM file {str(e)}") + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/roles/edpm_build_images/README.md b/roles/edpm_build_images/README.md index 4e0e5b5360..7b9c23d424 100644 --- a/roles/edpm_build_images/README.md +++ b/roles/edpm_build_images/README.md @@ -31,6 +31,10 @@ None * `cifmw_edpm_build_images_cert_install`: (Boolean) Whether to install cert in the image. Default: false * `cifmw_edpm_build_images_base_image`: (String) Base image to package the edpm and ipa qcow2 images into the container images for rhel distro. +* `cifmw_edpm_build_images_cert_dest`: (String) The path where the certificates should be placed inside the image builder. Default: `/etc/pki/ca-trust/source/anchors/edpm-build-images.crt`. +* `cifmw_edpm_build_images_cert_filter_ou`: (String) Optional. If given, filter out by OU the source certs using this regex. +* `cifmw_edpm_build_images_cert_filter_cn`: (String) Optional. If given, filter out by CN the source certs using this regex. + ## Example ```YAML --- diff --git a/roles/edpm_build_images/defaults/main.yml b/roles/edpm_build_images/defaults/main.yml index 0f00a84248..a2dc5bff5b 100644 --- a/roles/edpm_build_images/defaults/main.yml +++ b/roles/edpm_build_images/defaults/main.yml @@ -53,6 +53,11 @@ cifmw_edpm_build_images_dry_run: false cifmw_edpm_build_images_push_registry: 'quay.rdoproject.org' cifmw_edpm_build_images_push_registry_namespace: 'podified-master-centos9' cifmw_edpm_build_images_push_container_images: false -cifmw_edpm_build_images_cert_path: /etc/pki/ca-trust/source/anchors/rh.crt +cifmw_edpm_build_images_cert_path: "/etc/ssl/certs/ca-certificates.crt" +cifmw_edpm_build_images_cert_dest: "/etc/pki/ca-trust/source/anchors/edpm-build-images.crt" cifmw_edpm_build_images_cert_install: false cifmw_edpm_build_images_base_image: 'quay.io/centos/centos:stream9-minimal' + +# Optional parameters +# cifmw_edpm_build_images_cert_filter_ou: omit +# cifmw_edpm_build_images_cert_filter_cn: omit diff --git a/roles/edpm_build_images/tasks/add_cert.yml b/roles/edpm_build_images/tasks/add_cert.yml index 97853c7250..b85965b832 100644 --- a/roles/edpm_build_images/tasks/add_cert.yml +++ b/roles/edpm_build_images/tasks/add_cert.yml @@ -2,12 +2,29 @@ - name: Check if cert exits ansible.builtin.stat: path: "{{ cifmw_edpm_build_images_cert_path }}" - register: cert_path + register: _cifmw_edpm_build_images_stat - name: Install neccessary rpm and customize image to push correct certs in Image. - when: - - cert_path.stat.exists|bool + vars: + _cifmw_edpm_build_images_cert_source: >- + {{ + [cifmw_edpm_build_images_basedir, 'edpm-build-images.crt'] | path_join + }} + when: _cifmw_edpm_build_images_stat.stat.exists block: + - name: Read the certificates + cifmw.general.pem_read: + path: "{{ cifmw_edpm_build_images_cert_path }}" + ou_filter: "{{ cifmw_edpm_build_images_cert_filter_ou | default(omit) }}" + cn_filter: "{{ cifmw_edpm_build_images_cert_filter_cn | default(omit) }}" + register: _cifmw_edpm_build_images_certs + + - name: Dump the certificates + ansible.builtin.copy: + dest: "{{ _cifmw_edpm_build_images_cert_source }}" + content: "{{ _cifmw_edpm_build_images_certs.certs | join('\n') }}" + mode: "0644" + - name: Install libguestfs packages tags: - bootstrap @@ -22,7 +39,7 @@ - name: Add cert if it exists ansible.builtin.command: > - virt-customize -a {{ cifmw_edpm_build_images_basedir }}/{{ cifmw_discovered_image_name }} + virt-customize -a {{ _cifmw_edpm_build_images_cert_source }}/{{ cifmw_edpm_build_images_cert_dest }} --upload {{ cifmw_edpm_build_images_cert_path }}:{{ cifmw_edpm_build_images_cert_path }} --run-command 'update-ca-trust' environment: diff --git a/tests/integration/targets/pem_read/tasks/main.yml b/tests/integration/targets/pem_read/tasks/main.yml new file mode 100644 index 0000000000..a63780447d --- /dev/null +++ b/tests/integration/targets/pem_read/tasks/main.yml @@ -0,0 +1,61 @@ +--- +# Create some selfsigned certs to test +- name: Create a temporal directory for the certs + ansible.builtin.tempfile: + state: directory + register: _tmp_dir + +- name: Create private key (RSA, 4096 bits) + community.crypto.openssl_privatekey: + path: "{{ [_tmp_dir.path, 'certificate.key'] | path_join }}" + +- name: Generate the CSRs + community.crypto.openssl_csr_pipe: + privatekey_path: "{{ [_tmp_dir.path, 'certificate.key'] | path_join }}" + organizational_unit_name: "{{ item.ou }}" + common_name: "{{ item.cn }}" + register: _csrs + loop: + - cn: common-name-test-1 + ou: "Some OU" + - cn: common-name-test-2 + ou: "Test OU 1" + - cn: another-cert-cn + ou: "Test OU 2" + - cn: cn-1 + ou: "Not following any pattern" + +- name: Create simple self-signed certificate + community.crypto.x509_certificate: + path: "{{ [_tmp_dir.path, (idx | string) + '.crt'] | path_join }}" + privatekey_path: "{{ [_tmp_dir.path, 'certificate.key'] | path_join }}" + provider: selfsigned + csr_content: "{{ item.csr }}" + loop: "{{ _csrs.results }}" + loop_control: + index_var: idx + label: "{{ item.subject }}" + register: _certs + +- name: Create simple self-signed certificate + ansible.builtin.shell: + chdir: "{{ _tmp_dir.path }}" + cmd: >- + cat *.crt > original-pem.crt + +- name: Test the module + cifmw.general.pem_read: + path: "{{ [_tmp_dir.path, 'original-pem.crt'] | path_join }}" + ou_filter: "^Some\\s?OU" + cn_filter: "cert-cn$" + register: _result + +- name: Ensure we got the expected certificates + vars: + _cert_1_content: "{{ lookup('file', [_tmp_dir.path, '0.crt'] | path_join) }}" + _cert_2_content: "{{ lookup('file', [_tmp_dir.path, '2.crt'] | path_join) }}" + ansible.builtin.assert: + that: + - _result.certs | length == 2 + - _result.certs[0] | trim == _cert_1_content | trim + - _result.certs[1] | trim == _cert_2_content | trim diff --git a/tests/sanity/ignore.txt b/tests/sanity/ignore.txt index a8db927abb..dfa817c8c8 100644 --- a/tests/sanity/ignore.txt +++ b/tests/sanity/ignore.txt @@ -4,3 +4,4 @@ plugins/modules/tempest_list_allowed.py validate-modules:missing-gplv3-license # plugins/modules/tempest_list_skipped.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/cephx_key.py validate-modules:missing-gplv3-license # ignore license check plugins/modules/url_request.py validate-modules:missing-gplv3-license # ignore license check +plugins/modules/pem_read.py validate-modules:missing-gplv3-license # ignore license check