Skip to content

Commit

Permalink
feat: manage TLS cert/key files for registry connections and validate…
Browse files Browse the repository at this point in the history
… certs

Feature: Add two new parameters:
podman_registry_certificates is a list of dict.  Each dict specifies the
certs and keys to use to connect to the specified registry using TLS and
optionally use certificate authentication.  More information can be found
in the manpage for containers-certs.d.
podman_validate_certs is a boolean which allows you to require or disable
TLS certificate checking (i.e. if you do not have a CA cert for
podman_registry_certificates and you still want to pull images from a TLS
enabled registry).  This corresponds to the parameter "validate_certs"
of the module containers.podman.podman_image.  You can also control
certificate validation by using podman_registries_conf to configure
the "insecure" parameter for a registry.

Reason: Users need to be able to configure the TLS settings for
connecting to registries.

Result: Users can connect to registries using TLS and control how
that works.

QE: tests_auth_and_security.yml has been extended for this.

Signed-off-by: Rich Megginson <[email protected]>
  • Loading branch information
richm committed Apr 18, 2024
1 parent 45992ad commit 1abe0f7
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 24 deletions.
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,77 @@ owner will be `my_user` and the mode will be `"0600"`. The directories
`/home/my_user/.config` and `/home/my_user/.config/containers` will be created
if they do not exist.

### podman_registry_certificates

This variable is a `list` of `dict` elements that allows you to manage TLS
certificates and keys used to connect to registries. The directories, formats,
and files are as described in `man containers-certs.d`. The names of the keys
used for TLS certificates and keys follow the
[system roles TLS naming conventions](https://linux-system-roles.github.io/documentation/tls_crypto_parameter_and_key_names.html). NOTE: the `client_` prefix has been dropped
here for `cert` and `private_key` because there are only clients in this context.

NOTE: You are strongly encouraged to use Ansible Vault to encrypt private keys and
any other sensitive values.

The keys of each `dict` are as follows:

* `state` - default is `present`. Use `absent` to remove files.
* `run_as_user` - This is the user that will be the owner of the files, and is
used to find the `$HOME` directory for the files. If you do not specify this,
then the global default `podman_run_as_user` value will be used. Otherwise,
`root` will be used.
* `run_as_group` - This is the group that will be the owner of the files. If
you do not specify this, then the global default `podman_run_as_group` value
will be used. Otherwise, `root` will be used.
* `registry_host` - Required - the hostname or `hostname:port` of the registry.
This will be used as the name of the directory under
`$HOME/.config/containers/certs.d` (for rootless containers) or
`/etc/containers/certs.d` (for system containers) which will hold the
certificates and keys. If using `state: absent` and all of the files are
removed, the directory will be removed.
* `cert` - name of the file in the `certs.d` directory holding the TLS client
certificate. If not specified, use the basename of `cert_src`. If that isn't
specified, use `client.cert`.
* `private_key` - name of the file in the `certs.d` directory holding the TLS
client private key. If not specified, use the basename of `private_key_src`.
If that isn't specified, use `client.key`
* `ca_cert` - name of the file in the `certs.d` directory holding the TLS CA
certificate. If not specified, use the basename of `ca_cert_src`. If that
isn't specified, use `ca.crt`
* `cert_src` - name of the file on the control node to be copied to `cert`.
* `private_key_src` - name of the file on the control node to be copied to
`private_key`.
* `ca_cert_src` - name of the file on the control node to be copied to
`ca_cert`.
* `cert_content` - contents of the certificate to be copied to `cert`.
* `private_key_content` - contents of the private key to be copied to
`private_key`.

```yaml
podman_run_as_user: root
podman_registry_certificates:
- registry_host: quay.io:5000
cert_src: client.cert
private_key_content: !vault |
$ANSIBLE_VAULT.....
ca_cert_src: ca.crt
```

This will create the directory `/etc/containers/certs.d/quay.io:5000/`, will copy
the local file `client.cert` looked up from the usual Ansible file lookup path
to `/etc/containers/certs.d/quay.io:5000/client.cert`, will copy the contents of
the Ansible Vault encrypted `private_key_content` to
`/etc/containers/certs.d/quay.io:5000/client.key`, and will copy the local file
`ca.crt` looked up from the usual Ansible file lookup path to
`/etc/containers/certs.d/quay.io:5000/ca.crt`.

### podman_validate_certs

Boolean - default is null - use this to control if pulling images from
registries will validate TLS certs or not. The default `null` means to use
whatever is the default used by the `containers.podman.podman_image` module. You
can override this on a per-spec basis using `validate_certs`.

## Variables Exported by the Role

### podman_version
Expand Down
11 changes: 11 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,14 @@ podman_registry_password: "{{ container_image_password | d('') }}"
# For more information, see "man containers-auth.json" and
# "man containers-registries.conf"
podman_credential_files: []

# This variable is a `list` of `dict` elements that allows you to manage TLS
# certificates and keys used to connect to registries
podman_registry_certificates: []

# Can set to true or false to control if pulling images from
# registries will validate the TLS certs.
# The default null means to use whatever is the default used
# by the containers.podman.podman_image module
# You can override this on a per-spec basis using validate_certs
podman_validate_certs: null
2 changes: 2 additions & 0 deletions tasks/create_update_kube_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
if __podman_registry_username | length > 0 else omit }}"
password: "{{ __podman_registry_password
if __podman_registry_password | length > 0 else omit }}"
validate_certs: "{{ (__podman_validate_certs in ['', none]) |
ternary(omit, __podman_validate_certs) }}"
register: __podman_image_updated
when: __podman_pull_image | bool
until: __podman_image_updated is success
Expand Down
2 changes: 2 additions & 0 deletions tasks/create_update_quadlet_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
if __podman_registry_username | length > 0 else omit }}"
password: "{{ __podman_registry_password
if __podman_registry_password | length > 0 else omit }}"
validate_certs: "{{ (__podman_validate_certs in ['', none]) |
ternary(omit, __podman_validate_certs) }}"
register: __podman_image_updated
when: __podman_pull_image | bool
until: __podman_image_updated is success
Expand Down
115 changes: 115 additions & 0 deletions tasks/handle_certs_d.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: MIT
---
- name: Check given registry_host
fail:
msg: >-
The given registry host {{ __podman_cert_spec_item['registry_host'] }}
is invalid - cannot be used as the directory name
when: __podman_cert_spec_item["registry_host"] is search("/")

