From b2856e22f5a331df3d760b09cb78ca162dbf3472 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Wed, 13 May 2020 15:47:21 +0100
Subject: [PATCH] Refactor the credentials role

Separate a base "credentials" role and a "float-credentials" one that
instantiates the float-derived service credentials. This allows users
to install credentials from Ansible roles that do not have a
corresponding float service.
---
 playbooks/base.yml                            |   2 +-
 plugins/inventory/float.py                    |  10 +-
 roles/credentials/README.md                   |  19 ++-
 roles/credentials/tasks/install_certs.yml     |  76 ----------
 .../credentials/tasks/install_credentials.yml |  23 ----
 roles/credentials/tasks/main.yml              | 130 ++++++++++++------
 roles/float-credentials/README.md             |   2 +
 roles/float-credentials/meta/main.yml         |   7 +
 roles/float-credentials/tasks/main.yml        |  26 ++++
 9 files changed, 147 insertions(+), 148 deletions(-)
 delete mode 100644 roles/credentials/tasks/install_certs.yml
 delete mode 100644 roles/credentials/tasks/install_credentials.yml
 create mode 100644 roles/float-credentials/README.md
 create mode 100644 roles/float-credentials/meta/main.yml
 create mode 100644 roles/float-credentials/tasks/main.yml

diff --git a/playbooks/base.yml b/playbooks/base.yml
index 49e8eb2f..c69d94af 100644
--- a/playbooks/base.yml
+++ b/playbooks/base.yml
@@ -3,7 +3,7 @@
 - hosts: all
   roles:
     - base
-    - credentials
+    - float-credentials
     - vagrant-compat
 
 - hosts: net-overlay
diff --git a/plugins/inventory/float.py b/plugins/inventory/float.py
index 9a316732..995c7cf3 100644
--- a/plugins/inventory/float.py
+++ b/plugins/inventory/float.py
@@ -358,7 +358,6 @@ def _host_vars(name, inventory, services, assignments):
         'float_disabled_services': [],
         'float_enabled_containers': [],
         'float_host_service_credentials': [],
-        'float_host_service_credentials_certs': [],
         'float_host_overlay_networks': _host_net_overlays(name, inventory),
         'float_host_dns_map': _host_service_dns_map(
             name, inventory, services, assignments),
@@ -366,8 +365,7 @@ def _host_vars(name, inventory, services, assignments):
 
     # Add default client credentials that are present on all hosts.
     for c in DEFAULT_SERVICE_CREDENTIALS:
-        hv['float_host_service_credentials'].append({'credentials': c})
-        hv['float_host_service_credentials_certs'].append({
+        hv['float_host_service_credentials'].append({
             'credentials': c, 'service': 'LOCAL',
             'mode': 'client', 'x509_params': {}})
 
@@ -390,15 +388,13 @@ def _host_vars(name, inventory, services, assignments):
             for u in services[s].get('systemd_services', []):
                 enabled_systemd_units.add(u)
             for c in services[s].get('service_credentials', []):
-                hv['float_host_service_credentials'].append({
-                    'service': s, 'credentials': c})
                 if c.get('enable_server', True):
                     params = _service_credential_params(name, s, inventory, assignments)
-                    hv['float_host_service_credentials_certs'].append({
+                    hv['float_host_service_credentials'].append({
                         'credentials': c, 'service': s,
                         'mode': 'server', 'x509_params': params})
                 if c.get('enable_client', True):
-                    hv['float_host_service_credentials_certs'].append({
+                    hv['float_host_service_credentials'].append({
                         'credentials': c, 'service': s,
                         'mode': 'client', 'x509_params': {}})
         else:
diff --git a/roles/credentials/README.md b/roles/credentials/README.md
index 038bc3f3..de7a8aa2 100644
--- a/roles/credentials/README.md
+++ b/roles/credentials/README.md
@@ -3,7 +3,24 @@ credentials
 
 Ansible role that installs all the [service
 credentials](../docs/service_mesh.md#mutual-service-authentication) on
-the hosts where they're needed.
+the hosts where they're needed. This role works in combination with
+the 'x509' action plugin.
 
 Private keys never leave the target host, we create a CSR and sign it
 on the Ansible host.
+
+X509 credentials are stored in /etc/credentials/x509 under directories
+named after the services. Every service directory contains a copy of
+the public CA certificate, so it can be bind-mounted in a container
+easily.
+
+Private keys have mode 440, are owned by root and by a dedicated group
+named *service*-credentials. When the service is actually installed,
+later, maybe by an Ansible role, it can add the service user to this
+group.
+
+Use by including this role and setting the *credentials* variable to a
+list of entries specifying the desired credentials. This is already
+done once system-wide by the *float-credentials* role with the
+credentials automagically derived from the service definitions by
+*float*.
diff --git a/roles/credentials/tasks/install_certs.yml b/roles/credentials/tasks/install_certs.yml
deleted file mode 100644
index ae705651..00000000
--- a/roles/credentials/tasks/install_certs.yml
+++ /dev/null
@@ -1,76 +0,0 @@
----
-
-#- set_fact:
-#    x509_params: "{{ float_service_credentials_params[service_name_item + '-' + credentials.name] | default({}) }}"
-#  when: "service_name_item is defined"
-
-- name: Set up internal PKI credentials
-  block:
-
-    - file:
-        path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}"
-        state: directory
-      loop: "{{ float_host_service_credentials_certs }}"
-
-    - name: "Check the internal PKI certificates"
-      x509_csr:
-        credentials_name: "{{ item.credentials.name }}"
-        domain: "{{ domain }}"
-        mode: "{{ item.mode }}"
-        params: "{{ item.x509_params|default({}) }}"
-        private_key_path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/private_key.pem"
-        cert_path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/cert.pem"
-        ca_cert_path: "/etc/credentials/x509/{{ item.credentials.name }}/ca.pem"
-        check: true
-      loop: "{{ float_host_service_credentials_certs }}"
-      check_mode: no
-      register: x509_should_update
-
-    # TODO: set the right permissions (credentials.name-credentials)
-    - name: "Create internal PKI CSRs"
-      x509_csr:
-        credentials_name: "{{ item.0.credentials.name }}"
-        domain: "{{ domain }}"
-        mode: "{{ item.0.mode }}"
-        params: "{{ item.0.x509_params|default({}) }}"
-        private_key_path: "/etc/credentials/x509/{{ item.0.credentials.name }}/{{ item.0.mode }}/private_key.pem"
-        check: false
-      when: "item.1.changed"
-      loop: "{{ float_host_service_credentials_certs | zip(x509_should_update.results) | list }}"
-      register: x509_csr
-
-    - name: "Sign internal PKI certificates"
-      x509_sign:
-        csr: "{{ item.1.csr }}"
-        mode: "{{ item.0.mode }}"
-        ca_cert_path: "{{ credentials_dir }}/x509/ca.pem"
-        ca_key_path: "{{ credentials_dir }}/x509/ca_private_key.pem"
-      when: "item.1.changed"
-      loop: "{{ float_host_service_credentials_certs | zip(x509_csr.results) | list }}"
-      register: x509_sign
-
-    - name: "Install the signed internal PKI certificates"
-      copy:
-        dest: "/etc/credentials/x509/{{ item.0.credentials.name }}/{{ item.0.mode }}/cert.pem"
-        content: "{{ item.1.cert }}"
-        mode: 0644
-      when: "item.1.changed"
-      loop: "{{ float_host_service_credentials_certs | zip(x509_sign.results) | list }}"
-
-    - name: "Set permissions on the private keys"
-      file:
-        path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/private_key.pem"
-        group: "{{ item.credentials.name }}-credentials"
-        mode: 0640
-      loop: "{{ float_host_service_credentials_certs }}"
-
-    # This should use the systemd module but it doesn't take lists of services.
-    - name: "Restart associated services"
-      shell: "systemctl restart {{ services[item.0.service].systemd_services | join(' ') }}"
-      when: "item.1.changed"
-      loop: "{{ float_host_service_credentials_certs | zip(x509_sign.results) | list }}"
-
-  rescue:
-    - debug:
-        msg: "Failed to set up one or more credentials"
-
diff --git a/roles/credentials/tasks/install_credentials.yml b/roles/credentials/tasks/install_credentials.yml
deleted file mode 100644
index ec3cce43..00000000
--- a/roles/credentials/tasks/install_credentials.yml
+++ /dev/null
@@ -1,23 +0,0 @@
----
-
-- name: "Create service credentials group"
-  group:
-    name: "{{ item.credentials.name }}-credentials"
-    system: true
-  loop: "{{ float_host_service_credentials }}"
-
-- name: "Create service credentials dirs"
-  file:
-    path: "/etc/credentials/x509/{{ item.credentials.name }}"
-    state: directory
-  loop: "{{ float_host_service_credentials }}"
-
-- name: Copy CA
-  copy:
-    src: "{{ credentials_dir }}/x509/ca.pem"
-    dest: "/etc/credentials/x509/{{ item.credentials.name }}/ca.pem"
-    owner: root
-    group: root
-    mode: 0644
-  loop: "{{ float_host_service_credentials }}"
-
diff --git a/roles/credentials/tasks/main.yml b/roles/credentials/tasks/main.yml
index 45c12e31..3863396e 100644
--- a/roles/credentials/tasks/main.yml
+++ b/roles/credentials/tasks/main.yml
@@ -1,55 +1,105 @@
 ---
 
-# Distribute the SSO public key to all hosts.
+# This package needs to be on hosts in order to generate the CSRs.
+- name: Install x509ca package
+  apt:
+    name: x509ca
+    state: present
+
+# Get the credential names from the list of certs.
+- set_fact:
+    credentials_names: "{{ credentials | map(attribute='credentials') | map(attribute='name') | unique | list }}"
 
-- file:
-    path: /etc/sso
+- name: "Create service credentials group"
+  group:
+    name: "{{ item }}-credentials"
+    system: true
+  loop: "{{ credentials_names }}"
+
+- name: "Create service credentials dirs"
+  file:
+    path: "/etc/credentials/x509/{{ item }}"
     state: directory
-    owner: root
-    group: root
-    mode: 0755
+  loop: "{{ credentials_names }}"
 
-- name: Install SSO public key
+- name: Copy CA
   copy:
-    src: "{{ credentials_dir }}/sso/public.key"
-    dest: /etc/sso/public.key
+    src: "{{ credentials_dir }}/x509/ca.pem"
+    dest: "/etc/credentials/x509/{{ item }}/ca.pem"
+    owner: root
+    group: root
     mode: 0644
+  loop: "{{ credentials_names }}"
 
-# Distribute X509 credentials to all hosts as needed. This role works
-# in combination with the 'x509' action plugin.
+# Create and sign all certificates in a series of loops (with some
+# unfortunately complex change-detection logic).
+- name: Set up internal PKI credentials
+  block:
 
-# X509 credentials are stored in /etc/credentials/x509 under
-# directories named after the services. Every service directory
-# contains a copy of the public CA certificate, so it can be
-# bind-mounted in a container easily.
+    - file:
+        path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}"
+        state: directory
+      loop: "{{ credentials }}"
 
-# Private keys have mode 440, are owned by root and by a dedicated
-# group named <service>-credentials. When the service is actually
-# installed, later, maybe by an Ansible role, it can add the service
-# user to this group.
+    - name: "Check the internal PKI certificates"
+      x509_csr:
+        credentials_name: "{{ item.credentials.name }}"
+        domain: "{{ domain }}"
+        mode: "{{ item.mode }}"
+        params: "{{ item.x509_params|default({}) }}"
+        private_key_path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/private_key.pem"
+        cert_path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/cert.pem"
+        ca_cert_path: "/etc/credentials/x509/{{ item.credentials.name }}/ca.pem"
+        check: true
+      loop: "{{ credentials }}"
+      check_mode: no
+      register: x509_should_update
 
-- name: Install x509ca package
-  apt:
-    name: x509ca
-    state: present
+    # TODO: set the right permissions (credentials.name-credentials)
+    - name: "Create internal PKI CSRs"
+      x509_csr:
+        credentials_name: "{{ item.0.credentials.name }}"
+        domain: "{{ domain }}"
+        mode: "{{ item.0.mode }}"
+        params: "{{ item.0.x509_params|default({}) }}"
+        private_key_path: "/etc/credentials/x509/{{ item.0.credentials.name }}/{{ item.0.mode }}/private_key.pem"
+        check: false
+      when: "item.1.changed"
+      loop: "{{ credentials | zip(x509_should_update.results) | list }}"
+      register: x509_csr
 
-- import_tasks: install_credentials.yml
-- import_tasks: install_certs.yml
+    - name: "Sign internal PKI certificates"
+      x509_sign:
+        csr: "{{ item.1.csr }}"
+        mode: "{{ item.0.mode }}"
+        ca_cert_path: "{{ credentials_dir }}/x509/ca.pem"
+        ca_key_path: "{{ credentials_dir }}/x509/ca_private_key.pem"
+      when: "item.1.changed"
+      loop: "{{ credentials | zip(x509_csr.results) | list }}"
+      register: x509_sign
 
-# Remove credentials that shouldn't be here.
-# - file: path="/etc/credentials/x509/{{ item.1.name }}" state=absent
-#   with_subelements:
-#     - "{{ services }}"
-#     - service_credentials
-#     - { skip_missing: true }
+    - name: "Install the signed internal PKI certificates"
+      copy:
+        dest: "/etc/credentials/x509/{{ item.0.credentials.name }}/{{ item.0.mode }}/cert.pem"
+        content: "{{ item.1.cert }}"
+        mode: 0644
+      when: "item.1.changed"
+      loop: "{{ credentials | zip(x509_sign.results) | list }}"
 
-# Create a group for public credentials.
-- name: Create public-credentials group
-  group:
-    name: public-credentials
-    system: yes
+    - name: "Set permissions on the private keys"
+      file:
+        path: "/etc/credentials/x509/{{ item.credentials.name }}/{{ item.mode }}/private_key.pem"
+        group: "{{ item.credentials.name }}-credentials"
+        mode: 0640
+      loop: "{{ credentials }}"
+
+    # This should use the systemd module but it doesn't take lists of services.
+    - name: "Restart associated services"
+      shell: "systemctl restart {{ services[item.0.service].systemd_services | join(' ') }}"
+      when: "item.1.changed"
+      loop: "{{ credentials | zip(x509_sign.results) | list }}"
+
+  rescue:
+    - debug:
+        msg: "Failed to set up one or more credentials"
 
-# Create the root directory for public credentials.
-- file:
-    path: /etc/credentials/public
-    state: directory
diff --git a/roles/float-credentials/README.md b/roles/float-credentials/README.md
new file mode 100644
index 00000000..5edfe877
--- /dev/null
+++ b/roles/float-credentials/README.md
@@ -0,0 +1,2 @@
+This role includes the *credentials* role with the list of credentials
+derived from the service descriptions in services.yml.
diff --git a/roles/float-credentials/meta/main.yml b/roles/float-credentials/meta/main.yml
new file mode 100644
index 00000000..bd091102
--- /dev/null
+++ b/roles/float-credentials/meta/main.yml
@@ -0,0 +1,7 @@
+---
+
+dependencies:
+  - role: credentials
+    vars:
+      credentials: "{{ float_host_service_credentials }}"
+
diff --git a/roles/float-credentials/tasks/main.yml b/roles/float-credentials/tasks/main.yml
new file mode 100644
index 00000000..23a0988e
--- /dev/null
+++ b/roles/float-credentials/tasks/main.yml
@@ -0,0 +1,26 @@
+---
+
+# Distribute the SSO public key to all hosts.
+- file:
+    path: /etc/sso
+    state: directory
+    owner: root
+    group: root
+    mode: 0755
+
+- name: Install SSO public key
+  copy:
+    src: "{{ credentials_dir }}/sso/public.key"
+    dest: /etc/sso/public.key
+    mode: 0644
+
+# Create a group for public credentials.
+- name: Create public-credentials group
+  group:
+    name: public-credentials
+    system: yes
+
+# Create the root directory for public credentials.
+- file:
+    path: /etc/credentials/public
+    state: directory
-- 
GitLab