Skip to content

Commit 2c472fb

Browse files
committed
Add role and playbook to generate a certs tarball
Signed-off-by: Eric D. Helms <ericdhelms@gmail.com>
1 parent 5a2eb8b commit 2c472fb

File tree

7 files changed

+207
-0
lines changed

7 files changed

+207
-0
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ jobs:
111111
- name: Run deployment
112112
run: |
113113
./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
114+
- name: Generate certificate bundle for second system
115+
run: |
116+
./foremanctl certificate-bundle --certificate-source=${{ matrix.certificate_source }} proxy.example.com
114117
- name: Add optional feature - hammer
115118
run: |
116119
./foremanctl deploy --add-feature hammer

docs/certificates.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,37 @@ foremanctl deploy \
6565

6666
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.
6767

68+
### Generating Certificate Bundles
69+
70+
foremanctl can generate certificate bundles for additional hostnames (e.g. proxies). A certificate bundle is a tarball containing all the certificate files a host needs, structured in the format expected by foremanctl.
71+
72+
```bash
73+
# Generate a certificate bundle using the default (self-signed) CA
74+
foremanctl certificate-bundle proxy.example.com
75+
76+
# Generate a certificate bundle using installer certificates
77+
foremanctl certificate-bundle --certificate-source=installer proxy.example.com
78+
```
79+
80+
The generated tarball is written to `/root/<hostname>.tar.gz` and contains:
81+
82+
```
83+
ssl-build/
84+
├── katello-server-ca.crt
85+
├── katello-default-ca.crt
86+
└── <hostname>/
87+
├── <hostname>-apache.crt
88+
├── <hostname>-apache.key
89+
├── <hostname>-foreman-proxy.crt
90+
├── <hostname>-foreman-proxy.key
91+
├── <hostname>-foreman-proxy-client.crt
92+
├── <hostname>-foreman-proxy-client.key
93+
├── <hostname>-puppet-client.crt
94+
└── <hostname>-puppet-client.key
95+
```
96+
97+
The bundle uses the server certificate and key for both apache and foreman-proxy, and the client certificate and key for both foreman-proxy-client and puppet-client.
98+
6899
### Current Limitations
69100

70101
- Cannot provide custom certificate files during deployment
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
- name: Generate a certificate bundle for a hostname
3+
hosts:
4+
- quadlet
5+
become: true
6+
vars_files:
7+
- "../../vars/defaults.yml"
8+
vars:
9+
certificates_ca: false
10+
certificates_hostnames:
11+
- "{{ hostname }}"
12+
roles:
13+
- role: certificates
14+
when: "certificate_source == 'default'"
15+
- role: foreman_installer_certificates
16+
when: "certificate_source == 'installer'"
17+
- role: certificate_bundle
18+
vars:
19+
certificate_bundle_hostname: "{{ hostname }}"
20+
certificate_bundle_ca_certificate: "{{ certificates_ca_directory }}/certs/ca.crt"
21+
certificate_bundle_server_certificate: "{{ certificates_ca_directory }}/certs/{{ hostname }}.crt"
22+
certificate_bundle_server_key: "{{ certificates_ca_directory }}/private/{{ hostname }}.key"
23+
certificate_bundle_client_certificate: "{{ certificates_ca_directory }}/certs/{{ hostname }}-client.crt"
24+
certificate_bundle_client_key: "{{ certificates_ca_directory }}/private/{{ hostname }}-client.key"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
help: |
3+
Generate a certificate bundle
4+
5+
variables:
6+
hostname:
7+
parameter: hostname
8+
help: Hostname to generate a certificate bundle for that will be the common name.
9+
10+
include:
11+
- _certificate_source
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
certificate_bundle_output_directory: /root
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
- name: Create temporary directory
3+
ansible.builtin.tempfile:
4+
state: directory
5+
suffix: certificate-build
6+
register: certificate_bundle_build_directory
7+
8+
- name: Create directory structure
9+
ansible.builtin.file:
10+
state: directory
11+
path: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ certificate_bundle_hostname }}"
12+
mode: '0755'
13+
14+
- name: Copy CA certificate
15+
ansible.builtin.copy:
16+
src: "{{ certificate_bundle_ca_certificate }}"
17+
dest: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ item }}"
18+
remote_src: true
19+
mode: '0444'
20+
loop:
21+
- katello-server-ca.crt
22+
- katello-default-ca.crt
23+
24+
- name: Copy server certificate
25+
ansible.builtin.copy:
26+
src: "{{ certificate_bundle_server_certificate }}"
27+
dest: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ certificate_bundle_hostname }}/{{ certificate_bundle_hostname }}-{{ item }}"
28+
remote_src: true
29+
mode: '0444'
30+
loop:
31+
- apache.crt
32+
- foreman-proxy.crt
33+
34+
- name: Copy server key
35+
ansible.builtin.copy:
36+
src: "{{ certificate_bundle_server_key }}"
37+
dest: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ certificate_bundle_hostname }}/{{ certificate_bundle_hostname }}-{{ item }}"
38+
remote_src: true
39+
mode: '0440'
40+
loop:
41+
- apache.key
42+
- foreman-proxy.key
43+
44+
- name: Copy client certificate
45+
ansible.builtin.copy:
46+
src: "{{ certificate_bundle_client_certificate }}"
47+
dest: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ certificate_bundle_hostname }}/{{ certificate_bundle_hostname }}-{{ item }}"
48+
remote_src: true
49+
mode: '0444'
50+
loop:
51+
- foreman-proxy-client.crt
52+
- puppet-client.crt
53+
54+
- name: Copy client key
55+
ansible.builtin.copy:
56+
src: "{{ certificate_bundle_client_key }}"
57+
dest: "{{ certificate_bundle_build_directory.path }}/ssl-build/{{ certificate_bundle_hostname }}/{{ certificate_bundle_hostname }}-{{ item }}"
58+
remote_src: true
59+
mode: '0440'
60+
loop:
61+
- foreman-proxy-client.key
62+
- puppet-client.key
63+
64+
- name: Create tarball
65+
community.general.archive:
66+
path: "{{ certificate_bundle_build_directory.path }}/ssl-build"
67+
dest: "{{ certificate_bundle_output_directory }}/{{ certificate_bundle_hostname }}.tar.gz"
68+
mode: '0640'

