diff --git a/README.md b/README.md index 383577d4..c2a96236 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/defaults/main.yml b/defaults/main.yml index ded93c21..64d7e7c2 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -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 diff --git a/tasks/create_update_kube_spec.yml b/tasks/create_update_kube_spec.yml index ac6d4c14..173192d4 100644 --- a/tasks/create_update_kube_spec.yml +++ b/tasks/create_update_kube_spec.yml @@ -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 diff --git a/tasks/create_update_quadlet_spec.yml b/tasks/create_update_quadlet_spec.yml index 517824e3..468c18a6 100644 --- a/tasks/create_update_quadlet_spec.yml +++ b/tasks/create_update_quadlet_spec.yml @@ -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 diff --git a/tasks/handle_certs_d.yml b/tasks/handle_certs_d.yml new file mode 100644 index 00000000..ce5f87b5 --- /dev/null +++ b/tasks/handle_certs_d.yml @@ -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 }}" + 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 }}" + state: absent + when: __certs_d_dir_files.matched == 0 + no_log: true diff --git a/tasks/handle_credential_files.yml b/tasks/handle_credential_files.yml index 6b4e1a0c..44cbae87 100644 --- a/tasks/handle_credential_files.yml +++ b/tasks/handle_credential_files.yml @@ -31,8 +31,6 @@ 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] ~ @@ -40,7 +38,9 @@ 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: @@ -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: diff --git a/tasks/handle_kube_spec.yml b/tasks/handle_kube_spec.yml index e5f208c1..0490a630 100644 --- a/tasks/handle_kube_spec.yml +++ b/tasks/handle_kube_spec.yml @@ -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 diff --git a/tasks/handle_quadlet_spec.yml b/tasks/handle_quadlet_spec.yml index 6c75362b..23f44223 100644 --- a/tasks/handle_quadlet_spec.yml +++ b/tasks/handle_quadlet_spec.yml @@ -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 diff --git a/tasks/main.yml b/tasks/main.yml index 4ab96b69..98691559 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -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 }}" @@ -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 diff --git a/tests/tasks/setup_registry.yml b/tests/tasks/setup_registry.yml index 9b0a3b08..b5c2127a 100644 --- a/tests/tasks/setup_registry.yml +++ b/tests/tasks/setup_registry.yml @@ -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: @@ -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: @@ -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 @@ -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)) | @@ -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 }}" diff --git a/tests/tests_auth_and_security.yml b/tests/tests_auth_and_security.yml index 42c563ac..be08d430 100644 --- a/tests/tests_auth_and_security.yml +++ b/tests/tests_auth_and_security.yml @@ -8,6 +8,7 @@ __podman_test_username: podman_username local_test_image: "{{ podman_local_test_images[0] }}" podman_quadlet_spec_base: + name: auth_test_1_quadlet type: container state: created activate_systemd_unit: false @@ -48,12 +49,13 @@ __podman_test_images: - "{{ test_image }}" - - name: Run the role with no credentials, should fail + - name: Run the role with no credentials and no cert checking block: - - name: Run the role with no credentials + - name: Run the role with no credentials and no cert checking include_role: name: linux-system-roles.podman vars: + podman_validate_certs: false podman_quadlet_specs: - "{{ podman_quadlet_spec_base }}" podman_kube_specs: @@ -73,6 +75,32 @@ vars: expected_msg: Failed to pull image + - name: Run the role with credentials and cert checking + block: + - name: Run the role with credentials and cert checking + include_role: + name: linux-system-roles.podman + vars: + podman_validate_certs: true + podman_quadlet_specs: + - "{{ podman_quadlet_spec_creds }}" + podman_kube_specs: + - "{{ podman_kube_spec_creds }}" + + - name: Should not get here + fail: + msg: UNREACHABLE + + rescue: + - name: Check error + fail: + msg: >- + Role did not fail with the expected message {{ expected_msg }} but + instead failed with {{ ansible_failed_result | to_nice_json }} + when: not expected_msg in ansible_failed_result.results[0].msg | d("") + vars: + expected_msg: Failed to pull image + - name: Run remaining tasks in block with cleanup block: - name: Create a local tmpdir @@ -83,7 +111,7 @@ register: __local_tmpdir delegate_to: localhost - - name: Run the role with credentials in spec + - name: Run the role with credentials in spec and CA cert include_role: name: linux-system-roles.podman vars: @@ -93,6 +121,10 @@ - "{{ podman_quadlet_spec_creds }}" podman_kube_specs: - "{{ podman_kube_spec_creds }}" + podman_registry_certificates: + - registry_host: localhost:5000 + ca_cert_content: "{{ + certificate_test_certs['podman_registry']['ca_content'] }}" - name: Run the role with credentials in global vars include_role: @@ -123,6 +155,20 @@ podman_kube_specs: - "{{ podman_kube_spec_base }}" + - name: Remove all container resources - root + include_role: + name: linux-system-roles.podman + vars: + podman_kube_specs: + - "{{ podman_kube_spec_base | combine({'state': 'absent'}) }}" + podman_quadlet_specs: + - "{{ podman_quadlet_spec_base | combine({'state': 'absent'}) }}" + podman_credential_files: + - state: absent + podman_registry_certificates: + - registry_host: localhost:5000 + state: absent + - name: Create a user for rootless user: name: auth_test_user1 @@ -141,8 +187,14 @@ - "{{ podman_quadlet_spec_base | combine(__run_as_user) }}" podman_kube_specs: - "{{ podman_kube_spec_base | combine(__run_as_user) }}" + podman_registry_certificates: + - registry_host: localhost:5000 + run_as_user: auth_test_user1 + ca_cert_content: "{{ + certificate_test_certs['podman_registry']['ca_content'] }}" always: + # in case the test errored before this happened - name: Remove all container resources - root include_role: name: linux-system-roles.podman @@ -153,8 +205,11 @@ - "{{ podman_quadlet_spec_base | combine({'state': 'absent'}) }}" podman_credential_files: - state: absent + podman_registry_certificates: + - registry_host: localhost:5000 + state: absent - - name: Remove pods and units - rootless + - name: Remove all container resources - rootless include_role: name: linux-system-roles.podman vars: @@ -165,6 +220,10 @@ - "{{ podman_quadlet_spec_base | combine({'state': 'absent'}) }}" podman_credential_files: - state: absent + podman_registry_certificates: + - registry_host: localhost:5000 + run_as_user: auth_test_user1 + state: absent - name: Remove user user: diff --git a/vars/main.yml b/vars/main.yml index 38402ff3..bdc1135b 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -76,3 +76,9 @@ __podman_system_quadlet_path: "/etc/containers/systemd" # location for user quadlet files __podman_user_quadlet_path: "/.config/containers/systemd" + +# location for user certs_d +__podman_user_certs_d_path: "/.config/containers/certs.d" + +# location for system certs_d +__podman_system_certs_d_path: "/etc/containers/certs.d"