diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9c3f237..5423ce1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,4 +13,5 @@ jobs: steps: - uses: UCL-MIRSG/.github/actions/linting@v0.26.0 with: + ansible-roles-config: ./meta/requirements.yml pre-commit-config: ./.pre-commit-config.yaml diff --git a/README.md b/README.md index ee0141e..b7f6fb8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,32 @@ This role is for installing [docker-ce](https://docs.docker.com/engine/install/) | `docker_repo_baseurl` | URL to the directory containing the repodata. Defaults to `https://download.docker.com/linux/centos` | | `docker_yum_package` | The name of the Docker package. Defaults to `docker` | +If you would like to [configure](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket) +your Docker server such that clients can connect to it via TLS, you can also use this role to generate the necessary certificates. +The following variables can be used to configure certificate creation and signing: + +| Name | Description | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `docker_generate_certificates` | If `true`, CA, server, and client certificates will be generated. Defaults to `false` | +| `docker_certificate_directory` | Directory in which to store the certificates. Defaults to `/home/docker/.docker` | +| `docker_config_dir` | Docker configuration directory. Defaults to `/etc/docker` | +| `docker_daemon_conf_file` | Docker daemon configuration filename. Defaults to `/etc/docker/daemon.json` | +| `docker_server_hostname` | Hostname of your Docker server. Used for the `commonName` field of the certificate signing request subject. Defaults to `"{{ ansible_host }}"` | +| `docker_server_ip` | IP address of your Docker server. Defaults to `0.0.0.0` | +| `docker_ca_key` | Filename for the CA certificate key. Defaults to `/home/docker/.docker/ca.key` | +| `docker_ca_csr` | Filename for the CA certificate signing request. Defaults to `/home/docker/.docker/ca.csr` | +| `docker_ca_cert` | Filename for the CA certificate. Defaults to `/home/docker/.docker/ca.pem` | +| `docker_server_key` | Filename for the server certificate key. Defaults to `/home/docker/.docker/server-key.pem` | +| `docker_server_csr` | Filename for the server certificate signing request. Defaults to `/home/docker/.docker/server.csr` | +| `docker_server_cert` | Filename for the server certificate. Defaults to `/home/docker/.docker/server-cert.pem` | +| `docker_client_hostnames` | List of hostnames of clients that will connect to the server. Defaults to `[]` | +| `docker_client_certificate_directory` | Directory in which to store the client certificates. Defaults to `/home/docker/.docker/client_certs` | +| `docker_client_certificate_cache_directory` | Directory in which to client certificates will be copied to. Defaults to `~/ansible_persistent_files/docker_certificates` | + +If you have specified a list of clients in `docker_client_hostnames`, the certificate for each client will be stored locally on your Ansible +controller in the folder `docker_client_certificate_cache_directory`. You will then need to copy these certificates to the corresponding +client. + ## Installation Include in a requirements.yml file as follows: diff --git a/defaults/main.yml b/defaults/main.yml index 951fc35..8b40ccd 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -2,10 +2,38 @@ # defaults for mirsg.docker docker_owner: "root" docker_group: "root" -docker_config_dir: "/etc/docker" -docker_daemon_conf_file: "/etc/docker/daemon.json" + +# mirsg.docker service docker_service_directory: "/etc/systemd/system/docker.service.d" docker_service_name: "docker" + +# mirsg.docker install docker_rpm_gpg_key_url: "https://download.docker.com/linux/centos/gpg" docker_repo_baseurl: "https://download.docker.com/linux/centos/$releasever/$basearch/stable" docker_yum_package: "docker" + +# mirsg.docker certificates +docker_generate_certificates: false +docker_certificate_directory: "/home/docker/.docker" + +# mirsg.docker configuration +docker_config_dir: "/etc/docker" +docker_daemon_conf_file: "/etc/docker/daemon.json" +docker_server_hostname: "{{ ansible_host }}" +docker_server_ip: "0.0.0.0" +docker_server_port: "2376" + +# mirsg.docker CA certificate +docker_ca_key: "{{ docker_certificate_directory }}/ca.key" +docker_ca_csr: "{{ docker_certificate_directory }}/ca.csr" +docker_ca_cert: "{{ docker_certificate_directory }}/ca.pem" + +# mirsg.docker server certificate +docker_server_key: "{{ docker_certificate_directory }}/server-key.pem" +docker_server_csr: "{{ docker_certificate_directory }}/server.csr" +docker_server_cert: "{{ docker_certificate_directory }}/server-cert.pem" + +# mirsg.docker client certificates +docker_client_hostnames: [] # list of hostnames of clients that will connect to the server +docker_client_certificate_directory: "{{ docker_certificate_directory }}/client_certs" +docker_client_certificate_cache_directory: "{{ lookup('env', 'HOME') }}/ansible_persistent_files/docker_certificates" diff --git a/meta/requirements.yml b/meta/requirements.yml new file mode 100644 index 0000000..e2b522d --- /dev/null +++ b/meta/requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - community.crypto diff --git a/molecule/centos7/molecule.yml b/molecule/centos7/molecule.yml index 47fe1fc..351946b 100644 --- a/molecule/centos7/molecule.yml +++ b/molecule/centos7/molecule.yml @@ -1,12 +1,15 @@ --- dependency: name: galaxy + options: + role-file: meta/requirements.yml + force: true driver: name: docker platforms: - - name: instance + - name: server image: centos:7 dockerfile: ../resources/Dockerfile.j2 command: "" @@ -19,7 +22,11 @@ provisioner: config_options: defaults: callbacks_enabled: profile_tasks, timer, yaml + inventory: + links: + host_vars: ../resources/inventory/host_vars/ playbooks: + prepare: ./prepare.yml converge: ../resources/converge.yml env: ANSIBLE_VERBOSITY: "1" diff --git a/molecule/centos7/prepare.yml b/molecule/centos7/prepare.yml new file mode 100644 index 0000000..6b63c36 --- /dev/null +++ b/molecule/centos7/prepare.yml @@ -0,0 +1,30 @@ +--- +- name: Prepare + hosts: all + become: false + gather_facts: true + tasks: + - name: Install EPEL-release + ansible.builtin.yum: + name: "epel-release" + state: installed + + - name: Install Python + ansible.builtin.package: + name: "{{ item }}" + update_cache: true + state: present + loop: + - python + - python-pip + - python-setuptools + + - name: Update pip + ansible.builtin.pip: + name: pip + version: "20.3.4" + + - name: Install cryptography with pip - needed to generate certificates + ansible.builtin.pip: + name: + - cryptography diff --git a/molecule/resources/inventory/host_vars/server.yml b/molecule/resources/inventory/host_vars/server.yml new file mode 100644 index 0000000..1346869 --- /dev/null +++ b/molecule/resources/inventory/host_vars/server.yml @@ -0,0 +1,3 @@ +--- +docker_generate_certificates: true +docker_client_hostnames: ["docker-client.com"] diff --git a/molecule/rocky8/molecule.yml b/molecule/rocky8/molecule.yml index d487086..017320b 100644 --- a/molecule/rocky8/molecule.yml +++ b/molecule/rocky8/molecule.yml @@ -1,12 +1,15 @@ --- dependency: name: galaxy + options: + role-file: meta/requirements.yml + force: true driver: name: docker platforms: - - name: instance + - name: server image: rockylinux:8 dockerfile: ../resources/Dockerfile.j2 command: "" @@ -21,7 +24,11 @@ provisioner: config_options: defaults: callbacks_enabled: profile_tasks, timer, yaml + inventory: + links: + host_vars: ../resources/inventory/host_vars/ playbooks: + prepare: ./prepare.yml converge: ../resources/converge.yml env: ANSIBLE_VERBOSITY: "1" diff --git a/molecule/rocky8/prepare.yml b/molecule/rocky8/prepare.yml new file mode 100644 index 0000000..63e62ba --- /dev/null +++ b/molecule/rocky8/prepare.yml @@ -0,0 +1,30 @@ +--- +- name: Prepare + hosts: all + become: false + gather_facts: true + tasks: + - name: Install EPEL-release + ansible.builtin.yum: + name: "epel-release" + state: installed + + - name: Install Python + ansible.builtin.package: + name: "{{ item }}" + update_cache: true + state: present + loop: + - python3 + - python3-pip + - python3-setuptools + + - name: Update pip + ansible.builtin.pip: + name: pip + version: "21.3.1" + + - name: Install cryptography with pip - needed to generate certificates + ansible.builtin.pip: + name: + - cryptography diff --git a/tasks/ca-cert.yml b/tasks/ca-cert.yml new file mode 100644 index 0000000..fb13a70 --- /dev/null +++ b/tasks/ca-cert.yml @@ -0,0 +1,36 @@ +--- +- name: Ensure docker cert dir exists + ansible.builtin.file: + path: "{{ docker_certificate_directory }}" + state: directory + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0700" + +- name: Generate CA private key + community.crypto.openssl_privatekey: + path: "{{ docker_ca_key }}" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0400" + +- name: Generate CA CSR + community.crypto.openssl_csr: + path: "{{ docker_ca_csr }}" + privatekey_path: "{{ docker_ca_key }}" + common_name: "{{ docker_server_hostname }}" + subject_alt_name: "IP:{{ docker_server_ip }}" + basic_constraints_critical: true + basic_constraints: ["CA:TRUE"] + +- name: Generate self-signed CA certificate + community.crypto.x509_certificate: + path: "{{ docker_ca_cert }}" + privatekey_path: "{{ docker_ca_key }}" + csr_path: "{{ docker_ca_csr }}" + provider: selfsigned + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0400" + notify: + - Restart docker diff --git a/tasks/client-certs.yml b/tasks/client-certs.yml new file mode 100644 index 0000000..ab923b4 --- /dev/null +++ b/tasks/client-certs.yml @@ -0,0 +1,48 @@ +--- +- name: Ensure docker client cert dir exists on server + ansible.builtin.file: + path: "{{ docker_client_certificate_directory }}" + state: directory + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0700" + +- name: Generate OpenSSL client private key + community.crypto.openssl_privatekey: + path: "{{ docker_client_certificate_directory }}/key.pem" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0400" + +- name: Generate OpenSSL CSR for each client using private key + community.crypto.openssl_csr: + path: "{{ docker_client_certificate_directory }}/{{ item }}.csr" + privatekey_path: "{{ docker_client_certificate_directory }}/key.pem" + common_name: "{{ item }}" + register: new_docker_client_csr_generated + loop: "{{ docker_client_hostnames }}" + +- name: Generate client certificates signed by server CA + community.crypto.x509_certificate: + path: "{{ docker_client_certificate_directory }}/{{ item }}.cert" + csr_path: "{{ docker_client_certificate_directory }}/{{ item }}.csr" + provider: ownca + ownca_path: "{{ docker_ca_cert }}" + ownca_privatekey_path: "{{ docker_ca_key }}" + mode: "0400" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + loop: "{{ docker_client_hostnames }}" + +- name: Copy signed client certificates to temp dir on Ansible controller + ansible.builtin.fetch: + src: "{{ docker_client_certificate_directory }}/{{ item }}.cert" + dest: "{{ docker_client_certificate_cache_directory }}/{{ item }}.cert" + flat: true + loop: "{{ docker_client_hostnames }}" + +- name: Copy private key to temp dir on Ansible controller + ansible.builtin.fetch: + src: "{{ docker_client_certificate_directory }}/key.pem" + dest: "{{ docker_client_certificate_cache_directory }}/key.pem" + flat: true diff --git a/tasks/main.yml b/tasks/main.yml index d98509c..73794d9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -57,6 +57,37 @@ mode: "0644" notify: Reload docker +- name: Generate CA, server, and client certificates + when: docker_generate_certificates + notify: + - Restart docker + block: + - name: Ensure docker config directory exists - {{ docker_config_dir }} + ansible.builtin.file: + path: "{{ docker_config_dir }}" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + state: directory + mode: "0700" + + - name: Write docker daemon configuration file + ansible.builtin.template: + src: daemon.json.j2 + dest: "{{ docker_daemon_conf_file }}" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0640" + + - name: Generate CA certificate + ansible.builtin.import_tasks: ca-cert.yml + + - name: Generate server TLS certificate + ansible.builtin.import_tasks: server-cert.yml + + - name: Generate TLS certificates for each client + ansible.builtin.import_tasks: client-certs.yml + when: docker_client_hostnames + - name: "Ensure docker service configuration is reloaded before restarting the service" ansible.builtin.meta: flush_handlers diff --git a/tasks/server-cert.yml b/tasks/server-cert.yml new file mode 100644 index 0000000..ceb1117 --- /dev/null +++ b/tasks/server-cert.yml @@ -0,0 +1,27 @@ +--- +- name: Generate server private key + community.crypto.openssl_privatekey: + path: "{{ docker_server_key }}" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0400" + +- name: Generate server CSR + community.crypto.openssl_csr: + path: "{{ docker_server_csr }}" + privatekey_path: "{{ docker_server_key }}" + common_name: "{{ docker_server_hostname }}" + subject_alt_name: "IP:{{ docker_server_ip }}" + +- name: Generate server certificate + community.crypto.x509_certificate: + path: "{{ docker_server_cert }}" + csr_path: "{{ docker_server_csr }}" + provider: ownca + ownca_path: "{{ docker_ca_cert }}" + ownca_privatekey_path: "{{ docker_ca_key }}" + owner: "{{ docker_owner }}" + group: "{{ docker_group }}" + mode: "0400" + notify: + - Restart docker diff --git a/templates/daemon.json.j2 b/templates/daemon.json.j2 new file mode 100644 index 0000000..98b549d --- /dev/null +++ b/templates/daemon.json.j2 @@ -0,0 +1,7 @@ +{ + "hosts": ["tcp://{{ docker_server_ip }}:{{ docker_server_port }}", "unix:///var/run/docker.sock"], + "tlsverify": true, + "tlscacert": "{{ docker_ca_cert }}", + "tlscert": "{{ docker_server_cert }}", + "tlskey": "{{ docker_server_key }}" + }