Skip to content

Commit

Permalink
[edpm_image_build] Copy certificates based on regexes
Browse files Browse the repository at this point in the history
To avoid assuming the source certificate to copy into the
diskimage-builder has a specific name just use the system CAs chain and
allow the user to provide some regexes to select which CAs should be
copied.

The new module can read a PEM file (that can list many certs) and,
optionally, filter the certs by OU or CN regexes.
  • Loading branch information
pablintino authored and openshift-merge-bot[bot] committed Jan 23, 2025
1 parent 5bbd8ad commit c6ada1f
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 5 deletions.
4 changes: 4 additions & 0 deletions docs/dictionary/en-custom.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ cli
clusterimageset
clusterpool
cmd
cn
cni
cniversion
codeql
Expand Down Expand Up @@ -387,6 +388,7 @@ osds
osp
osprh
otz
ou
overprovisionratio
ovirt
ovirtmgmt
Expand All @@ -398,6 +400,7 @@ params
passwd
passwordless
pastebin
pem
pkgs
pki
png
Expand Down Expand Up @@ -449,6 +452,7 @@ rebaser
redfish
redhat
refspec
regexes
repo
repos
rgw
Expand Down
131 changes: 131 additions & 0 deletions plugins/modules/pem_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/python

# Copyright: (c) 2025, Pablo Rodriguez <pabrodri@redhat.com>
# 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()
4 changes: 4 additions & 0 deletions roles/edpm_build_images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
7 changes: 6 additions & 1 deletion roles/edpm_build_images/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 21 additions & 4 deletions roles/edpm_build_images/tasks/add_cert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions tests/integration/targets/pem_read/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/sanity/ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c6ada1f

Please sign in to comment.