- name: Set per-cert spec variables part 0
set_fact:
__podman_user: "{{ __podman_cert_spec_item['run_as_user'] |
d(podman_run_as_user) }}"

- name: Set per-cert spec variables part 1
set_fact:
__podman_rootless: "{{ __podman_user != 'root' }}"

- name: Check user and group information
include_tasks: handle_user_group.yml
vars:
__podman_spec_item: "{{ __podman_cert_spec_item }}"

- name: Set per-cert spec variables part 2
set_fact:
__podman_user_home_dir: "{{
ansible_facts['getent_passwd'][__podman_user][4] }}"

- name: Set per-cert spec variables part 3
set_fact:
__podman_certs_d_path: "{{ (__podman_user_home_dir ~
__podman_user_certs_d_path
if __podman_rootless else __podman_system_certs_d_path) ~
'/' ~ __podman_cert_spec_item['registry_host'] }}"

- name: Set per-cert spec variables part 4
set_fact:
__podman_cert_file_list:
- dest: "{{ __podman_certs_d_path ~ '/' ~
(__podman_cert_spec_item['cert'] | basename
if 'cert' in __podman_cert_spec_item
else __podman_cert_spec_item['cert_src'] | basename
if 'cert_src' in __podman_cert_spec_item
else 'client.cert') }}"
content: "{{ __podman_cert_spec_item['cert_content'] | d('') }}"
src: "{{ __podman_cert_spec_item['cert_src'] | d('') }}"
- dest: "{{ __podman_certs_d_path ~ '/' ~
(__podman_cert_spec_item['key'] | basename
if 'key' in __podman_cert_spec_item
else __podman_cert_spec_item['key_src'] | basename
if 'key_src' in __podman_cert_spec_item
else 'client.key') }}"
content: "{{ __podman_cert_spec_item['key_content'] | d('') }}"
src: "{{ __podman_cert_spec_item['key_src'] | d('') }}"
- dest: "{{ __podman_certs_d_path ~ '/' ~
(__podman_cert_spec_item['ca_cert'] | basename
if 'ca_cert' in __podman_cert_spec_item
else __podman_cert_spec_item['ca_cert_src'] | basename
if 'ca_cert_src' in __podman_cert_spec_item
else 'ca.crt') }}"
content: "{{ __podman_cert_spec_item['ca_cert_content'] | d('') }}"
src: "{{ __podman_cert_spec_item['ca_cert_src'] | d('') }}"
no_log: true

