diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfd7a5d97..5d8dcd404 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,10 @@ jobs: security: none database: external box: centos/stream9 + - certificate_source: custom + security: none + database: internal + box: centos/stream9 runs-on: ubuntu-24.04 env: FOREMANCTL_BASE_BOX: ${{ matrix.box }} @@ -93,6 +97,10 @@ jobs: if: contains(matrix.certificate_source, 'installer') run: | ./forge installer-certs + - name: Create custom certificates + if: matrix.certificate_source == 'custom' + run: | + ./forge custom-certs - name: Setup security mode ${{ matrix.security }} if: matrix.security != 'none' run: | @@ -110,7 +118,7 @@ jobs: ./foremanctl pull-images - name: Run deployment run: | - ./foremanctl deploy --certificate-source=${{ matrix.certificate_source }} ${{ matrix.database == 'external' && '--database-mode=external --database-host=database.example.com --database-ssl-ca $(pwd)/.var/lib/foremanctl/db-ca.crt --database-ssl-mode verify-full' || '' }} --foreman-initial-admin-password=changeme --tuning development + ./foremanctl deploy ${{ matrix.database == 'external' && '--database-mode=external --database-host=database.example.com --database-ssl-ca $(pwd)/.var/lib/foremanctl/db-ca.crt --database-ssl-mode verify-full' || '' }} ${{ matrix.certificate_source == 'custom' && '--server-certificate /root/custom-certificates/certs/quadlet.example.com.crt --server-key /root/custom-certificates/private/quadlet.example.com.key --server-ca-certificate /root/custom-certificates/certs/server-ca.crt' || '' }} --foreman-initial-admin-password=changeme --tuning development - name: Add optional feature - hammer run: | ./foremanctl deploy --add-feature hammer @@ -122,7 +130,7 @@ jobs: ./foremanctl deploy --add-feature azure-rm --add-feature google --add-feature remote-execution - name: Run tests run: | - ./forge test --pytest-args="--certificate-source=${{ matrix.certificate_source }} --database-mode=${{ matrix.database }}" + ./forge test --pytest-args="--database-mode=${{ matrix.database }} --certificate-source=${{ matrix.certificate_source }}" - name: Run smoker run: | ./forge smoker diff --git a/development/playbooks/custom-certs/custom-certs.yaml b/development/playbooks/custom-certs/custom-certs.yaml new file mode 100644 index 000000000..a3f98bcc4 --- /dev/null +++ b/development/playbooks/custom-certs/custom-certs.yaml @@ -0,0 +1,13 @@ +--- +- name: Generate custom certificates for testing + hosts: + - quadlet + become: true + vars: + certificates_ca_directory: /root/custom-certificates + certificates_ca_password: "CUSTOMCA" + certificates_ca_subject: '/CN=Custom Test CA' + certificates_hostnames: + - "{{ ansible_facts['fqdn'] }}" + roles: + - role: certificates diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index 738381cd4..1f73abf45 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -5,7 +5,7 @@ vars_files: - "../../../src/vars/defaults.yml" - "../../../src/vars/flavors/{{ flavor }}.yml" - - "../../../src/vars/{{ certificate_source }}_certificates.yml" + - "../../../src/vars/default_certificates.yml" - "../../../src/vars/images.yml" - "../../../src/vars/database.yml" - "../../../src/vars/foreman.yml" diff --git a/docs/certificates.md b/docs/certificates.md index cbcca750f..90df2351f 100644 --- a/docs/certificates.md +++ b/docs/certificates.md @@ -4,52 +4,49 @@ This document describes how certificate generation and management works in forem ## User Guide -### Certificate Sources +### How Certificates Work -foremanctl supports two certificate sources that determine how certificates are obtained: +foremanctl automatically manages certificates for all deployed services. On first deploy, it detects existing certificates or generates new ones: -**Default Source (`certificate_source: default`)** -- Automatically generates self-signed certificates during deployment -- Creates a complete PKI infrastructure with CA, server, and client certificates -- Recommended for development and testing environments +1. Fresh install — If no installer certificates exist, a self-signed CA and certificates are generated automatically. +2. Existing foreman-installer certificates — If certificates from a previous `foreman-installer` deployment exist at `/root/ssl-build/`, they are automatically adopted and normalized into `/root/certificates/`. All certificates are copied and the original directory is backed up to `/root/ssl-build.bak/` so foremanctl can manage their full lifecycle going forward. -**Installer Source (`certificate_source: installer`)** -- Uses existing certificates from a previous `foreman-installer` deployment -- Useful for migration scenarios where certificates already exist -- Certificate files must be present at expected foreman-installer paths +On subsequent deploys, existing certificates at `/root/certificates/` are reused and any new host certificates are issued as needed. ### Usage -#### Using Auto-Generated Certificates (Default) +#### Auto-Generated Certificates (Default) ```bash # Deploy with auto-generated certificates foremanctl deploy - -# Explicitly specify default certificate source -foremanctl deploy --certificate-source=default ``` -#### Using Existing Installer Certificates +No flags are needed — certificates are generated automatically on first deploy. + +#### Upgrading from foreman-installer ```bash -# Use certificates from previous foreman-installer -foremanctl deploy --certificate-source=installer +# Just deploy — installer certificates at /root/ssl-build/ are auto-detected +foremanctl deploy ``` -### Certificate Locations +foremanctl detects the existing certificates, normalizes them into its canonical structure, and manages them going forward. The original CA is preserved so existing client trust is maintained. The original `/root/ssl-build/` directory is backed up to `/root/ssl-build.bak/`. -After deployment, certificates are available at: +#### Custom Server Certificates -**Default Source:** -- CA Certificate: `/root/certificates/certs/ca.crt` -- Server Certificate: `/root/certificates/certs/.crt` -- Client Certificate: `/root/certificates/certs/-client.crt` +To use certificates signed by your own CA instead of foremanctl's self-signed certificates: -**Installer Source:** -- CA Certificate: `/root/ssl-build/katello-default-ca.crt` -- Server Certificate: `/root/ssl-build//-apache.crt` -- Client Certificate: `/root/ssl-build//-foreman-client.crt` +```bash +foremanctl deploy \ + --server-certificate /path/to/server.crt \ + --server-key /path/to/server.key \ + --server-ca-certificate /path/to/ca-bundle.crt +``` + +All three flags must be provided together. The custom server certificate, key, and CA are copied into `/root/certificates/` and used for all server-facing TLS. An internal CA is still generated (or preserved from a previous deploy) to manage client certificates and the localhost certificate. + +On subsequent deploys, the custom certificates persist — you only need to pass the flags again if you want to update them (e.g., for certificate rotation). ### CNAME Support @@ -65,71 +62,83 @@ foremanctl deploy \ When CNAMEs are specified, certificates will include all names in the Subject Alternative Name field, allowing the same certificate to be valid for multiple hostnames. -### Current Limitations +### Certificate Locations + +After deployment, certificates are at: -- Cannot provide custom certificate files during deployment -- Fixed 20-year certificate validity period -- Limited certificate customization options +``` +/root/certificates/ +├── certs/ +│ ├── ca.crt # CA certificate +│ ├── server-ca.crt # Server CA certificate +│ ├── .crt # Server certificate +│ ├── -client.crt # Client certificate +│ └── localhost.crt # Localhost certificate (for Candlepin) +├── private/ +│ ├── ca.key # CA private key +│ ├── ca.pwd # CA key password +│ ├── .key # Server private key +│ ├── -client.key # Client private key +│ └── localhost.key # Localhost private key +└── requests/ # Certificate signing requests +``` ## Internal Design ### Architecture -The certificate system uses a modular Ansible role-based approach with clear separation between generation, validation, and usage phases. +The certificate system uses a modular Ansible role-based approach with auto-detection, normalization, and clear separation between generation, validation, and usage phases. #### Certificate Role Structure ``` src/roles/certificates/ ├── tasks/ -│ ├── main.yml # Entry point - orchestrates CA and certificate generation -│ ├── ca.yml # CA certificate generation -│ └── issue.yml # Host certificate issuance -├── defaults/main.yml # Default configuration variables +│ ├── main.yml # Entry point — auto-detects source and dispatches +│ ├── setup.yml # Shared directory/config setup +│ ├── ca.yml # CA certificate generation (fresh installs) +│ ├── issue.yml # Host certificate issuance +│ ├── normalize.yml # Normalizes foreman-installer certs +│ └── custom.yml # Applies user-provided custom server certs +├── defaults/main.yml # Default configuration variables └── templates/ - ├── openssl.cnf.j2 # OpenSSL configuration template - └── serial.j2 # Serial number template + ├── openssl.cnf.j2 # OpenSSL configuration template + └── serial.j2 # Serial number template ``` -#### Certificate Generation Workflow +#### Auto-Detection Workflow -1. **CA Generation** (when `certificates_ca: true`): - - Install OpenSSL and create directory structure - - Generate 4096-bit RSA private key - - Create self-signed CA certificate (CN: "Foreman Self-signed CA", 20-year validity) +1. **Check installer path**: If `/root/ssl-build/katello-default-ca.crt` exists, normalize installer certificates into the canonical structure. +2. **Fresh install**: If no installer certificates found, generate a new self-signed CA and certificates. +3. **Custom server certificates**: If `--server-certificate` flags are provided, copy the custom server cert, key, and CA into the canonical structure, overwriting any existing server certificate. +4. **Issue certificates**: For each hostname in `certificates_hostnames`, issue server and client certificates if they don't already exist. Server certificate issuance is skipped if a custom certificate was already placed in step 3. -2. **Host Certificate Issuance** (for each hostname in `certificates_hostnames`): - - Generate 4096-bit RSA private key - - Create certificate signing request (CSR) with Subject Alternative Names - - Include primary hostname and any additional CNAMEs from `certificate_cname` - - Sign certificate with CA (includes serverAuth/clientAuth extensions) - - Generate both server and client certificates per hostname +#### Normalization + +Installer certificates are copied from `/root/ssl-build/` into the canonical `/root/certificates/` structure. The original directory is backed up to `/root/ssl-build.bak/`. This means: +- Only one variable file (`src/vars/default_certificates.yml`) is needed +- All downstream roles (httpd, foreman, candlepin, etc.) use the same paths +- The `certificates` role always runs during deployment +- The CA key is preserved, enabling foremanctl to issue new certificates using the original CA #### Variable System -Certificate paths are defined in source-specific variable files: +Certificate paths are defined in `src/vars/default_certificates.yml`: -**Default Source (`src/vars/default_certificates.yml`):** ```yaml ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" server_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}.crt" +server_ca_certificate: "{{ certificates_ca_directory }}/certs/server-ca.crt" client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}-client.crt" ``` -**Installer Source (`src/vars/installer_certificates.yml`):** -```yaml -ca_certificate: "/root/ssl-build/katello-default-ca.crt" -server_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" -client_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" -``` - #### Integration with Deployment In `src/playbooks/deploy/deploy.yaml`: -1. **Variable Loading**: Loads certificate variables based on `certificate_source` -2. **Certificate Generation**: Runs `certificates` role when `certificate_source == 'default'` -3. **Certificate Validation**: Runs `certificate_checks` role for all sources +1. **Variable Loading**: Loads `default_certificates.yml` (always the same paths) +2. **Certificate Management**: Runs `certificates` role (auto-detects, normalizes, and generates as needed) +3. **Certificate Validation**: Runs `certificate_checks` role 4. **Service Configuration**: Passes certificate paths to dependent roles #### Validation System @@ -148,14 +157,6 @@ The `certificate_checks` role uses `foreman-certificate-check` binary to validat - Validity Period: 7300 days (20 years) - Extensions: serverAuth, clientAuth, nsSGC, msSGC -**Directory Structure:** -``` -/root/certificates/ -├── certs/ # Public certificates -├── private/ # Private keys and passwords -└── requests/ # Certificate signing requests -``` - **OpenSSL Configuration:** - Custom configuration template supports SAN extensions - Multiple DNS entries supported: `subjectAltName = DNS:{{ certificates_hostname }}{% for cname in certificate_cname %},DNS:{{ cname }}{% endfor %}` diff --git a/src/playbooks/_certificate_source/metadata.obsah.yaml b/src/playbooks/_certificate_source/metadata.obsah.yaml deleted file mode 100644 index 0037a6ce4..000000000 --- a/src/playbooks/_certificate_source/metadata.obsah.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -variables: - certificate_source: - help: Where certificates are coming from. Currently default Ansible role or the foreman-installer. - choices: - - default - - installer diff --git a/src/playbooks/deploy/deploy.yaml b/src/playbooks/deploy/deploy.yaml index 89180f1bc..b07912ee5 100644 --- a/src/playbooks/deploy/deploy.yaml +++ b/src/playbooks/deploy/deploy.yaml @@ -6,7 +6,7 @@ vars_files: - "../../vars/defaults.yml" - "../../vars/flavors/{{ flavor }}.yml" - - "../../vars/{{ certificate_source }}_certificates.yml" + - "../../vars/default_certificates.yml" - "../../vars/images.yml" - "../../vars/tuning/{{ tuning }}.yml" - "../../vars/database.yml" @@ -16,12 +16,11 @@ - role: pre_install - role: checks - role: certificates - when: "certificate_source == 'default'" - role: certificate_checks vars: certificate_checks_certificate: "{{ server_certificate }}" certificate_checks_key: "{{ server_key }}" - certificate_checks_ca: "{{ ca_certificate }}" + certificate_checks_ca: "{{ server_ca_certificate }}" - role: postgresql when: - database_mode == 'internal' diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index e9aef2092..3c2398a5b 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -23,9 +23,24 @@ variables: action: append_unique type: FQDN parameter: --certificate-cname + certificates_custom_server_certificate: + help: Path to a custom server certificate to use instead of the auto-generated one. + type: AbsolutePath + parameter: --server-certificate + certificates_custom_server_key: + help: Path to the private key for the custom server certificate. + type: AbsolutePath + parameter: --server-key + certificates_custom_server_ca_certificate: + help: Path to the CA certificate that signed the custom server certificate. + type: AbsolutePath + parameter: --server-ca-certificate + +constraints: + required_together: + - [certificates_custom_server_certificate, certificates_custom_server_key, certificates_custom_server_ca_certificate] include: - - _certificate_source - _database_mode - _database_connection - _tuning diff --git a/src/roles/certificates/defaults/main.yml b/src/roles/certificates/defaults/main.yml index 2c7c67b2d..5b0416910 100644 --- a/src/roles/certificates/defaults/main.yml +++ b/src/roles/certificates/defaults/main.yml @@ -4,4 +4,5 @@ certificates_ca_directory: /root/certificates # Change this to /var/lib? certificates_ca_directory_keys: "{{ certificates_ca_directory }}/private" certificates_ca_directory_certs: "{{ certificates_ca_directory }}/certs" certificates_ca_directory_requests: "{{ certificates_ca_directory }}/requests" +certificates_ca_subject: '/CN=Foreman Self-signed CA' certificates_cnames: [] diff --git a/src/roles/certificates/tasks/ca.yml b/src/roles/certificates/tasks/ca.yml index f426dd735..b394a7d8c 100644 --- a/src/roles/certificates/tasks/ca.yml +++ b/src/roles/certificates/tasks/ca.yml @@ -1,56 +1,12 @@ --- -- name: 'Install openssl' - ansible.builtin.package: - name: openssl - state: present - -- name: 'Create certs directory' - ansible.builtin.file: - path: "{{ certificates_ca_directory_certs }}" - state: directory - mode: '0755' - -- name: 'Create keys directory' - ansible.builtin.file: - path: "{{ certificates_ca_directory_keys }}" - state: directory - mode: '0755' - -- name: 'Create requests directory' - ansible.builtin.file: - path: "{{ certificates_ca_directory_requests }}" - state: directory - mode: '0755' - -- name: 'Deploy configuration file' - ansible.builtin.template: - src: openssl.cnf.j2 - dest: "{{ certificates_ca_directory }}/openssl.cnf" - owner: root - group: root - mode: '0644' - -- name: 'Create index file' - ansible.builtin.file: - path: "{{ certificates_ca_directory }}/index.txt" - state: touch - owner: root - group: root - mode: '0644' - -- name: 'Ensure serial starting number' - ansible.builtin.template: - src: serial.j2 - dest: "{{ certificates_ca_directory }}/serial" - force: false - owner: root - group: root - mode: '0644' +- name: 'Setup certificate directory' + ansible.builtin.include_tasks: setup.yml - name: 'Create CA key password file' ansible.builtin.copy: content: "{{ certificates_ca_password }}" dest: "{{ certificates_ca_directory_keys }}/ca.pwd" + force: false owner: root group: root mode: '0600' @@ -64,9 +20,25 @@ -extensions v3_ca -days 7300 -config "{{ certificates_ca_directory }}/openssl.cnf" - -subj "/CN=Foreman Self-signed CA" + -subj "{{ certificates_ca_subject }}" -keyout "{{ certificates_ca_directory_keys }}/ca.key" -out "{{ certificates_ca_directory_certs }}/ca.crt" -passout "file:{{ certificates_ca_directory_keys }}/ca.pwd" args: creates: "{{ certificates_ca_directory_certs }}/ca.crt" + +- name: 'Copy CA as server CA certificate' + ansible.builtin.copy: + src: "{{ certificates_ca_directory_certs }}/ca.crt" + dest: "{{ certificates_ca_directory_certs }}/server-ca.crt" + remote_src: true + force: false + mode: '0444' + +- name: 'Create CA bundle' + ansible.builtin.copy: + src: "{{ certificates_ca_directory_certs }}/ca.crt" + dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" + remote_src: true + force: false + mode: '0444' diff --git a/src/roles/certificates/tasks/custom.yml b/src/roles/certificates/tasks/custom.yml new file mode 100644 index 000000000..52d53064e --- /dev/null +++ b/src/roles/certificates/tasks/custom.yml @@ -0,0 +1,28 @@ +--- +- name: Copy custom server certificate + ansible.builtin.copy: + src: "{{ certificates_custom_server_certificate }}" + dest: "{{ certificates_ca_directory_certs }}/{{ ansible_facts['fqdn'] }}.crt" + remote_src: true + mode: '0444' + +- name: Copy custom server key + ansible.builtin.copy: + src: "{{ certificates_custom_server_key }}" + dest: "{{ certificates_ca_directory_keys }}/{{ ansible_facts['fqdn'] }}.key" + remote_src: true + mode: '0440' + +- name: Copy custom server CA certificate + ansible.builtin.copy: + src: "{{ certificates_custom_server_ca_certificate }}" + dest: "{{ certificates_ca_directory_certs }}/server-ca.crt" + remote_src: true + mode: '0444' + +- name: Create CA bundle with internal CA and custom server CA + ansible.builtin.assemble: + src: "{{ certificates_ca_directory_certs }}" + dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" + regexp: '(ca|server-ca)\.crt$' + mode: '0444' diff --git a/src/roles/certificates/tasks/main.yml b/src/roles/certificates/tasks/main.yml index de400e696..ba910e94d 100644 --- a/src/roles/certificates/tasks/main.yml +++ b/src/roles/certificates/tasks/main.yml @@ -1,9 +1,24 @@ --- -- name: 'Generate CA certificate' +- name: Check if installer certificates exist + ansible.builtin.stat: + path: /root/ssl-build/katello-default-ca.crt + register: certificates_installer_ca + +- name: Normalize installer certificates + ansible.builtin.include_tasks: normalize.yml + when: certificates_installer_ca.stat.exists + +- name: Generate CA certificate ansible.builtin.include_tasks: ca.yml - when: certificates_ca + when: + - certificates_ca + - not certificates_installer_ca.stat.exists + +- name: Apply custom server certificates + ansible.builtin.include_tasks: custom.yml + when: certificates_custom_server_certificate is defined -- name: 'Issue other certificates' +- name: Issue host certificates ansible.builtin.include_tasks: issue.yml when: certificates_hostnames is defined with_items: "{{ certificates_hostnames }}" diff --git a/src/roles/certificates/tasks/normalize.yml b/src/roles/certificates/tasks/normalize.yml new file mode 100644 index 000000000..480425c6a --- /dev/null +++ b/src/roles/certificates/tasks/normalize.yml @@ -0,0 +1,102 @@ +--- +- name: 'Setup certificate directory' + ansible.builtin.include_tasks: setup.yml + +- name: Copy CA certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.crt + dest: "{{ certificates_ca_directory_certs }}/ca.crt" + remote_src: true + force: false + mode: '0444' + +- name: Copy server CA certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-server-ca.crt + dest: "{{ certificates_ca_directory_certs }}/server-ca.crt" + remote_src: true + force: false + mode: '0444' + +- name: Create CA bundle from installer certificates + ansible.builtin.assemble: + src: "{{ certificates_ca_directory_certs }}" + dest: "{{ certificates_ca_directory_certs }}/ca-bundle.crt" + regexp: '(ca|server-ca)\.crt$' + mode: '0444' + +- name: Copy CA key from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.key + dest: "{{ certificates_ca_directory_keys }}/ca.key" + remote_src: true + force: false + mode: '0440' + +- name: Copy CA password from installer + ansible.builtin.copy: + src: /root/ssl-build/katello-default-ca.pwd + dest: "{{ certificates_ca_directory_keys }}/ca.pwd" + remote_src: true + force: false + mode: '0440' + +- name: Copy server certificate from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" + dest: "{{ certificates_ca_directory_certs }}/{{ ansible_facts['fqdn'] }}.crt" + remote_src: true + force: false + mode: '0444' + +- name: Copy server key from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.key" + dest: "{{ certificates_ca_directory_keys }}/{{ ansible_facts['fqdn'] }}.key" + remote_src: true + force: false + mode: '0440' + +- name: Copy client certificate from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" + dest: "{{ certificates_ca_directory_certs }}/{{ ansible_facts['fqdn'] }}-client.crt" + remote_src: true + force: false + mode: '0444' + +- name: Copy client key from installer + ansible.builtin.copy: + src: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.key" + dest: "{{ certificates_ca_directory_keys }}/{{ ansible_facts['fqdn'] }}-client.key" + remote_src: true + force: false + mode: '0440' + +- name: Copy localhost certificate from installer + ansible.builtin.copy: + src: /root/ssl-build/localhost/localhost-tomcat.crt + dest: "{{ certificates_ca_directory_certs }}/localhost.crt" + remote_src: true + force: false + mode: '0444' + +- name: Copy localhost key from installer + ansible.builtin.copy: + src: /root/ssl-build/localhost/localhost-tomcat.key + dest: "{{ certificates_ca_directory_keys }}/localhost.key" + remote_src: true + force: false + mode: '0440' + +- name: Backup installer certificate directory + ansible.builtin.copy: + src: /root/ssl-build/ + dest: /root/ssl-build.bak/ + remote_src: true + mode: preserve + +- name: Remove original installer certificate directory + ansible.builtin.file: + path: /root/ssl-build + state: absent diff --git a/src/roles/certificates/tasks/setup.yml b/src/roles/certificates/tasks/setup.yml new file mode 100644 index 000000000..4d87b214f --- /dev/null +++ b/src/roles/certificates/tasks/setup.yml @@ -0,0 +1,48 @@ +--- +- name: 'Install openssl' + ansible.builtin.package: + name: openssl + state: present + +- name: 'Create certs directory' + ansible.builtin.file: + path: "{{ certificates_ca_directory_certs }}" + state: directory + mode: '0755' + +- name: 'Create keys directory' + ansible.builtin.file: + path: "{{ certificates_ca_directory_keys }}" + state: directory + mode: '0755' + +- name: 'Create requests directory' + ansible.builtin.file: + path: "{{ certificates_ca_directory_requests }}" + state: directory + mode: '0755' + +- name: 'Deploy configuration file' + ansible.builtin.template: + src: openssl.cnf.j2 + dest: "{{ certificates_ca_directory }}/openssl.cnf" + owner: root + group: root + mode: '0644' + +- name: 'Create index file' + ansible.builtin.file: + path: "{{ certificates_ca_directory }}/index.txt" + state: touch + owner: root + group: root + mode: '0644' + +- name: 'Ensure serial starting number' + ansible.builtin.template: + src: serial.j2 + dest: "{{ certificates_ca_directory }}/serial" + force: false + owner: root + group: root + mode: '0644' diff --git a/src/roles/foreman_proxy/tasks/certs.yaml b/src/roles/foreman_proxy/tasks/certs.yaml index fbb504ecd..eff34e88a 100644 --- a/src/roles/foreman_proxy/tasks/certs.yaml +++ b/src/roles/foreman_proxy/tasks/certs.yaml @@ -2,7 +2,7 @@ - name: Create the podman secret for Foreman Proxy CA certificate containers.podman.podman_secret: name: foreman-proxy-ssl-ca - path: "{{ server_ca_certificate }}" + path: "{{ ca_certificate }}" state: present notify: - Restart Foreman Proxy diff --git a/src/vars/base.yaml b/src/vars/base.yaml index b19d12044..40911084b 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -15,7 +15,7 @@ candlepin_tomcat_certificate: "{{ localhost_certificate }}" candlepin_client_key: "{{ client_key }}" candlepin_client_certificate: "{{ client_certificate }}" -foreman_ca_certificate: "{{ server_ca_certificate }}" +foreman_ca_certificate: "{{ ca_bundle }}" foreman_client_key: "{{ client_key }}" foreman_client_certificate: "{{ client_certificate }}" foreman_plugins: "{{ enabled_features | features_to_foreman_plugins }}" diff --git a/src/vars/default_certificates.yml b/src/vars/default_certificates.yml index 09f47c5c9..a70f33583 100644 --- a/src/vars/default_certificates.yml +++ b/src/vars/default_certificates.yml @@ -5,7 +5,8 @@ ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" ca_key: "{{ certificates_ca_directory }}/private/ca.key" server_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}.crt" server_key: "{{ certificates_ca_directory }}/private/{{ ansible_facts['fqdn'] }}.key" -server_ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" +server_ca_certificate: "{{ certificates_ca_directory }}/certs/server-ca.crt" +ca_bundle: "{{ certificates_ca_directory }}/certs/ca-bundle.crt" client_certificate: "{{ certificates_ca_directory }}/certs/{{ ansible_facts['fqdn'] }}-client.crt" client_key: "{{ certificates_ca_directory }}/private/{{ ansible_facts['fqdn'] }}-client.key" client_ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt" diff --git a/src/vars/defaults.yml b/src/vars/defaults.yml index 7fae5df26..2686b5f77 100644 --- a/src/vars/defaults.yml +++ b/src/vars/defaults.yml @@ -1,5 +1,4 @@ --- -certificate_source: default database_mode: internal tuning: default flavor: katello diff --git a/src/vars/installer_certificates.yml b/src/vars/installer_certificates.yml deleted file mode 100644 index c6ab83af3..000000000 --- a/src/vars/installer_certificates.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -ca_key_password: "/root/ssl-build/katello-default-ca.pwd" -ca_certificate: "/root/ssl-build/katello-default-ca.crt" -ca_key: "/root/ssl-build/katello-default-ca.key" -server_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.crt" -server_key: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-apache.key" -server_ca_certificate: "/root/ssl-build/katello-server-ca.crt" -client_certificate: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.crt" -client_key: "/root/ssl-build/{{ ansible_facts['fqdn'] }}/{{ ansible_facts['fqdn'] }}-foreman-client.key" -client_ca_certificate: "{{ ca_certificate }}" -localhost_key: "/root/ssl-build/localhost/localhost-tomcat.key" -localhost_certificate: "/root/ssl-build/localhost/localhost-tomcat.crt" diff --git a/tests/certificates_test.py b/tests/certificates_test.py index 2933a861c..3efaf9c55 100644 --- a/tests/certificates_test.py +++ b/tests/certificates_test.py @@ -6,9 +6,27 @@ def certificate_info(server, certificate): openssl_result = server.run(f"openssl x509 -in {certificate} -noout -enddate -dateopt iso_8601 -subject -issuer") return dict([x.split('=', 1) for x in openssl_result.stdout.splitlines()]) -@pytest.mark.parametrize("certificate_type", ['ca_certificate', 'server_certificate', 'client_certificate', 'localhost_certificate']) +@pytest.mark.parametrize("certificate_type", ['ca_certificate', 'server_ca_certificate', 'server_certificate', 'client_certificate', 'localhost_certificate']) def test_certificate_expiry(server, certificates, certificate_type): openssl_data = certificate_info(server, certificates[certificate_type]) not_after = dateutil.parser.parse(openssl_data['notAfter']) now = datetime.datetime.now(tz=not_after.tzinfo) assert not_after - now > datetime.timedelta(days=365*10) + +def test_custom_server_ca_differs_from_internal_ca(server, certificates, custom_certificates): + ca_info = certificate_info(server, certificates['ca_certificate']) + server_ca_info = certificate_info(server, certificates['server_ca_certificate']) + assert ca_info['subject'] != server_ca_info['subject'], \ + "Custom server CA should have a different subject than the internal CA" + +def test_custom_server_certificate_issued_by_custom_ca(server, certificates, custom_certificates): + server_info = certificate_info(server, certificates['server_certificate']) + server_ca_info = certificate_info(server, certificates['server_ca_certificate']) + assert server_info['issuer'] == server_ca_info['subject'], \ + "Server certificate should be issued by the custom server CA" + +def test_client_certificate_issued_by_internal_ca(server, certificates, custom_certificates): + client_info = certificate_info(server, certificates['client_certificate']) + ca_info = certificate_info(server, certificates['ca_certificate']) + assert client_info['issuer'] == ca_info['subject'], \ + "Client certificate should still be issued by the internal CA" diff --git a/tests/conftest.py b/tests/conftest.py index d9ed914cc..0281b7f68 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,8 +14,8 @@ def pytest_addoption(parser): - parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'installer'), help="Where to obtain certificates from") parser.addoption("--database-mode", action="store", default="internal", choices=('internal', 'external'), help="Whether the database is internal or external") + parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'installer', 'custom'), help="Certificate source used during deployment") @pytest.fixture(scope="module") @@ -44,15 +44,23 @@ def client_fqdn(client_hostname): @pytest.fixture(scope="module") -def certificates(pytestconfig, server_fqdn): - source = pytestconfig.getoption("certificate_source") +def certificates(server_fqdn): env = Environment(loader=FileSystemLoader("."), autoescape=select_autoescape()) - template = env.get_template(f"./src/vars/{source}_certificates.yml") + template = env.get_template("./src/vars/default_certificates.yml") context = {'certificates_ca_directory': '/root/certificates', 'ansible_facts': {'fqdn': server_fqdn}} return yaml.safe_load(template.render(context)) +@pytest.fixture(scope="module") +def certificate_source(pytestconfig): + return pytestconfig.getoption("certificate_source") + +@pytest.fixture(scope="module") +def custom_certificates(certificate_source): + if certificate_source != 'custom': + pytest.skip("Only applies to custom certificate deployments") + @pytest.fixture(scope="module") def database_mode(pytestconfig): return pytestconfig.getoption("database_mode")