tests/certificate_bundle_test.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import pytest
2+
3+
4+
HOSTNAME = 'proxy.example.com'
5+
TARBALL = f'/root/{HOSTNAME}.tar.gz'
6+
7+
EXPECTED_CA_FILES = [
8+
'ssl-build/katello-server-ca.crt',
9+
'ssl-build/katello-default-ca.crt',
10+
]
11+
12+
EXPECTED_SERVER_FILES = [
13+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-apache.crt',
14+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-apache.key',
15+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy.crt',
16+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy.key',
17+
]
18+
19+
EXPECTED_CLIENT_FILES = [
20+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy-client.crt',
21+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy-client.key',
22+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-puppet-client.crt',
23+
f'ssl-build/{HOSTNAME}/{HOSTNAME}-puppet-client.key',
24+
]
25+
26+
27+
@pytest.fixture(scope="module")
28+
def tarball_members(server):
29+
result = server.run(f'tar tzf {TARBALL}')
30+
assert result.succeeded, f'Tarball {TARBALL} not found. Run foremanctl certificate-bundle {HOSTNAME} before tests.'
31+
return result.stdout.strip().splitlines()
32+
33+
34+
def test_tarball_created(server):
35+
assert server.file(TARBALL).exists
36+
37+
38+
@pytest.mark.parametrize("expected_file", EXPECTED_CA_FILES)
39+
def test_tarball_contains_ca_certificate(tarball_members, expected_file):
40+
assert expected_file in tarball_members
41+
42+
43+
@pytest.mark.parametrize("expected_file", EXPECTED_SERVER_FILES)
44+
def test_tarball_contains_server_certificate(tarball_members, expected_file):
45+
assert expected_file in tarball_members
46+
47+
48+
@pytest.mark.parametrize("expected_file", EXPECTED_CLIENT_FILES)
49+
def test_tarball_contains_client_certificate(tarball_members, expected_file):
50+
assert expected_file in tarball_members
51+
52+
53+
def test_server_certs_are_identical(server):
54+
apache = server.run(f'tar xzf {TARBALL} -O ssl-build/{HOSTNAME}/{HOSTNAME}-apache.crt')
55+
proxy = server.run(f'tar xzf {TARBALL} -O ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy.crt')
56+
assert apache.stdout == proxy.stdout
57+
58+
59+
def test_client_certs_are_identical(server):
60+
proxy_client = server.run(f'tar xzf {TARBALL} -O ssl-build/{HOSTNAME}/{HOSTNAME}-foreman-proxy-client.crt')
61+
puppet_client = server.run(f'tar xzf {TARBALL} -O ssl-build/{HOSTNAME}/{HOSTNAME}-puppet-client.crt')
62+
assert proxy_client.stdout == puppet_client.stdout
63+
64+
65+
def test_ca_certs_are_identical(server):
66+
server_ca = server.run(f'tar xzf {TARBALL} -O ssl-build/katello-server-ca.crt')
67+
default_ca = server.run(f'tar xzf {TARBALL} -O ssl-build/katello-default-ca.crt')
68+
assert server_ca.stdout == default_ca.stdout

0 commit comments

Comments
 (0)