- name: Create TLS files
when:
- __podman_cert_spec_item["state"] | d("present") == "present"
- __podman_handle_state == "present"
block:
- name: Ensure certs.d directory
file:
path: "{{ __podman_certs_d_path }}"
state: directory
owner: "{{ __podman_user }}"
group: "{{ __podman_group }}"
mode: "0700"

- name: Ensure certs.d files
copy:
content: "{{ item.content if item.content | length > 0 else omit }}"
src: "{{ item.src if item.src | length > 0 else omit }}"
dest: "{{ item.dest }}"
owner: "{{ __podman_user }}"
group: "{{ __podman_group }}"
mode: "0600"
when: (item.content | length > 0) or (item.src | length > 0)
loop: "{{ __podman_cert_file_list }}"
no_log: true

- name: Remove TLS files
when:
- __podman_cert_spec_item["state"] | d("present") == "absent"
- __podman_handle_state == "absent"
block:
- name: Remove certs.d files
file:
path: "{{ item.dest }}"
state: absent
loop: "{{ __podman_cert_file_list }}"
no_log: true

- name: Find files in certs.d directory
find:
path: "{{ __podman_certs_d_path | dirname }}"
file_type: any
hidden: true
register: __certs_d_dir_files
no_log: true

- name: Ensure the certs.d directory is absent if empty
file:
path: "{{ __podman_certs_d_path | dirname }}"
state: absent
when: __certs_d_dir_files.matched == 0
no_log: true
10 changes: 6 additions & 4 deletions tasks/handle_credential_files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@
else __podman_credential_item['file'] }}"
__podman_credential_mode: "{{ __podman_credential_item['mode']
if 'mode' in __podman_credential_item else '0600' }}"
__podman_credential_state: "{{ __podman_credential_item['state']
if 'state' in __podman_credential_item else 'present' }}"
vars:
__authdir: "{{
ansible_facts['getent_passwd'][__podman_credential_user][4] ~
'/.config/containers/' }}"
no_log: true

- name: Handle state present
when: __podman_credential_state == "present"
when:
- __podman_credential_item["state"] | d("present") == "present"
- __podman_handle_state == "present"
block:
- name: Ensure the credentials directory is present
file:
Expand Down Expand Up @@ -73,7 +73,9 @@
no_log: true

- name: Handle state absent
when: __podman_credential_state == "absent"
when:
- __podman_credential_item["state"] | d("present") == "absent"
- __podman_handle_state == "absent"
block:
- name: Ensure credential file is absent
file:
Expand Down
3 changes: 3 additions & 0 deletions tasks/handle_kube_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
__podman_kube_spec_item['registry_password']
if 'registry_password' in __podman_kube_spec_item
else podman_registry_password }}"
__podman_validate_certs: "{{ __podman_kube_spec_item['validate_certs']
if 'validate_certs' in __podman_kube_spec_item
else podman_validate_certs }}"
no_log: true

- name: Get service name using systemd-escape
Expand Down
3 changes: 3 additions & 0 deletions tasks/handle_quadlet_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@
__podman_quadlet_spec_item['registry_password']
if 'registry_password' in __podman_quadlet_spec_item
else podman_registry_password }}"
__podman_validate_certs: "{{ __podman_quadlet_spec_item['validate_certs']
if 'validate_certs' in __podman_quadlet_spec_item
else podman_validate_certs }}"
no_log: true

