diff --git a/changelogs/fragments/432-tls.yml b/changelogs/fragments/432-tls.yml new file mode 100644 index 000000000..39dd42c92 --- /dev/null +++ b/changelogs/fragments/432-tls.yml @@ -0,0 +1,2 @@ +bugfixes: + - "modules and plugins communicating directly with the Docker daemon - prevent crash when TLS is used (https://github.com/ansible-collections/community.docker/pull/432)." diff --git a/plugins/module_utils/common_api.py b/plugins/module_utils/common_api.py index 1ab898361..7b98bc4a6 100644 --- a/plugins/module_utils/common_api.py +++ b/plugins/module_utils/common_api.py @@ -71,7 +71,7 @@ def is_using_tls(auth_data): def get_connect_params(auth_data, fail_function): if is_using_tls(auth_data): - auth['docker_host'] = auth_data['docker_host'].replace('tcp://', 'https://') + auth_data['docker_host'] = auth_data['docker_host'].replace('tcp://', 'https://') result = dict( base_url=auth_data['docker_host'], diff --git a/tests/integration/targets/generic_connection_tests/aliases b/tests/integration/targets/generic_connection_tests/aliases new file mode 100644 index 000000000..9683a6a5a --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +shippable/posix/group4 +destructive diff --git a/tests/integration/targets/generic_connection_tests/files/nginx.conf b/tests/integration/targets/generic_connection_tests/files/nginx.conf new file mode 100644 index 000000000..50f92c29f --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/files/nginx.conf @@ -0,0 +1,50 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +user root; + +events { + worker_connections 16; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + error_log /dev/stdout info; + access_log /dev/stdout; + + server { + listen *:5000 ssl; + server_name daemon-tls.ansible.com; + server_name_in_redirect on; + + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256'; + ssl_ecdh_curve X25519:secp521r1:secp384r1; + ssl_prefer_server_ciphers on; + ssl_certificate /etc/nginx/cert.pem; + ssl_certificate_key /etc/nginx/cert.key; + + location / { + proxy_pass http://unix:/var/run/docker.sock:/; + + client_max_body_size 0; + chunked_transfer_encoding on; + } + } + + server { + listen *:6000; + server_name daemon.ansible.com; + server_name_in_redirect on; + + location / { + proxy_pass http://unix:/var/run/docker.sock:/; + + client_max_body_size 0; + chunked_transfer_encoding on; + } + } +} diff --git a/tests/integration/targets/generic_connection_tests/filter_plugins/filter_attr.py b/tests/integration/targets/generic_connection_tests/filter_plugins/filter_attr.py new file mode 100644 index 000000000..f821b7e72 --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/filter_plugins/filter_attr.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +def sanitize_host_info(data): + data = data.copy() + for key in ('SystemTime', 'NFd', 'NGoroutines', ): + data.pop(key, None) + return data + + +class FilterModule: + def filters(self): + return { + 'sanitize_host_info': sanitize_host_info, + } diff --git a/tests/integration/targets/generic_connection_tests/meta/main.yml b/tests/integration/targets/generic_connection_tests/meta/main.yml new file mode 100644 index 000000000..e7ff3d68b --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_docker + - setup_openssl + - setup_remote_tmp_dir diff --git a/tests/integration/targets/generic_connection_tests/tasks/main.yml b/tests/integration/targets/generic_connection_tests/tasks/main.yml new file mode 100644 index 000000000..22aa534b7 --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/tasks/main.yml @@ -0,0 +1,185 @@ +--- +# Copyright (c) 2022 Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Create random nginx frontend name + set_fact: + daemon_nginx_frontend: '{{ "ansible-docker-test-daemon-frontend-%0x" % ((2**32) | random) }}' + +- block: + - name: Create volume for config files + docker_volume: + name: '{{ daemon_nginx_frontend }}' + state: present + + - name: Create container for nginx frontend for daemon + docker_container: + state: stopped + name: '{{ daemon_nginx_frontend }}' + image: "{{ docker_test_image_registry_nginx }}" + volumes: + - '{{ daemon_nginx_frontend }}:/etc/nginx/' + - '/var/run/docker.sock:/var/run/docker.sock' + network_mode: '{{ current_container_network_ip | default(omit, true) }}' + networks: >- + {{ + [dict([['name', current_container_network_ip]])] + if current_container_network_ip not in ['', 'bridge'] else omit + }} + register: nginx_container + + - name: Copy config files + copy: + src: "{{ item }}" + dest: "{{ remote_tmp_dir }}/{{ item }}" + mode: "0644" + loop: + - nginx.conf + + - name: Copy static files into volume + command: docker cp {{ remote_tmp_dir }}/{{ item }} {{ daemon_nginx_frontend }}:/etc/nginx/{{ item }} + loop: + - nginx.conf + register: can_copy_files + ignore_errors: yes + + - when: can_copy_files is not failed + block: + + - name: Create private keys + community.crypto.openssl_privatekey: + path: '{{ remote_tmp_dir }}/{{ item }}.key' + type: ECC + curve: secp256r1 + force: yes + loop: + - cert + - ca + + - name: Create CSR for CA certificate + community.crypto.openssl_csr: + path: '{{ remote_tmp_dir }}/ca.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: yes + + - name: Create CA certificate + community.crypto.x509_certificate: + path: '{{ remote_tmp_dir }}/ca.pem' + csr_path: '{{ remote_tmp_dir }}/ca.csr' + privatekey_path: '{{ remote_tmp_dir }}/ca.key' + provider: selfsigned + + - name: Create CSR for frontend certificate + community.crypto.openssl_csr: + path: '{{ remote_tmp_dir }}/cert.csr' + privatekey_path: '{{ remote_tmp_dir }}/cert.key' + subject_alt_name: + - DNS:daemon-tls.ansible.com + + - name: Create frontend certificate + community.crypto.x509_certificate: + path: '{{ remote_tmp_dir }}/cert.pem' + csr_path: '{{ remote_tmp_dir }}/cert.csr' + privatekey_path: '{{ remote_tmp_dir }}/cert.key' + ownca_path: '{{ remote_tmp_dir }}/ca.pem' + ownca_privatekey_path: '{{ remote_tmp_dir }}/ca.key' + provider: ownca + + - name: Copy dynamic files into volume + command: docker cp {{ remote_tmp_dir }}/{{ item }} {{ daemon_nginx_frontend }}:/etc/nginx/{{ item }} + loop: + - ca.pem + - cert.pem + - cert.key + + - name: Start nginx frontend for daemon + docker_container: + name: '{{ daemon_nginx_frontend }}' + state: started + register: nginx_container + + - name: Output nginx container network settings + debug: + var: nginx_container.container.NetworkSettings + + - name: Get proxied daemon URLs + set_fact: + docker_daemon_frontend_https: "https://{{ nginx_container.container.NetworkSettings.Networks[current_container_network_ip].IPAddress if current_container_network_ip else nginx_container.container.NetworkSettings.IPAddress }}:5000" + docker_daemon_frontend_http: "http://{{ nginx_container.container.NetworkSettings.Networks[current_container_network_ip].IPAddress if current_container_network_ip else nginx_container.container.NetworkSettings.IPAddress }}:6000" + + - name: Wait for registry frontend + uri: + url: '{{ docker_daemon_frontend_http }}/version' + register: result + until: result is success + retries: 5 + delay: 1 + + - name: Get docker daemon information directly + docker_host_info: + register: output_direct + + - name: Show direct host info + debug: + var: output_direct.host_info | sanitize_host_info + + - name: Get docker daemon information via HTTP + docker_host_info: + docker_host: '{{ docker_daemon_frontend_http }}' + register: output_http + + - name: Show HTTP host info + debug: + var: output_http.host_info | sanitize_host_info + + - name: Check that information matches + assert: + that: + - (output_direct.host_info | sanitize_host_info) == (output_http.host_info | sanitize_host_info) + + - name: Get docker daemon information via HTTPS + docker_host_info: + docker_host: '{{ docker_daemon_frontend_https }}' + tls_hostname: daemon-tls.ansible.com + ca_cert: '{{ remote_tmp_dir }}/ca.pem' + tls: true + validate_certs: true + register: output_https + + - name: Show HTTPS host info + debug: + var: output_https.host_info | sanitize_host_info + + - name: Check that information matches + assert: + that: + - (output_direct.host_info | sanitize_host_info) == (output_https.host_info | sanitize_host_info) + + always: + - command: docker logs {{ daemon_nginx_frontend }} + register: output + ignore_errors: true + - debug: + var: output.stdout_lines + ignore_errors: true + + - name: Remove container + docker_container: + state: absent + name: '{{ daemon_nginx_frontend }}' + force_kill: true + ignore_errors: true + + - name: Remove volume + docker_volume: + name: '{{ daemon_nginx_frontend }}' + state: absent + ignore_errors: true diff --git a/tests/integration/targets/generic_connection_tests/vars/main.yml b/tests/integration/targets/generic_connection_tests/vars/main.yml new file mode 120000 index 000000000..bacffb837 --- /dev/null +++ b/tests/integration/targets/generic_connection_tests/vars/main.yml @@ -0,0 +1 @@ +../../setup_docker/vars/main.yml \ No newline at end of file