- name: Cleanup quadlets
Expand Down
39 changes: 34 additions & 5 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,31 @@
set_fact:
__podman_cancel_user_linger: []

- name: Handle secrets
include_tasks: handle_secret.yml
loop: "{{ podman_secrets }}"
- name: Handle certs.d files - present
include_tasks: handle_certs_d.yml
vars:
__podman_handle_state: present
loop: "{{ podman_registry_certificates }}"
loop_control:
loop_var: __podman_secret_item
loop_var: __podman_cert_spec_item
no_log: true

- name: Handle credential files
- name: Handle credential files - present
include_tasks: handle_credential_files.yml
vars:
__podman_handle_state: present
loop: "{{ podman_credential_files }}"
loop_control:
loop_var: __podman_credential_item
no_log: true

- name: Handle secrets
include_tasks: handle_secret.yml
loop: "{{ podman_secrets }}"
loop_control:
loop_var: __podman_secret_item
no_log: true

- name: Handle Kubernetes specifications
include_tasks: handle_kube_spec.yml
loop: "{{ podman_kube_specs }}"
Expand All @@ -144,3 +155,21 @@
loop: "{{ __podman_cancel_user_linger }}"
loop_control:
loop_var: __podman_linger_user

- name: Handle credential files - absent
include_tasks: handle_credential_files.yml
vars:
__podman_handle_state: absent
loop: "{{ podman_credential_files }}"
loop_control:
loop_var: __podman_credential_item
no_log: true

- name: Handle certs.d files - absent
include_tasks: handle_certs_d.yml
vars:
__podman_handle_state: absent
loop: "{{ podman_registry_certificates }}"
loop_control:
loop_var: __podman_cert_spec_item
no_log: true
19 changes: 8 additions & 11 deletions tests/tasks/setup_registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
- name: Set authdir
set_fact:
__podman_registry_authdir: "{{ __podman_registry_tempfile.path ~ '/auth' }}"
__podman_test_authfile: "{{ __podman_registry_tempfile.path ~ '/auth/auth.json' }}"
__podman_test_authfile: "{{
__podman_registry_tempfile.path ~ '/auth/auth.json' }}"

- name: Create authdir
file:
Expand Down Expand Up @@ -43,17 +44,12 @@
dest: "{{ __podman_registry_authdir ~ '/registry_key.pem' }}"
mode: "0600"

- name: Create cert dir for registry
file:
path: /etc/containers/certs.d/localhost:5000
state: directory
mode: "0755"

- name: Write CA cert for registry
copy:
content: "{{ certificate_test_certs['podman_registry']['ca_content'] }}"
dest: /etc/containers/certs.d/localhost:5000/ca.crt
mode: "0644"
content: "{{
certificate_test_certs['podman_registry']['ca_content'] }}"
dest: "{{ __podman_registry_authdir ~ '/ca.crt' }}"
mode: "0600"

- name: Ensure test packages
package:
Expand Down Expand Up @@ -88,7 +84,6 @@
- name: Set paths for cleanup
set_fact:
__podman_cleanup_paths:
- /etc/containers/certs.d/localhost:5000
- "{{ __podman_registry_tempfile.path }}"

# # In case $PODMAN_TEST_KEEP_LOGIN_REGISTRY is set, for testing later
Expand Down Expand Up @@ -124,6 +119,7 @@
shell: >-
podman pull {{ item.key }};
podman push --authfile="{{ __podman_test_authfile }}"
--cert-dir="{{ __podman_registry_authdir }}"
{{ item.key }} docker://{{ item.value }}
loop: "{{
dict(__podman_test_images | zip(podman_local_test_images)) |
Expand All @@ -133,6 +129,7 @@
- name: Verify test images in local registry
command: >-
skopeo inspect --authfile="{{ __podman_test_authfile }}"
--cert-dir="{{ __podman_registry_authdir }}"
docker://{{ item }}
changed_when: false
loop: "{{ podman_local_test_images }}"
Loading

0 comments on commit 1abe0f7

Please sign in to comment.