Zach Isbach %!s(int64=4) %!d(string=hai) anos
achega
8b3b3c26d8
Modificáronse 69 ficheiros con 2047 adicións e 0 borrados
  1. 20 0
      docs/workflow.txt
  2. 47 0
      plays/docker_reg-start.yaml
  3. 5 0
      plays/docker_reg-stop.yaml
  4. 43 0
      plays/gclnt-start.yaml
  5. 5 0
      plays/gclnt-stop.yaml
  6. 38 0
      plays/gluster-run.yaml
  7. 29 0
      plays/gluster-start.yaml
  8. 5 0
      plays/gluster-stop.yaml
  9. 70 0
      plays/hosts.ini
  10. 1 0
      plays/roles/add_group/defaults/main.yml
  11. 12 0
      plays/roles/add_group/tasks/main.yml
  12. 15 0
      plays/roles/base_user/defaults/main.yml
  13. 44 0
      plays/roles/base_user/tasks/main.yml
  14. 56 0
      plays/roles/common/handlers/main.yml
  15. 5 0
      plays/roles/docker/defaults/main.yml
  16. 80 0
      plays/roles/docker/tasks/main.yml
  17. 8 0
      plays/roles/docker_glusterfs/defaults/main.yml
  18. 56 0
      plays/roles/docker_glusterfs/tasks/main.yml
  19. 2 0
      plays/roles/ec2_hosts/defaults/main.yml
  20. 37 0
      plays/roles/ec2_hosts/tasks/main.yml
  21. 17 0
      plays/roles/ec2_launch/defaults/main.yml
  22. 2 0
      plays/roles/ec2_launch/meta/main.yml
  23. 99 0
      plays/roles/ec2_launch/tasks/main.yml
  24. 1 0
      plays/roles/ec2_term/defaults/main.yml
  25. 16 0
      plays/roles/ec2_term/tasks/main.yml
  26. 4 0
      plays/roles/etc_hosts/defaults/main.yml
  27. 10 0
      plays/roles/etc_hosts/tasks/main.yml
  28. 8 0
      plays/roles/fail2ban/defaults/main.yml
  29. 5 0
      plays/roles/fail2ban/handlers/main.yml
  30. 13 0
      plays/roles/fail2ban/tasks/main.yml
  31. 12 0
      plays/roles/fail2ban/zi3.j2
  32. 31 0
      plays/roles/gluster/defaults/main.yml
  33. 22 0
      plays/roles/gluster/handlers/main.yml
  34. 142 0
      plays/roles/gluster/tasks/main.yml
  35. 8 0
      plays/roles/gluster_mnt/defaults/main.yml
  36. 18 0
      plays/roles/gluster_mnt/tasks/main.yml
  37. 14 0
      plays/roles/gluster_vol/defaults/main.yml
  38. 21 0
      plays/roles/gluster_vol/handlers/main.yml
  39. 98 0
      plays/roles/gluster_vol/tasks/main.yml
  40. 25 0
      plays/roles/gluster_vol/tasks/options.yml
  41. 9 0
      plays/roles/hostname/defaults/main.yml
  42. 40 0
      plays/roles/hostname/tasks/main.yml
  43. 7 0
      plays/roles/iptables/defaults/main.yml
  44. 52 0
      plays/roles/iptables/gluster.j2
  45. 52 0
      plays/roles/iptables/gluster6.j2
  46. 16 0
      plays/roles/iptables/tasks/main.yml
  47. 17 0
      plays/roles/key_store/defaults/main.yml
  48. 127 0
      plays/roles/key_store/tasks/main.yml
  49. 12 0
      plays/roles/luks/defaults/main.yml
  50. 47 0
      plays/roles/luks/luks.j2
  51. 34 0
      plays/roles/luks/tasks/main.yml
  52. 4 0
      plays/roles/lvm/defaults/main.yml
  53. 22 0
      plays/roles/lvm/tasks/main.yml
  54. 3 0
      plays/roles/mount/defaults/main.yml
  55. 21 0
      plays/roles/mount/tasks/main.yml
  56. 6 0
      plays/roles/route53/defaults/main.yml
  57. 14 0
      plays/roles/route53/tasks/main.yml
  58. 6 0
      plays/roles/sshd_config/defaults/main.yml
  59. 2 0
      plays/roles/sshd_config/meta/main.yml
  60. 21 0
      plays/roles/sshd_config/sshd.j2
  61. 13 0
      plays/roles/sshd_config/tasks/main.yml
  62. 1 0
      plays/roles/timezone/defaults/main.yml
  63. 3 0
      plays/roles/timezone/tasks/main.yml
  64. 19 0
      plays/roles/updates/tasks/main.yml
  65. 15 0
      tls/.gitignore
  66. 20 0
      tls/bin/caSign.sh
  67. 228 0
      tls/bin/genCA.sh
  68. 1 0
      tls/bin/genCert.sh
  69. 91 0
      tls/openssl.conf

+ 20 - 0
docs/workflow.txt

@@ -0,0 +1,20 @@
+AWS prerequisites:
+EBS volumes
+Subnets
+Security groups
+
+TLS prerequisites:
+./bin/genCA.sh
+mkdir gluster && ./bin/genCA.sh gluster
+./bin/genCert.sh gluster nodeA
+./bin/genCert.sh gluster nodeB
+./bin/genCert.sh gluster clnt1
+
+Launch:
+ansible-playbook -i hosts.ini gluster-start.yaml
+ssh a.gluster.zi3.fi 'sudo ~/bin/luks-open' < disk_key
+ssh b.gluster.zi3.fi 'sudo ~/bin/luks-open' < disk_key
+ansible-playbook -i hosts.ini gluster-run.yaml
+
+fail2ban:
+fail2ban-client set sshd unbanip 75.172.12.35

+ 47 - 0
plays/docker_reg-start.yaml

@@ -0,0 +1,47 @@
+- name: Launch instances
+  hosts: docker_reg
+  gather_facts: no
+  vars:
+    aws_pub_ip: true
+  roles:
+    - ec2_launch
+
+- name: Initial setup
+  hosts: docker_reg
+  become: true
+  vars:
+    hostname_domain: "{{ aws_tag_name }}.{{ route53_zone }}"
+    hostname_fqdn: "{{ aws_tag_value }}.{{ hostname_domain }}"
+    hostname_tag: gluster
+    hostname_tag_domain: "{{ hostname_tag }}.{{ route53_zone }}"
+    hostname_cluster: "{{ groups.gluster }}"
+  roles:
+    - timezone
+    - hostname
+    - sshd_config
+    - updates
+    - base_user
+    - { role: updates, repeat: 1 }
+  tasks:
+  - name: Reboot (if needed)
+    reboot:
+    when: reboot_force
+
+- name: Install gluster
+  hosts: docker_reg
+  become: true
+  roles:
+    - key_store
+    - gluster
+    # Add the base user to any new groups
+    - { role: add_group, add_group_groups: 'docker_registry:2202' }
+
+- name: Install docker
+  hosts: docker_reg
+  become: true
+  vars:
+    docker_run: true
+    docker_glusterfs_servers: "{{ groups.gluster | join(',') }}"
+  roles:
+    - docker
+    - docker_glusterfs

+ 5 - 0
plays/docker_reg-stop.yaml

@@ -0,0 +1,5 @@
+- name: Terminate instances
+  hosts: docker_reg
+  gather_facts: no
+  roles:
+    - ec2_term

+ 43 - 0
plays/gclnt-start.yaml

@@ -0,0 +1,43 @@
+- name: Launch instances
+  hosts: gluster_clients
+  gather_facts: no
+  vars:
+    aws_pub_ip: true
+  roles:
+    - ec2_launch
+
+- name: Initial setup
+  hosts: gluster_clients
+  become: true
+  vars:
+    hostname_domain: "{{ domain }}.{{ route53_zone }}"
+    hostname_fqdn: "{{ aws_tag_value }}.{{ hostname_domain }}"
+    hostname_tag: gluster
+    hostname_cluster: "{{ groups.gluster }}"
+  roles:
+    - timezone
+    - hostname
+    - sshd_config
+    - updates
+    - base_user
+    - { role: updates, repeat: 1 }
+  tasks:
+  - name: Reboot (if needed)
+    reboot:
+    when: reboot_force
+
+- name: Init gluster
+  hosts: gluster_clients
+  become: true
+  vars:
+    gluster_mnt_srv: "a.{{ domain }}.{{ route53_zone }}"
+    gluster_mnt_bkp: "b.{{ domain }}.{{ route53_zone }}"
+
+  roles:
+    - key_store
+    - gluster
+    - { role: gluster_mnt,
+        gluster_mnt_vol: docker_registry,
+        gluster_mnt_gid: 2202,
+        gluster_mnt_mode: '0770'
+      }

+ 5 - 0
plays/gclnt-stop.yaml

@@ -0,0 +1,5 @@
+- name: Terminate instances
+  hosts: gluster_clients
+  gather_facts: no
+  roles:
+    - ec2_term

+ 38 - 0
plays/gluster-run.yaml

@@ -0,0 +1,38 @@
+- name: Init services
+  hosts: gluster
+  become: true
+  vars:
+    vg: gluster
+    lv: lib
+  roles:
+    - { role: iptables, iptables_tmpl: gluster }
+    - { role: iptables, iptables_ver: 6, iptables_tmpl: gluster6 }
+    - key_store
+    - fail2ban
+    - { role: lvm, lvm_vg: "{{ vg }}", lvm_lv: "{{ lv }}", lvm_size: 100M }
+    - { role: gluster,
+          gluster_libdev: "/dev/mapper/{{ vg }}-{{ lv }}",
+          gluster_cluster: "{{ groups.gluster }}"
+      }
+
+- name: Run services
+  hosts: gluster
+  become: true
+  vars:
+    vg: gluster
+    gluster_vol_cluster: "{{ groups['gluster'] }}"
+    gluster_vol_ssl_allow: "nodeA,nodeB,clnt1,docker_registry"
+  roles:
+    # Docker registry
+    - { role: lvm,
+          lvm_vg: "{{ vg }}",
+          lvm_lv: docker_registry,
+          lvm_size: 2G
+      }
+    - { role: gluster_vol,
+          gluster_vol_name: docker_registry,
+          gluster_vol_gid: 2202,
+          gluster_vol_dev: "/dev/mapper/{{ vg }}-docker_registry"
+      }
+    # Add the base user to any new groups
+    - { role: add_group, add_group_groups: 'docker_registry' }

+ 29 - 0
plays/gluster-start.yaml

@@ -0,0 +1,29 @@
+- name: Launch instances
+  hosts: gluster
+  gather_facts: no
+  roles:
+    - ec2_launch
+
+- name: Initial setup
+  hosts: gluster
+  become: true
+  vars:
+    hostname_domain: "{{ domain }}.{{ route53_zone }}"
+    hostname_fqdn: "{{ aws_tag_value }}.{{ hostname_domain }}"
+    hostname_tag: gluster
+    hostname_cluster: "{{ groups.gluster }}"
+    luks_dev: /dev/nvme1n1
+    luks_crypt_name: gluster
+    luks_vg_name: gluster
+  roles:
+    - timezone
+    - hostname
+    - sshd_config
+    - updates
+    - base_user
+    - luks
+    - { role: updates, repeat: 1 }
+  tasks:
+  - name: Reboot (if needed)
+    reboot:
+    when: reboot_force

+ 5 - 0
plays/gluster-stop.yaml

@@ -0,0 +1,5 @@
+- name: Terminate instances
+  hosts: gluster
+  gather_facts: no
+  roles:
+    - ec2_term

+ 70 - 0
plays/hosts.ini

@@ -0,0 +1,70 @@
+[local]
+localhost ansible_connection=local ansible_python_interpreter=/opt/local/bin/python3
+
+[all:children]
+gluster
+gluster_clients
+docker_reg
+
+[all:vars]
+ansible_user=ubuntu
+ec2_launch_key=Balto
+# arm64 1GB/2core
+ec2_launch_ec2type=t4g.micro
+# Ubuntu 22.04-LTS
+ec2_launch_ami=ami-0db84aebfa8d17e23
+gluster_tls_prefix='../tls/gluster'
+gluster_ca=ca-chain.pem
+route53_zone=aws.zi3.fi
+
+[gluster:children]
+gluster_a
+gluster_b
+
+[gluster:vars]
+domain=gluster
+aws_tag_name=gluster
+gluster_run=True
+ec2_launch_pub_ip=True
+ec2_launch_ebs_match=True
+ec2_launch_sec_groups='Outbound;ICMP;SSH;Gluster'
+
+[gluster_a]
+a.gluster.aws.zi3.fi
+
+[gluster_a:vars]
+aws_tag_value=a
+ec2_launch_subnet=subnet-04a22339184f4874f
+gluster_tls_name=nodeA
+
+[gluster_b]
+b.gluster.aws.zi3.fi
+
+[gluster_b:vars]
+aws_tag_value=b
+ec2_launch_subnet=subnet-0b85c9e80b74e913c
+gluster_tls_name=nodeB
+
+[gluster_clients]
+clnt1.gluster.aws.zi3.fi
+
+[gluster_clients:vars]
+aws_tag_name=gclient
+aws_tag_value=clnt1
+ec2_launch_subnet=subnet-0a926876bfe115312
+ec2_launch_sec_groups='Outbound;ICMP;SSH;Gluster_Clients'
+ec2_launch_pub_ip=True
+domain=gluster
+gluster_tls_name=clnt1
+
+[docker_reg]
+registry.docker.aws.zi3.fi
+
+[docker_reg:vars]
+domain=docker
+aws_tag_name=docker
+aws_tag_value=registry
+ec2_launch_subnet=subnet-0b307e772307a1bd5
+ec2_launch_sec_groups='Outbound;ICMP;SSH;Backend_Swarm;Gluster_Clients'
+ec2_launch_pub_ip=True
+gluster_tls_name=docker_registry

+ 1 - 0
plays/roles/add_group/defaults/main.yml

@@ -0,0 +1 @@
+add_group_user: "{{ base_user | default('profplump') }}"

+ 12 - 0
plays/roles/add_group/tasks/main.yml

@@ -0,0 +1,12 @@
+- name: Ensure groups exist {{ add_group_groups }}
+  group:
+    name: "{{ item.split(':')[0] }}"
+    gid: "{{ item.split(':')[1] | default(omit) }}"
+    state: present
+  with_items: "{{ add_group_groups.split(',') }}"
+- name: Add {{ add_group_user }} to groups {{ add_group_groups }}
+  user:
+    name: "{{ add_group_user }}"
+    groups: "{{ add_group_groups | regex_replace(':\\d+') }}"
+    append: true
+  when: add_group_user is defined

+ 15 - 0
plays/roles/base_user/defaults/main.yml

@@ -0,0 +1,15 @@
+base_user_os: ubuntu
+
+base_user: profplump
+base_user_uid: 2000
+base_user_name: Zach Isbach
+base_user_groups: sudo
+base_user_shell: /bin/bash
+base_user_rc: "{{ lookup('env','HOME') + '/.bashrc' }}"
+
+base_user_key: "{{ lookup('env','HOME') + '/.ssh/id_ed25519.pub' }}"
+base_user_keys:
+  - "{{ lookup('env','HOME') + '/.ssh/friends/pumpkin-ed25519.pub' }}"
+  - "{{ lookup('env','HOME') + '/.ssh/friends/rondo-ed25519.pub' }}"
+  - "{{ lookup('env','HOME') + '/.ssh/friends/walton-ed25519.pub' }}"
+base_user_keylist: "{{  [ base_user_key ] + base_user_keys }}"

+ 44 - 0
plays/roles/base_user/tasks/main.yml

@@ -0,0 +1,44 @@
+- name: Add {{ base_user }} group
+  group:
+    name: "{{ base_user }}"
+    gid: "{{ base_user_uid }}"
+- name: Add {{ base_user }} user
+  user:
+    uid: "{{ base_user_uid }}"
+    name: "{{ base_user }}"
+    comment: "{{ base_user_name }}"
+    shell: "{{ base_user_shell }}"
+    group: "{{ base_user }}"
+    groups: "{{ base_user_groups }}"
+    append: true
+
+- name: Enable sudo access
+  lineinfile:
+    path: /etc/sudoers.d/10-base_users
+    line: "{{ base_user }} ALL=(ALL) NOPASSWD:ALL"
+    create: yes
+    mode: 0440
+    owner: root
+    group: root
+
+- name: Set {{ base_user }} keypairs
+  ansible.posix.authorized_key:
+    user: "{{ base_user }}"
+    state: present
+    key: '{{ item }}'
+  with_file: "{{ base_user_keylist }}"
+
+- name: Set ubuntu keypairs
+  ansible.posix.authorized_key:
+    user: "{{ base_user_os }}"
+    state: present
+    key: '{{ item }}'
+  with_file: "{{ base_user_key }}"
+
+- name: Install .bashrc
+  copy:
+    src: "{{ base_user_rc }}"
+    dest: "/home/{{ base_user }}/.bashrc"
+    owner: "{{ base_user }}"
+    group: "{{ base_user }}"
+    mode: 0644

+ 56 - 0
plays/roles/common/handlers/main.yml

@@ -0,0 +1,56 @@
+- name: Restart sshd
+  systemd:
+    name: sshd
+    state: restarted
+  listen: sshd_restart
+
+- name: Wait for SSH
+  wait_for:
+    host: "{{ inventory_hostname }}"
+    port: "{{ ansible_port | default('22') }}"
+    search_regex: OpenSSH
+  delegate_to: localhost
+  become: false
+  listen: known_hosts
+
+- name: Find IP(s) for hostname
+  set_fact:
+    known_hosts_ips: "{{ lookup('community.general.dig', inventory_hostname + '.', wantlist=true) }}"
+  delegate_to: localhost
+  become: false
+  listen: known_hosts
+
+- name: Find /etc/hosts IP(s) for host
+  shell: cat /etc/hosts | awk '$2 == "{{ inventory_hostname }}" {print $1}'
+  register: known_hosts_etc
+  delegate_to: localhost
+  become: false
+  listen: known_hosts
+- name: Append /etc/hosts results
+  set_fact:
+    known_hosts_ips: "{{ known_hosts_ips + [ item ]}}"
+  when: item != ''
+  loop: "{{ known_hosts_etc.stdout_lines }}"
+  delegate_to: localhost
+  become: false
+  listen: known_hosts
+
+- name: Delete old host keys
+  shell: "sleep {{ 2 | random }}.{{ 9 | random }} && ssh-keygen -R '{{ item }}' && rm -f ~/.ssh/known_hosts.old"
+  when: item != '' and item != 'NXDOMAIN'
+  loop: "{{ known_hosts_ips + [ inventory_hostname ] }}"
+  register: known_hosts_del
+  changed_when: "'not found in' in known_hosts_del.stderr"
+  delegate_to: localhost
+  become: false
+  listen: known_hosts
+
+- name: Add new keys
+  shell: "sleep {{ 2 | random }}.{{ 9 | random }} && ssh-keyscan -H '{{ inventory_hostname }}' >> ~/.ssh/known_hosts 2>/dev/null"
+  register: keys
+  until: keys.rc == 0
+  retries: 5
+  delay: 5
+  delegate_to: localhost
+  become: false
+  listen: known_hosts

+ 5 - 0
plays/roles/docker/defaults/main.yml

@@ -0,0 +1,5 @@
+docker_owner: root
+docker_group: docker
+docker_gid: 2103
+
+docker_keydir: /data/keys/docker

+ 80 - 0
plays/roles/docker/tasks/main.yml

@@ -0,0 +1,80 @@
+# Configure groups
+- name: Ensure group "{{ docker_group }}" exists
+  include_role:
+    name: add_group
+  vars:
+    add_group_groups: "{{ docker_group }}:{{ docker_gid }}"
+
+# Install TLS files
+- name: Install the TLS CA
+  copy:
+    src: "{{ docker_ca }}"
+    dest: "{{ docker_keydir }}/ca.pem"
+    owner: "{{ docker_owner }}"
+    group: "{{ docker_group }}"
+    mode: 0400
+  register: docker_copy
+  changed_when: docker_copy.src is defined
+  when: docker_ca is defined
+- name: Install the TLS certificate
+  copy:
+    src: "{{ docker_cert }}"
+    dest: "{{ docker_keydir }}/node.pem"
+    owner: "{{ docker_owner }}"
+    group: "{{ docker_group }}"
+    mode: 0400
+  register: docker_copy
+  changed_when: docker_copy.src is defined
+  when: docker_cert is defined
+- name: Install the TLS key
+  copy:
+    src: "{{ docker_key }}"
+    dest: "{{ docker_keydir }}/node.key"
+    owner: "{{ docker_owner }}"
+    group: "{{ docker_group }}"
+    mode: 0400
+  register: docker_copy
+  changed_when: docker_copy.src is defined
+  when: docker_cert is defined
+
+# Install docker
+- name: Add the Docker apt repo
+  vars:
+    docker_gpgkey: /etc/apt/trusted.gpg.d/docker.asc
+  block:
+    - name: Fetch the Docker GPG key
+      ansible.builtin.get_url:
+        url: https://download.docker.com/linux/ubuntu/gpg
+        dest: "{{ docker_gpgkey }}"
+
+    - name: Add the Docker repo
+      ansible.builtin.apt_repository:
+        repo: "deb [arch=arm64 signed-by={{ docker_gpgkey }}] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+        state: present
+- name: Sync apt
+  apt:
+    update_cache: yes
+  changed_when: false
+- name: Install Docker
+  apt:
+    name:
+      - docker-ce
+      - docker-ce-cli
+      - containerd.io
+      - docker-compose-plugin
+      - python3-docker
+      - python3-websocket
+    state: latest
+
+# Run docker
+- name: Start Docker
+  systemd:
+    name: docker
+    enabled: no
+    state: started
+  when: docker_run
+- name: Start a swarm from first node
+  community.docker.docker_swarm:
+    state: present
+  delegate_to: "{{ groups.gluster_clients[0] }}"
+  run_once: true

+ 8 - 0
plays/roles/docker_glusterfs/defaults/main.yml

@@ -0,0 +1,8 @@
+docker_glusterfs_keydir: /data/keys/gluster
+docker_glusterfs_pluginssl: "{{ docker_glusterfs_keydir }}/ssl"
+docker_glusterfs_hostssl: /etc/ssl
+
+docker_glusterfs_owner: root
+docker_glusterfs_group: root
+docker_glusterfs_dirmode: "0755"
+docker_glusterfs_mode: "0640"

+ 56 - 0
plays/roles/docker_glusterfs/tasks/main.yml

@@ -0,0 +1,56 @@
+- name: "Create {{ docker_glusterfs_pluginssl }}"
+  file:
+    path: "{{ docker_glusterfs_pluginssl }}"
+    owner: "{{ docker_glusterfs_owner }}"
+    group: "{{ docker_glusterfs_group }}"
+    mode: "{{ docker_glusterfs_dirmode }}"
+    state: directory
+  register: docker_glusterfs_createssl
+- name: Copy {{ docker_glusterfs_hostssl }} to {{ docker_glusterfs_pluginssl }}
+  ansible.builtin.copy:
+    remote_src: true
+    src: "{{ docker_glusterfs_hostssl }}/"
+    dest: "{{ docker_glusterfs_pluginssl }}/"
+  when: docker_glusterfs_createssl.changed
+- name: Install the SSL CA
+  ansible.builtin.copy:
+    remote_src: true
+    src: "{{ docker_glusterfs_keydir }}/ca.pem"
+    dest: "{{ docker_glusterfs_pluginssl }}/glusterfs.ca"
+    owner: "{{ docker_glusterfs_owner }}"
+    group: "{{ docker_glusterfs_group }}"
+    mode: "{{ docker_glusterfs_mode }}"
+- name: Install the SSL certificate
+  ansible.builtin.copy:
+    remote_src: true
+    src: "{{ docker_glusterfs_keydir }}/node.pem"
+    dest: "{{ docker_glusterfs_pluginssl }}/glusterfs.pem"
+    owner: "{{ docker_glusterfs_owner }}"
+    group: "{{ docker_glusterfs_group }}"
+    mode: "{{ docker_glusterfs_mode }}"
+- name: Install the SSL key
+  ansible.builtin.copy:
+    remote_src: true
+    src: "{{ docker_glusterfs_keydir }}/node.key"
+    dest: "{{ docker_glusterfs_pluginssl }}/glusterfs.key"
+    owner: "{{ docker_glusterfs_owner }}"
+    group: "{{ docker_glusterfs_group }}"
+    mode: "{{ docker_glusterfs_mode }}"
+- name: Mount {{ docker_glusterfs_pluginssl }} @ {{ docker_glusterfs_hostssl }}
+  ansible.posix.mount:
+    path: "{{ docker_glusterfs_hostssl }}"
+    src: "{{ docker_glusterfs_pluginssl }}"
+    opts: bind
+    fstype: none
+    state: mounted
+
+# This would be one task if not for the SSL path fuckery
+- name: Install glusterfs-volume-plugin
+  community.docker.docker_plugin:
+    state: enable
+    alias: glusterfs
+    #https://github.com/marcelo-ochoa/docker-volume-plugins/
+    plugin_name: "mochoa/glusterfs-volume-plugin-aarch64"
+    plugin_options:
+      SERVERS: "{{ docker_glusterfs_servers }}"
+      SECURE_MANAGEMENT: true

+ 2 - 0
plays/roles/ec2_hosts/defaults/main.yml

@@ -0,0 +1,2 @@
+ec2_hosts_tag: "{{ aws_tag_name }}"
+ec2_hosts_domain: "{{ domain | default('local') }}"

+ 37 - 0
plays/roles/ec2_hosts/tasks/main.yml

@@ -0,0 +1,37 @@
+- name: "Find ec2 instances: #{{ ec2_hosts_tag }}"
+  amazon.aws.ec2_instance_info:
+    filters: '{ "tag-key": "{{ ec2_hosts_tag }}",
+      "instance-state-name": [ "running" ] }'
+  register: ec2_hosts_instances
+  delegate_to: localhost
+  become: false
+
+- name: Extract IPs and tag names
+  set_fact:
+    ec2_hosts_tags: "{{
+      ec2_hosts_instances |
+      community.general.json_query('instances[*].tags') |
+      map(attribute=ec2_hosts_tag)
+    }}"
+    ec2_hosts_ips: "{{
+      ec2_hosts_instances |
+      community.general.json_query('instances[*].network_interfaces[0].association.public_ip')
+    }}"
+    ec2_hosts_ips_v6: "{{
+      ec2_hosts_instances |
+      community.general.json_query('instances[*].network_interfaces[0].ipv6_addresses[0].ipv6_address')
+    }}"
+    ec2_hosts_ips_local: "{{
+      ec2_hosts_instances |
+      community.general.json_query('instances[*].private_ip_address')
+    }}"
+  delegate_to: localhost
+  become: false
+
+- name: Combine
+  set_fact:
+    ec2_hosts: "{{ dict(ec2_hosts_tags | zip(ec2_hosts_ips)) }}"
+    ec2_hosts_v6: "{{ dict(ec2_hosts_tags | zip(ec2_hosts_ips_v6)) }}"
+    ec2_hosts_local: "{{ dict(ec2_hosts_tags | zip(ec2_hosts_ips_local)) }}"
+  delegate_to: localhost
+  become: false

+ 17 - 0
plays/roles/ec2_launch/defaults/main.yml

@@ -0,0 +1,17 @@
+ec2_launch_zone: "{{ aws_zone | default(omit) }}"
+ec2_launch_region: "{{ aws_region | default('us-west-2') }}"
+ec2_launch_tag_name: "{{ aws_tag_name }}"
+ec2_launch_tag_value: "{{ aws_tag_value }}"
+ec2_launch_domain: "{{ domain | default('local') }}"
+
+ec2_launch_ec2type: t4g.micro
+ec2_launch_count: 1
+
+ec2_launch_vol_dev: /dev/sda1
+ec2_launch_vol_size: 8
+ec2_launch_vol_type: gp3
+ec2_launch_vol_del: yes
+
+ec2_launch_ebs_match: false
+ec2_launch_pub_ip: false
+ec2_launch_eip: false

+ 2 - 0
plays/roles/ec2_launch/meta/main.yml

@@ -0,0 +1,2 @@
+dependencies:
+- role: common

+ 99 - 0
plays/roles/ec2_launch/tasks/main.yml

@@ -0,0 +1,99 @@
+- name: Launch EC2 instances
+  amazon.aws.ec2_instance:
+    availability_zone: "{{ ec2_launch_zone | default(omit) }}"
+    exact_count: "{{ ec2_launch_count }}"
+    filters: '{ "tag:{{ ec2_launch_tag_name }}": "{{ ec2_launch_tag_value }}", "instance-state-name": [ "running" ] }'
+    image_id: "{{ ec2_launch_ami }}"
+    instance_type: "{{ ec2_launch_ec2type }}"
+    key_name: "{{ ec2_launch_key }}"
+    network:
+      assign_public_ip: "{{ ec2_launch_pub_ip }}"
+      interfaces: "{{ ec2_launch_eni is defined and [ { 'id': ec2_launch_eni } ] or omit }}"
+    region: "{{ ec2_launch_region }}"
+    security_groups: "{{ ec2_launch_sec_groups is defined and ec2_launch_sec_groups | split(';') or omit }}"
+    tags: '{ "{{ ec2_launch_tag_name }}": "{{ ec2_launch_tag_value }}" }'
+    volumes:
+    - device_name: "{{ ec2_launch_vol_dev }}"
+      ebs:
+        delete_on_termination: "{{ ec2_launch_vol_del }}"
+        encrypted: True
+        volume_size: "{{ ec2_launch_vol_size }}"
+        volume_type: "{{ ec2_launch_vol_type }}"
+    vpc_subnet_id: "{{ ec2_launch_subnet | default(omit) }}"
+    wait: True
+  register: ec2_launch_instances
+  notify: known_hosts
+  delegate_to: localhost
+- name: Wait just a few ticks for AWS to settle
+  ansible.builtin.pause:
+    seconds: 5
+  when: ec2_launch_instances.changed
+
+- name: Find tagged instances
+  amazon.aws.ec2_instance_info:
+    region: "{{ ec2_launch_region }}"
+    filters: '{ "tag:{{ ec2_launch_tag_name }}": "{{ ec2_launch_tag_value }}",
+      "instance-state-name": [ "running" ] }'
+  register: ec2_launch_tagged
+  when: ec2_launch_sec_groups is defined
+  delegate_to: localhost
+- name: Update instance security groups
+  amazon.aws.ec2_instance:
+    instance_ids: [ "{{ ec2_launch_tagged.instances[0].instance_id }}" ]
+    security_groups: "{{ ec2_launch_sec_groups | split(';') }}"
+    vpc_subnet_id: "{{ ec2_launch_subnet }}"
+  when: ec2_launch_sec_groups is defined and ec2_launch_subnet is defined
+  delegate_to: localhost
+- name: Update ENI security groups
+  amazon.aws.ec2_eni:
+    eni_id: "{{ ec2_launch_tagged.instances[0].network_interfaces[0].network_interface_id }}"
+    security_groups: "{{ ec2_launch_sec_groups | split(';') }}"
+    subnet_id: "{{ ec2_launch_subnet }}"
+  when: ec2_launch_sec_groups is defined
+  delegate_to: localhost
+
+- name: Grab our local and global addresses
+  include_role:
+    name: ec2_hosts
+  vars:
+    ec2_hosts_tag: "{{ ec2_launch_tag_name }}"
+- name: Update route53 A record
+  include_role:
+    name: route53
+  vars:
+    route53_record: "{{ item.key }}.{{ ec2_launch_domain }}"
+    route53_value: "{{ item.value }}"
+  loop: "{{ ec2_hosts | dict2items }}"
+- name: Update route53 AAAA record
+  include_role:
+    name: route53
+  vars:
+    route53_type: AAAA
+    route53_record: "{{ item.key }}.{{ ec2_launch_domain }}"
+    route53_value: "{{ item.value }}"
+  loop: "{{ ec2_hosts_v6 | dict2items }}"
+
+- name: Find tagged instances
+  amazon.aws.ec2_instance_info:
+    region: "{{ ec2_launch_region }}"
+    filters: '{
+        "tag:{{ ec2_launch_tag_name }}": "{{ ec2_launch_tag_value }}",
+        "instance-state-name": [ "running" ]
+      }'
+  register: ec2_launch_tagged
+  delegate_to: localhost
+  when: ec2_launch_ebs_match
+- name: Find tagged EBS volumes
+  amazon.aws.ec2_vol_info:
+    region: "{{ ec2_launch_region }}"
+    filters: '{ "tag:{{ ec2_launch_tag_name }}": "{{ ec2_launch_tag_value }}" }'
+  register: ec2_launch_ebs
+  delegate_to: localhost
+  when: ec2_launch_ebs_match
+- name: Attach matching volumes
+  amazon.aws.ec2_vol:
+    region: "{{ ec2_launch_region }}"
+    id: "{{ ec2_launch_ebs.volumes[0].id }}"
+    instance: "{{ ec2_launch_tagged.instances[0].instance_id }}"
+  delegate_to: localhost
+  when: ec2_launch_ebs_match

+ 1 - 0
plays/roles/ec2_term/defaults/main.yml

@@ -0,0 +1 @@
+aws_region: us-west-2

+ 16 - 0
plays/roles/ec2_term/tasks/main.yml

@@ -0,0 +1,16 @@
+- name: Find our EC2 instances
+  amazon.aws.ec2_instance_info:
+    region: "{{ aws_region }}"
+    filters: '{ "tag:{{ aws_tag_name }}": "{{ aws_tag_value }}", "instance-state-name": [ "pending", "running" ] }'
+  register: aws_ec2
+  delegate_to: localhost
+
+- name: Terminate any running instances
+  amazon.aws.ec2_instance:
+    instance_ids:
+      - "{{ item.instance_id }}"
+    region: "{{ aws_region }}"
+    state: absent
+    wait: yes
+  loop: "{{ aws_ec2.instances }}"
+  delegate_to: localhost

+ 4 - 0
plays/roles/etc_hosts/defaults/main.yml

@@ -0,0 +1,4 @@
+etc_hosts_name: "{{ inventory_hostname }}"
+
+# This only runs on localhost, where I use TouchID
+ansible_become_password: nothing

+ 10 - 0
plays/roles/etc_hosts/tasks/main.yml

@@ -0,0 +1,10 @@
+- name: Add the hostname
+  lineinfile:
+    path: /etc/hosts
+    insertafter: '^# Ansible'
+    regexp: "{{ etc_hosts_name }}$"
+    line: "{{ etc_hosts_ip }} {{ etc_hosts_name }}" 
+  become: true
+  notify: known_hosts
+  changed_when: true
+  delegate_to: localhost

+ 8 - 0
plays/roles/fail2ban/defaults/main.yml

@@ -0,0 +1,8 @@
+fail2ban_user: root
+fail2ban_group: root
+fail2ban_mode: 0644
+
+fail2ban_dir: "/etc/fail2ban/jail.d"
+fail2ban_file: "{{ fail2ban_dir }}/zi3.conf"
+
+fail2ban_tmpl: zi3

+ 5 - 0
plays/roles/fail2ban/handlers/main.yml

@@ -0,0 +1,5 @@
+- name: Restart fail2ban
+  systemd:
+    name: fail2ban
+    state: restarted
+  listen: fail2ban_restart

+ 13 - 0
plays/roles/fail2ban/tasks/main.yml

@@ -0,0 +1,13 @@
+- name: Install fail2ban
+  apt:
+    name: fail2ban
+    state: latest
+  notify: fail2ban_restart
+- name: Upload jail.local template {{ fail2ban_tmpl }}
+  template:
+    src: "../{{ fail2ban_tmpl }}.j2"
+    dest: "{{ fail2ban_file }}"
+    owner: "{{ fail2ban_user }}"
+    group: "{{ fail2ban_group }}"
+    mode: "{{ fail2ban_mode }}"
+  notify: fail2ban_restart

+ 12 - 0
plays/roles/fail2ban/zi3.j2

@@ -0,0 +1,12 @@
+[DEFAULT]  
+ignoreip  = 127.0.0.1/8 ::1 75.172.12.35
+backend   = systemd
+banaction = iptables-allports
+
+bantime  = 1d
+findtime = 1h
+maxretry = 5
+
+[sshd] 
+enabled = true
+mode    = aggressive

+ 31 - 0
plays/roles/gluster/defaults/main.yml

@@ -0,0 +1,31 @@
+# Disable the daemon by default
+gluster_run: false
+
+# Set a limit for port numbers
+gluster_volfile: /etc/glusterfs/glusterd.vol
+gluster_max_port: 49159
+
+# TLS params
+gluster_cert_depth: 1
+
+# Local FS params
+gluster_owner: root
+gluster_group: gluster
+gluster_gid:   2200
+
+# Where gluster normally keeps things
+gluster_ssldir: /usr/lib/ssl
+gluster_libmnt: /var/lib/glusterd
+gluster_secure: "{{ gluster_libmnt }}/secure-access"
+
+# Where we want gluster to keep things
+gluster_datadir: /data/gluster
+gluster_keydir: /data/keys/gluster
+# Undefined by default - we only need this for persistance
+#gluster_libdev: /dev/mapper/gluster-lib
+
+gluster_tls_prefix: tls
+gluster_tls_cert_ext: pem
+gluster_tls_cert_dir: "{{ gluster_tls_prefix }}/certs"
+gluster_tls_key_ext: ansible
+gluster_tls_key_dir: "{{ gluster_tls_prefix }}/keys"

+ 22 - 0
plays/roles/gluster/handlers/main.yml

@@ -0,0 +1,22 @@
+- name: Restart glusterd
+  systemd:
+    name: glusterd
+    enabled: no
+    state: restarted
+  when: gluster_run
+  listen: gluster_restart
+  notify: gluster_probe
+
+- name: Give it a second...
+  wait_for:
+    timeout: 2
+  when: gluster_run
+  listen: gluster_probe
+
+- name: Probe the cluster
+  gluster.gluster.gluster_peer:
+      state: present
+      nodes: "{{ gluster_cluster }}"
+  when: gluster_run
+  run_once: true
+  listen: gluster_probe

+ 142 - 0
plays/roles/gluster/tasks/main.yml

@@ -0,0 +1,142 @@
+- name: Add base_user to the group "{{ gluster_group }}"
+  include_role:
+    name: add_group
+  vars:
+    add_group_groups: "{{ gluster_group }}:{{ gluster_gid }}"
+- name: Init gluster paths
+  file:
+    path: "{{ item }}"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0750
+    state: directory
+  loop: [ "{{ gluster_datadir }}", "{{ gluster_keydir }}", "{{ gluster_libmnt }}" ]
+
+- name: Init alternate libdir
+  file:
+    path: "{{ gluster_libmnt }}"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0750
+    state: directory
+  when: gluster_libdev is defined
+- name: Mount gluster lib storage
+  ansible.posix.mount:
+    path: "{{ gluster_libmnt }}"
+    src: "{{ gluster_libdev }}"
+    fstype: xfs
+    opts: defaults,noatime
+    state: mounted
+  when: gluster_libdev is defined
+- name: Secure alternate libdir
+  file:
+    path: "{{ gluster_libmnt }}"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0750
+    state: directory
+  when: gluster_libdev is defined
+
+- name: Install the TLS CA
+  copy:
+    src: "{{ gluster_tls_prefix }}/{{ gluster_ca }}"
+    dest: "{{ gluster_keydir }}/ca.pem"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0400
+  register: gluster_copy
+  changed_when: gluster_copy.src is defined
+  when: gluster_ca is defined
+  notify: gluster_restart
+- name: Install the TLS certificate
+  copy:
+    src: "{{ gluster_tls_cert_dir }}/{{ gluster_tls_name }}.{{ gluster_tls_cert_ext }}"
+    dest: "{{ gluster_keydir }}/node.pem"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0400
+  register: gluster_copy
+  changed_when: gluster_copy.src is defined
+  when: gluster_tls_name is defined
+  notify: gluster_restart
+- name: Install the TLS key
+  copy:
+    src: "{{ gluster_tls_key_dir }}/{{ gluster_tls_name }}.{{ gluster_tls_key_ext }}"
+    dest: "{{ gluster_keydir }}/node.key"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0400
+  register: gluster_copy
+  changed_when: gluster_copy.src is defined
+  when: gluster_tls_name is defined
+  notify: gluster_restart
+
+- name: Init gluster SSL path
+  file:
+    path: "{{ gluster_ssldir }}"
+    owner: root
+    group: root
+    mode: 0755
+    state: directory
+  when: gluster_cert is defined or gluster_ca is defined
+  notify: gluster_restart
+- name: Link the SSL CA
+  file:
+    src: "{{ gluster_keydir }}/ca.pem"
+    dest: "{{ gluster_ssldir }}/glusterfs.ca"
+    state: link
+  when: gluster_ca is defined
+  notify: gluster_restart
+- name: Link the SSL certificate
+  file:
+    src: "{{ gluster_keydir }}/node.pem"
+    dest: "{{ gluster_ssldir }}/glusterfs.pem"
+    state: link
+  when: gluster_tls_name is defined
+  notify: gluster_restart
+- name: Link the SSL key
+  file:
+    src: "{{ gluster_keydir }}/node.key"
+    dest: "{{ gluster_ssldir }}/glusterfs.key"
+    state: link
+  when: gluster_tls_name is defined
+  notify: gluster_restart
+
+- name: Install gluster
+  apt:
+    name: glusterfs-server
+    state: latest
+  notify: gluster_restart
+
+- name: Require TLS on the mgmt path
+  file:
+    path: "{{ gluster_secure }}"
+    owner: "{{ gluster_owner }}"
+    group: "{{ gluster_group }}"
+    mode: 0640
+    state: touch
+  register: gluster_touch
+  when: gluster_cert is defined or gluster_ca is defined
+  changed_when: gluster_touch.diff.before.state != "file"
+  notify: gluster_restart
+- name: Accept CA-based auth on the mgmt path
+  lineinfile:
+    path: "{{ gluster_secure }}"
+    insertbefore: BOF
+    line: "option transport.socket.ssl-cert-depth {{ gluster_cert_depth }}"
+  when: gluster_ca is defined
+  notify: gluster_restart
+- name: Limit dynamic ports to a small range
+  lineinfile:
+    path: "{{ gluster_volfile }}"
+    regexp: "^[#\\s]*option max-port"
+    line: "    option max-port  {{ gluster_max_port }}"
+  notify: gluster_restart
+
+- name: Start glusterd
+  systemd:
+    name: glusterd
+    enabled: no
+    state: started
+  notify: gluster_probe
+  when: gluster_run

+ 8 - 0
plays/roles/gluster_mnt/defaults/main.yml

@@ -0,0 +1,8 @@
+gluster_mnt_srv: a.gluster
+gluster_mnt_bkp: b.gluster
+
+gluster_mnt_path: "/mnt/{{ gluster_mnt_vol }}"
+
+gluster_mnt_mode: '0750'
+gluster_mnt_owner: root
+gluster_mnt_grp: "{{ gluster_mnt_vol }}"

+ 18 - 0
plays/roles/gluster_mnt/tasks/main.yml

@@ -0,0 +1,18 @@
+- name: Add base_user to the group "{{ gluster_mnt_vol }}"
+  include_role:
+    name: add_group
+  vars:
+    add_group_groups: "{{ gluster_mnt_vol }}:{{ gluster_mnt_gid }}"
+  when: gluster_mnt_gid
+
+- name: Mount {{ gluster_mnt_vol }}
+  include_role:
+    name: mount
+  vars:
+    mount_path: "{{ gluster_mnt_path }}"
+    mount_mode: "{{ gluster_mnt_mode }}"
+    mount_owner: "{{ gluster_mnt_owner }}"
+    mount_group: "{{ gluster_mnt_grp }}" 
+    mount_dev: "{{ gluster_mnt_srv }}:/{{ gluster_mnt_vol }}"
+    mount_fs: glusterfs
+    mount_opts: "defaults,_netdev,noatime,backupvolfile-server={{ gluster_mnt_bkp }}"

+ 14 - 0
plays/roles/gluster_vol/defaults/main.yml

@@ -0,0 +1,14 @@
+gluster_vol_cert_depth: 1
+gluster_vol_ssl: on
+gluster_vol_ssl_allow: '*'
+gluster_vol_ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:!SSLv3:!TLSv1:!TLSv1.1'
+# Ideally we would disable TLSv1.2 as well, but gluster won't connect if we do
+#gluster_vol_ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:!SSLv3:!TLSv1:!TLSv1.1:!TLSv1.2'
+
+gluster_vol_replicas: 2
+gluster_vol_datadir: "/data/gluster/{{ gluster_vol_name }}"
+gluster_vol_brick: "{{ gluster_vol_datadir }}/brick"
+#gluster_vol_dev: "/dev/mapper/gluster-{{ gluster_vol_name }}"
+gluster_vol_owner: root
+gluster_vol_group: "{{ gluster_vol_name }}"
+gluster_vol_perms: 0770

+ 21 - 0
plays/roles/gluster_vol/handlers/main.yml

@@ -0,0 +1,21 @@
+- name: Check if {{ gluster_vol_name }} is running
+  command:
+    argv:
+      - gluster
+      - volume
+      - status
+      - "{{ gluster_vol_name }}"
+  register: gluster_vol_stopped
+  changed_when: "not ('is not started' in gluster_vol_stopped.stderr)"
+  failed_when: false
+  run_once: true
+  when: gluster_vol_name == gluster_vol_handler
+  listen: gluster_vol_restart
+- name: Stop {{ gluster_vol_name }}
+  gluster.gluster.gluster_volume:
+    state: stopped
+    name: "{{ gluster_vol_name }}"
+  run_once: true
+  when: gluster_vol_stopped.changed and gluster_vol_name == gluster_vol_handler
+  listen: gluster_vol_restart
+  

+ 98 - 0
plays/roles/gluster_vol/tasks/main.yml

@@ -0,0 +1,98 @@
+- name: Ensure group {{ gluster_vol_group }} exists
+  group:
+    name: "{{ gluster_vol_group }}"
+    gid: "{{ gluster_vol_gid }}"
+    state: present
+- name: Init volume mountpoint
+  file:
+    path: "{{ gluster_vol_datadir }}"
+    owner: "{{ gluster_vol_owner }}"
+    group: "{{ gluster_vol_group }}"
+    mode: "{{ gluster_vol_perms }}"
+    state: directory
+  when: gluster_vol_dev is defined
+- name: Mount the data dev
+  ansible.posix.mount:
+    path: "{{ gluster_vol_datadir }}"
+    src: "{{ gluster_vol_dev }}"
+    fstype: xfs
+    opts: defaults,noatime
+    state: mounted
+  register: gluster_vol_mounted
+  notify: gluster_vol_restart
+  when: gluster_vol_dev is defined
+- name: Init volume paths
+  file:
+    path: "{{ item }}"
+    owner: "{{ gluster_vol_owner }}"
+    group: "{{ gluster_vol_group }}"
+    mode: "{{ gluster_vol_perms }}"
+    state: directory
+  loop: [ "{{ gluster_vol_datadir }}", "{{ gluster_vol_brick }}" ]
+
+- name: Create {{ gluster_vol_name }}
+  gluster.gluster.gluster_volume:
+    state: present
+    start_on_create: false
+    replicas: "{{ gluster_vol_replicas }}"
+    name: "{{ gluster_vol_name }}"
+    bricks: "{{ gluster_vol_brick }}"
+    cluster: "{{ gluster_vol_cluster }}"
+    # This seems to be busted in the module
+    #options:
+    #  {
+    #    client.ssl: "{{ gluster_vol_ssl }}",
+    #    server.ssl: "{{ gluster_vol_ssl }}",
+    #    ssl.certificate-depth: "{{ gluster_vol_cert_depth }}",
+    #    auth.ssl-allow: "{ gluster_vol_ssl_allow }}",
+    #    ssl.cipher-list: "{{ gluster_vol_ciphers }}"
+    #  }
+  run_once: true
+  register: gluster_vol_create
+  notify: gluster_vol_restart
+- name: Build options for {{ gluster_vol_name }}
+  set_fact:
+    gluster_vol_options:
+      {
+        client.ssl: "{{ gluster_vol_ssl }}",
+        server.ssl: "{{ gluster_vol_ssl }}",
+        auth.ssl-allow: "{{ gluster_vol_ssl_allow }}",
+        ssl.certificate-depth: "{{ gluster_vol_cert_depth }}",
+        ssl.cipher-list: "{{ gluster_vol_ciphers }}",
+      }
+  when: gluster_vol_options is not defined
+- name: Set options for {{ gluster_vol_name }}
+  include_tasks: options.yml
+  loop: "{{ gluster_vol_options | dict2items }}"
+
+
+- name: Set handler volume (because Ansible sucks)
+  set_fact:
+    gluster_vol_handler: "{{ gluster_vol_name }}"
+- name: Handlers break
+  meta: flush_handlers
+
+- name: Ensure {{ gluster_vol_name }} is started
+  gluster.gluster.gluster_volume:
+    state: started
+    name: "{{ gluster_vol_name }}"
+  register: gluster_vol_started
+  run_once: true
+- name: Give it a second...
+  wait_for:
+    timeout: 1
+  when: gluster_vol_started.changed
+- name: Check for sensible status for volume {{ gluster_vol_name }}
+  command:
+    argv:
+      - gluster
+      - volume
+      - status
+      - "{{ gluster_vol_name }}"
+  register: gluster_vol_status
+  changed_when: false
+  failed_when: >-
+    gluster_vol_status.rc != 0 or
+    'is not started' in gluster_vol_status.stderr or
+    '4915' not in gluster_vol_status.stdout
+  run_once: true

+ 25 - 0
plays/roles/gluster_vol/tasks/options.yml

@@ -0,0 +1,25 @@
+- name: Check option {{ item.key }} on {{ gluster_vol_name }}
+  command:
+    argv:
+      - gluster
+      - volume
+      - get
+      - "{{ gluster_vol_name }}"
+      - "{{ item.key }}"
+  register: gluster_vol_getoption
+  changed_when: (gluster_vol_getoption.stdout_lines | last | regex_replace('^\\S*\\s+', '') | trim ) != (item.value | string)
+  run_once: true
+- name: Set option {{ item.key }} on {{ gluster_vol_name }}
+  command:
+    argv:
+      - gluster
+      - volume
+      - set
+      - "{{ gluster_vol_name }}"
+      - "{{ item.key }}"
+      - "{{ item.value }}"
+  when: gluster_vol_getoption.changed
+  changed_when: true
+  run_once: true
+  notify: gluster_vol_restart
+  

+ 9 - 0
plays/roles/hostname/defaults/main.yml

@@ -0,0 +1,9 @@
+hostname_fqdn: "{{ inventory_hostname }}"
+hostname_regex: '^127\.0\.0\.1'
+hostname_domain: "{{ domain | default('') }}.{{ route53_zone | default('') }}"
+hostname_tag_domain: "{{ hostname_domain }}"
+
+hostname_path: /etc/hosts
+hostname_owner: root
+hostname_group: root
+hostname_mode: '0644'

+ 40 - 0
plays/roles/hostname/tasks/main.yml

@@ -0,0 +1,40 @@
+- name: Add our IPv4 localhost
+  lineinfile:
+    path: "{{ hostname_path }}"
+    regexp: "{{ hostname_regex }}"
+    line: "127.0.0.1 localhost {{ hostname_fqdn }}"
+    owner: "{{ hostname_owner }}"
+    group: "{{ hostname_group }}"
+    mode: "{{ hostname_mode }}"
+
+- name: Add our IPv6 localhost
+  lineinfile:
+    path: "{{ hostname_path }}"
+    regexp: '^::1'
+    line: "::1 ip6-localhost {{ hostname_fqdn }}"
+    owner: "{{ hostname_owner }}"
+    group: "{{ hostname_group }}"
+    mode: "{{ hostname_mode }}"
+
+- name: Set the hostname
+  hostname:
+    name: "{{ hostname_fqdn }}"
+
+- name: "Find ec2 hosts #{{ hostname_tag | default('') }}"
+  include_role:
+    name: ec2_hosts
+  vars:
+    ec2_hosts_tag: "{{ hostname_tag }}"
+    ec2_hosts_cluster: "{{ hostname_cluster }}"
+  when: hostname_tag is defined and hostname_cluster is defined
+
+- name: Update /etc/hosts with cluster-local addresses
+  lineinfile:
+    path: "{{ hostname_path }}"
+    insertafter: "{{ hostname_regex }}"
+    line: "{{ item.value }} {{ item.key + '.' + hostname_tag_domain }}"
+    owner: "{{ hostname_owner }}"
+    group: "{{ hostname_group }}"
+    mode: "{{ hostname_mode }}"
+  loop: "{{ ec2_hosts_local | dict2items }}"
+  when: hostname_tag is defined and hostname_cluster is defined

+ 7 - 0
plays/roles/iptables/defaults/main.yml

@@ -0,0 +1,7 @@
+iptables_user: root
+iptables_group: root
+iptables_mode: 0400
+
+iptables_dir: "/var/tmp"
+iptables_ver: ""
+iptables_file: "{{ iptables_dir }}/firewall.ipt{{ iptables_ver }}"

+ 52 - 0
plays/roles/iptables/gluster.j2

@@ -0,0 +1,52 @@
+*filter
+:INPUT DROP [0:0]
+:FORWARD DROP [0:0]
+:OUTPUT DROP [0:0]
+
+# =============
+# Gluster
+# =============
+:gluster_in - [0:0]
+# Rate-limits
+-A gluster_in -m hashlimit --hashlimit-mode srcip --hashlimit-name gluster --hashlimit-above 1/sec --hashlimit-burst 25 -j DROP
+# AWS gluster subnets
+-A gluster_in -s 172.16.1.0/26 -j ACCEPT
+-A gluster_in -s 172.16.1.64/26 -j ACCEPT
+# Workstations
+-A gluster_in -s 172.16.2.0/26 -j ACCEPT
+# Backend
+-A gluster_in -s 172.16.4.0/26 -j ACCEPT
+-A gluster_in -s 172.16.4.64/26 -j ACCEPT
+# Logging
+#-A gluster_in -j LOG --log-prefix "gluster_in: "
+-A gluster_in -j RETURN
+
+# =============
+# INPUT
+# =============
+# Established connections
+-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+# Loopback
+-A INPUT -s 127.0.0.0/8 -d 127.0.0.0/8 -i lo -j ACCEPT
+# ICMP
+-A INPUT -p icmp -m hashlimit --hashlimit-name icmp --hashlimit-mode srcip --hashlimit-upto 2/s --hashlimit-burst 25 -j ACCEPT
+-A INPUT -p icmp -j DROP
+# SSH
+-A INPUT -p tcp -m tcp --dport 22 -m hashlimit --hashlimit-mode srcip --hashlimit-name ssh --hashlimit-upto 1/s --hashlimit-burst 5 -j ACCEPT
+-A INPUT -p tcp -m tcp --dport 22 -j DROP
+# Gluster
+-A INPUT -p tcp -m multiport --dports 111,24007,49152:49159 -j gluster_in
+-A INPUT -p udp -m multiport --dports 111 -j gluster_in
+# Logging
+#-A INPUT -j LOG --log-prefix "INPUT: "
+
+# =============
+# OUTPUT
+# =============
+# Established connections
+-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+# Loopback
+-A OUTPUT -s 127.0.0.0/8 -d 127.0.0.0/8 -o lo -j ACCEPT
+# Everything else
+-A OUTPUT -j ACCEPT
+COMMIT

+ 52 - 0
plays/roles/iptables/gluster6.j2

@@ -0,0 +1,52 @@
+*filter
+:INPUT DROP [0:0]
+:FORWARD DROP [0:0]
+:OUTPUT DROP [0:0]
+
+# =============
+# Gluster
+# =============
+:gluster_in - [0:0]
+# Rate-limits
+-A gluster_in -m hashlimit --hashlimit-mode srcip --hashlimit-name gluster --hashlimit-above 1/sec --hashlimit-burst 25 -j DROP
+# AWS gluster subnets
+-A gluster_in -s 2600:1f14:6b8:9101::/64 -j ACCEPT
+-A gluster_in -s 2600:1f14:6b8:9102::/64 -j ACCEPT
+# Workstations
+-A gluster_in -s 2600:1f14:6b8:9103::/64 -j ACCEPT
+# Backend
+-A gluster_in -s 2600:1f14:6b8:9107::/64 -j ACCEPT
+-A gluster_in -s 2600:1f14:6b8:9108::/64 -j ACCEPT
+# Logging
+#-A gluster_in -j LOG --log-prefix "gluster_in: "
+-A gluster_in -j RETURN
+
+# =============
+# INPUT
+# =============
+# Established connections
+-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+# Loopback
+-A INPUT -s ::1 -d ::1 -i lo -j ACCEPT
+# ICMP
+-A INPUT -p icmpv6 -m hashlimit --hashlimit-mode srcip --hashlimit-name icmp --hashlimit-upto 2/s --hashlimit-burst 25 -j ACCEPT
+-A INPUT -p icmpv6 -j DROP
+# SSH
+-A INPUT -p tcp -m tcp --dport 22 -m hashlimit --hashlimit-mode srcip --hashlimit-name ssh --hashlimit-upto 1/s --hashlimit-burst 5 -j ACCEPT
+-A INPUT -p tcp -m tcp --dport 22 -j DROP
+# Gluster
+-A INPUT -p tcp -m multiport --dports 111,24007,49152:49159 -j gluster_in
+-A INPUT -p udp -m multiport --dports 111 -j gluster_in
+# Logging
+#-A INPUT -j LOG --log-prefix "INPUT: "
+
+# =============
+# OUTPUT
+# =============
+# Established connections
+-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
+# Loopback
+-A OUTPUT -s ::1 -d ::1 -o lo -j ACCEPT
+# Everything else
+-A OUTPUT -j ACCEPT
+COMMIT

+ 16 - 0
plays/roles/iptables/tasks/main.yml

@@ -0,0 +1,16 @@
+- name: Upload ip{{ iptables_ver }}tables template {{ iptables_tmpl }}
+  template:
+    src: "../{{ iptables_tmpl }}.j2"
+    dest: "{{ iptables_file }}"
+    owner: "{{ iptables_user }}"
+    group: "{{ iptables_group }}"
+    mode: "{{ iptables_mode }}"
+  register: iptables_updated
+- name: Activate ip{{ iptables_ver }}tables
+  command:
+    argv:
+      - "ip{{ iptables_ver }}tables-restore"
+      - -w
+      - 2
+      - "{{ iptables_file }}"
+  when: iptables_updated.changed

+ 17 - 0
plays/roles/key_store/defaults/main.yml

@@ -0,0 +1,17 @@
+key_store_force: false
+key_store_build: false
+
+key_store_block: 1M
+
+key_store_rammnt: /var/tmp/ramfs
+key_store_ramkey: "{{ key_store_rammnt }}/key"
+key_store_keysize: 8
+
+key_store_file: /var/tmp/keys.img
+key_store_size: 100
+key_store_crypt: keys
+
+key_store_datadir: /data/keys
+key_store_owner: root
+key_store_group: keys
+key_store_gid: 2101

+ 127 - 0
plays/roles/key_store/tasks/main.yml

@@ -0,0 +1,127 @@
+- name: Determine if keys is mapped
+  stat:
+    path: "/dev/mapper/{{ key_store_crypt }}"
+  register: key_store_luks
+- name: Determine if we should wipe the key_store
+  set_fact:
+    key_store_build: true
+  when: key_store_luks.stat.exists is false or key_store_force
+
+- name: Drop the keyfile ramfs
+  ansible.posix.mount:
+    path: "{{ key_store_rammnt }}"
+    src: ramfs
+    fstype: ramfs
+    state: absent
+  when: key_store_build
+- name: Drop the local encrypted keystore
+  ansible.posix.mount:
+    path: "{{ key_store_datadir }}"
+    src: "/dev/mapper/{{ key_store_crypt }}"
+    fstype: xfs
+    state: absent
+  when: key_store_build
+- name: Drop the crypt device
+  community.crypto.luks_device:
+    state: closed
+    name: keys
+  when: key_store_build
+
+- name: Add base_user to the group "{{ key_store_group }}"
+  include_role:
+    name: add_group
+  vars:
+    add_group_groups: "{{ key_store_group }}:{{ key_store_gid }}"
+  when: key_store_build
+- name: Load the loop module
+  community.general.modprobe:
+    name: loop
+    state: present
+  when: key_store_build
+
+- name: Create a backing file
+  file:
+    path: "{{ key_store_file }}"
+    owner: "{{ key_store_owner }}"
+    group: "{{ key_store_group }}"
+    mode: 0600
+    state: touch
+  when: key_store_build
+- name: Allocate backing file
+  command:
+    argv:
+      - dd
+      - if=/dev/zero
+      - "of={{ key_store_file }}"
+      - "bs={{ key_store_block }}"
+      - "count={{ key_store_size }}"
+  when: key_store_build
+- name: Attach the loop device
+  command:
+    argv:
+      - losetup
+      - --find
+      - --show
+      - "{{ key_store_file }}"
+  register: key_store_lodev
+  when: key_store_build
+
+- name: Mount ramfs for keyfile
+  include_role:
+    name: mount
+  vars:
+    mount_path: "{{ key_store_rammnt }}"
+    mount_owner: "{{ key_store_owner }}"
+    mount_group: "{{ key_store_group }}"
+    mount_mode: '0700'
+    mount_dev: ramfs
+    mount_fs: ramfs
+  when: key_store_build
+- name: Create the keyfile
+  file:
+    path: "{{ key_store_ramkey }}"
+    owner: "{{ key_store_owner }}"
+    group: "{{ key_store_group }}"
+    mode: '0600'
+    state: touch
+  when: key_store_build
+- name: Generate a key
+  command:
+    argv:
+      - dd
+      - if=/dev/urandom
+      - "of={{ key_store_file }}"
+      - "bs={{ key_store_block }}"
+      - "count={{ key_store_keysize }}"
+      - iflag=fullblock
+  when: key_store_build
+- name: Encrypt the loop device
+  community.crypto.luks_device:
+    device: "{{ key_store_lodev.stdout }}"
+    state: opened
+    cipher: aes-xts-plain64
+    keyfile: "{{ key_store_ramkey }}"
+    name: keys
+  when: key_store_build
+- name: Drop the ramfs (and its keyfile)
+  ansible.posix.mount:
+    path: "{{ key_store_rammnt }}"
+    state: absent
+  when: key_store_build
+
+- name: Create a filesystem
+  community.general.filesystem:
+    fstype: xfs
+    dev: "/dev/mapper/{{ key_store_crypt }}"
+  when: key_store_build
+- name: Mount the encrypted keystore
+  include_role:
+    name: mount
+  vars:
+    mount_path: "{{ key_store_datadir }}"
+    mount_owner: "{{ key_store_owner }}"
+    mount_group: "{{ key_store_group }}"
+    mount_mode: '0750'
+    mount_dev: "/dev/mapper/{{ key_store_crypt }}"
+    mount_fs: xfs
+    mount_opts: "defaults,noatime"

+ 12 - 0
plays/roles/luks/defaults/main.yml

@@ -0,0 +1,12 @@
+luks_user: "{{ base_user | default('root') }}"
+luks_dir: "/home/{{ luks_user }}/bin"
+
+luks_datadir: /data
+luks_owner: root
+luks_group: data
+luks_gid: 2100
+
+#luks_dev: "{{ aws_ebs_dev }}"
+#luks_crypt_name: dmcrypt
+#luks_vg_name: lvm
+#luks_lvm_vols:

+ 47 - 0
plays/roles/luks/luks.j2

@@ -0,0 +1,47 @@
+#!/bin/sh
+
+# Templated paths
+MAPPER="/dev/mapper"
+CRYPT_NAME="{{ luks_crypt_name }}"
+CRYPT_PATH="${MAPPER}/${CRYPT_NAME}"
+VG_NAME="{{ luks_vg_name }}"
+DEV="{{ luks_dev }}"
+
+# Sanity checks
+if [ ! -b "${DEV}" ]; then
+	echo "Unable to find a suitable device" 1>&2
+	exit 2
+fi
+
+# Determine if we need to format or just open
+FORMAT=0
+if basename "${0}" | grep -qi format; then
+	FORMAT=1
+fi
+
+# Initial encryption
+if [ $FORMAT -gt 0 ]; then
+	cryptsetup --type luks2 \
+		--cipher aes-xts-plain64 \
+		--hash sha512 \
+		--key-size 512 \
+		--sector-size 4096 \
+		luksFormat "${DEV}"
+fi
+
+# Unlock
+cryptsetup luksOpen "${DEV}" "${CRYPT_NAME}"
+
+# Initial filesystem - note the big inodes for gluster
+if [ $FORMAT -gt 0 ]; then
+	pvcreate "${CRYPT_PATH}"
+	if [ $? != 0 ]; then
+		echo "Unable to create LVM PV: ${CRYPT_PATH}" 1>&2
+		exit 3
+	fi
+	vgcreate "${VG_NAME}" "${CRYPT_PATH}"
+	if [ $? != 0 ]; then
+		echo "Unable to create LVM VG: ${VG_NAME}" 1>&2
+		exit 3
+	fi
+fi

+ 34 - 0
plays/roles/luks/tasks/main.yml

@@ -0,0 +1,34 @@
+- name: Add base_user to the group "{{ luks_group }}"
+  include_role:
+    name: add_group
+  vars:
+    add_group_groups: "{{ luks_group }}:{{ luks_gid }}"
+- name: Init the data dir
+  file:
+    path: "{{ luks_datadir }}"
+    owner: "{{ luks_owner }}"
+    group: "{{ luks_group }}"
+    mode: 0750
+    state: directory
+
+- name: Init luks helper path
+  file:
+    path: "{{ luks_dir }}"
+    owner: "{{ luks_user }}"
+    group: "{{ luks_user }}"
+    mode: 0755
+    state: directory
+- name: Install luks-open
+  template:
+    src: ../luks.j2
+    dest: "{{ luks_dir }}/luks-open"
+    owner: "{{ luks_user }}"
+    group: "{{ luks_user }}"
+    mode: 0750
+- name: Link luks-format
+  file:
+    src: "{{ luks_dir }}/luks-open"
+    path: "{{ luks_dir }}/luks-format"
+    owner: "{{ luks_user }}"
+    group: "{{ luks_user }}"
+    state: link

+ 4 - 0
plays/roles/lvm/defaults/main.yml

@@ -0,0 +1,4 @@
+lvm_shrink: no
+lvm_fs: xfs
+lvm_fs_opts: "-i size=512"
+lvm_dev: "/dev/mapper/{{ lvm_vg }}-{{ lvm_lv }}"

+ 22 - 0
plays/roles/lvm/tasks/main.yml

@@ -0,0 +1,22 @@
+- name: Create {{ lvm_vg }}:{{ lvm_lv }}
+  community.general.lvol:
+    vg: "{{ lvm_vg | mandatory }}"
+    lv: "{{ lvm_lv | mandatory }}"
+    size: "{{ lvm_size | mandatory }}"
+    shrink: "{{ lvm_shrink }}"
+  register: lvm_create
+- name: Check for {{ lvm_fs }} filesystem on {{ lvm_vg }}:{{ lvm_lv }}
+  command:
+    argv:
+      - "{{ lvm_fs }}_info"
+      - "{{ lvm_dev }}"
+  register: lvm_fs_exists
+  changed_when: lvm_fs_exists.rc != 0
+  failed_when: false
+  when: lvm_create.changed
+- name: Create {{ lvm_fs }} filesystem on {{ lvm_vg }}:{{ lvm_lv }}
+  community.general.filesystem:
+    dev: "{{ lvm_dev }}"
+    fstype: "{{ lvm_fs }}"
+    opts: "{{ lvm_fs_opts }}"
+  when: lvm_fs_exists is defined and lvm_fs_exists.changed

+ 3 - 0
plays/roles/mount/defaults/main.yml

@@ -0,0 +1,3 @@
+mount_owner: root
+mount_group: root
+mount_mode: '0755'

+ 21 - 0
plays/roles/mount/tasks/main.yml

@@ -0,0 +1,21 @@
+- name: Ensure mount point exists {{ mount_path }}
+  file:
+    path: "{{ mount_path }}"
+    owner: "{{ mount_owner }}"
+    group: "{{ mount_group }}"
+    mode: "{{ mount_mode }}"
+    state: directory
+- name: Mount {{ mount_dev }} @ {{ mount_path }}
+  ansible.posix.mount:
+    path: "{{ mount_path }}"
+    src: "{{ mount_dev }}"
+    fstype: "{{ mount_fs | default(omit) }}"
+    opts: "{{ mount_opts | default(omit) }}"
+    state: mounted
+- name: Secure mounted path {{ mount_path }}
+  file:
+    path: "{{ mount_path }}"
+    owner: "{{ mount_owner }}"
+    group: "{{ mount_group }}"
+    mode: "{{ mount_mode }}"
+    state: directory

+ 6 - 0
plays/roles/route53/defaults/main.yml

@@ -0,0 +1,6 @@
+route53_overwrite: true
+route53_state: present
+route53_ttl: 300
+route53_type: A
+route53_wait: true
+route53_zone: aws.local

+ 14 - 0
plays/roles/route53/tasks/main.yml

@@ -0,0 +1,14 @@
+- name: "Update {{ route53_record }}.{{ route53_zone }}"
+  community.aws.route53:
+    overwrite: "{{ route53_overwrite }}"
+    record: "{{ route53_record }}.{{ route53_zone }}"
+    state: "{{ route53_state }}"
+    ttl: "{{ route53_ttl }}"
+    type: "{{ route53_type }}"
+    value: "{{ route53_value }}"
+    wait: "{{ route53_wait }}"
+    zone: "{{ route53_zone }}"
+  delegate_to: localhost
+  run_once: true
+  become: false
+  register: route53_res

+ 6 - 0
plays/roles/sshd_config/defaults/main.yml

@@ -0,0 +1,6 @@
+sshd_config_dest: /etc/ssh/sshd_config.d/overrides.conf
+sshd_config_owner: root
+sshd_config_group: root
+
+# This fails with a (false positive) PrivSep error on modern sshd
+#sshd_config_validate: '/usr/sbin/sshd -T -f %s'

+ 2 - 0
plays/roles/sshd_config/meta/main.yml

@@ -0,0 +1,2 @@
+dependencies:
+- role: common

+ 21 - 0
plays/roles/sshd_config/sshd.j2

@@ -0,0 +1,21 @@
+# Override Debian
+#ChallengeResponseAuthentication no
+X11Forwarding no
+#PrintMotd no
+#AcceptEnv LANG LC_*
+#Subsystem sftp /usr/lib/openssh/sftp-server # Overriden below
+#UsePAM yes
+
+# Prefered hostkey order
+HostKey /etc/ssh/ssh_host_ed25519_key
+#HostKey /etc/ssh/ssh_host_rsa_key
+#HostKey /etc/ssh/ssh_host_ecdsa_key
+
+# Restricted KEX/MAC/Ciphers
+KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
+Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr
+MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
+
+# Keys only, no root
+AuthenticationMethods publickey
+PermitRootLogin No

+ 13 - 0
plays/roles/sshd_config/tasks/main.yml

@@ -0,0 +1,13 @@
+- name: Add some sshd_config overrides
+  template:
+    src: ../sshd.j2
+    dest: "{{ sshd_config_dest }}"
+    owner: "{{ sshd_config_owner }}"
+    group: "{{ sshd_config_group }}"
+    mode: 0644
+    validate: "{{ sshd_config_validate | default(omit) }}"
+  notify:
+    - sshd_restart
+    - known_hosts
+- name: Run handlers
+  meta: flush_handlers

+ 1 - 0
plays/roles/timezone/defaults/main.yml

@@ -0,0 +1 @@
+timezone: America/Los_Angeles

+ 3 - 0
plays/roles/timezone/tasks/main.yml

@@ -0,0 +1,3 @@
+- name: Set timezone
+  community.general.timezone:
+    name: "{{ timezone }}"

+ 19 - 0
plays/roles/updates/tasks/main.yml

@@ -0,0 +1,19 @@
+- name: Repo updates
+  apt:
+    update_cache: yes
+  changed_when: false
+- name: OS updates
+  apt:
+    upgrade: safe
+  register: updates_os_change
+- name: Remember to reboot if the OS says so
+  stat:
+    path: /var/run/reboot-required
+  register: updates_os_flag
+- name: Remember our OS update status
+  set_fact:
+    reboot_force: "{{ updates_os_change.changed or updates_os_flag.stat.exists }}"
+  when: reboot_force is not defined or updates_os_change.changed or updates_os_flag.stat.exists
+- name: Pacakge cleanup
+  apt:
+    autoremove: yes

+ 15 - 0
tls/.gitignore

@@ -0,0 +1,15 @@
+ca.pem
+*/ca.pem
+*/ca-chain.pem
+
+keys/*
+*/keys/*
+certs/*
+*/certs/*
+
+db/serial*
+*/db/serial*
+db/index.txt*
+*/db/index.txt*
+db/crl/number*
+*/db/crl/number*

+ 20 - 0
tls/bin/caSign.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Support intermediate CAs
+PREFIX=""
+if [ -n "${1}" ]; then
+        if [ ! -d "${1}" ]; then
+                echo "Intermediate CA directory does not exist: ${1}" 1>&2
+                exit 1
+        fi
+        echo "Using intermediate CA: ${1}"
+        PREFIX="${1}/"
+fi
+
+
+export CA_DIR="."
+
+# Sign
+openssl ca -config "${CONF}" -policy "${INTER_POLICY}" -extensions "${INTER_CA_EXT}" \
+	-days "${DAYS}" -notext \
+	-in "${CERT}.csr" -out "${CERT}"

+ 228 - 0
tls/bin/genCA.sh

@@ -0,0 +1,228 @@
+#!/bin/bash
+
+# Config
+CA_NAME="ca"
+DAYS=1825
+ROOT_POLICY="policy_root"
+ROOT_EXT="v3_ca"
+INTER_POLICY="policy_inter"
+INTER_EXT="v3_intermediate_ca"
+REQ_POLICY="policy_req"
+REQ_EXT="v3_req"
+
+# Support intermediate CAs
+INTER="${1}"
+if [ -n "${INTER}" ] && [ "${INTER}" != "@" ]; then
+	if [ ! -d "${INTER}" ]; then
+		echo "Intermediate CA directory does not exist: ${INTER}" 1>&2
+		exit 1
+	fi
+	PREFIX="${INTER}/"
+else
+	INTER="@"
+	PREFIX=""
+fi
+
+# Calculate all our file paths
+CONF="openssl.conf"
+CA_CERT="${PREFIX}${CA_NAME}.pem"
+CERTS="${PREFIX}certs"
+KEYS="${PREFIX}keys"
+DB_DIR="${PREFIX}db"
+CRL="${DB_DIR}/crl/number"
+INDEX="${DB_DIR}/index.txt"
+SERIAL="${DB_DIR}/serial"
+
+# Export the CA directory for OpenSSL config path substitution
+export CA_DIR="${INTER}"
+
+# Decide what we're doing
+GEN_CA=1
+GEN_CERT=0
+if basename "${0}" | grep -q -i genCert; then
+	GEN_CA=0
+	GEN_CERT=1
+fi
+
+# If we're generating a normal certificate we need a name
+if [ $GEN_CERT -gt 0 ]; then
+	CN="${2}"
+	if [ -z "${CN}" ]; then
+		echo "Certificate name not specified" 1>&2
+		echo "Usage: ${0} INTERMEDIATE CERT" 1>&2
+		exit 1
+	fi
+	CERT="${CERTS}/${CN}.pem"
+	KEY="${KEYS}/${CN}.key"
+fi
+
+# If we're generating a CA we know the name
+if [ $GEN_CA -gt 0 ]; then
+	CERT="${CA_CERT}"
+	KEY="${KEYS}/${CA_NAME}.key"
+	# If we're signing with the root override the CA_CERT path
+	CA_CERT="${CA_NAME}.pem"
+fi
+
+# Remind admins what is happening
+if [ $GEN_CA -gt 0 ]; then
+	echo
+	echo "Generating new CA at ${PWD}/${PREFIX}"
+	echo
+fi
+if [ $GEN_CERT -gt 0 ]; then
+	echo
+	echo "Generating certificate for ${CN} with ${INTER}"
+	echo
+fi
+
+# Ensure this operation will be safe
+if [ ! -r "${CONF}" ]; then
+	echo "Expected OpenSSL config: ${CONF}" 1>&2
+	exit 1
+fi
+if [ -e "${CERT}" ]; then
+	echo "Output certificate exists: ${CERT}" 1>&2
+	exit 1
+fi
+if [ -e "${KEY}" ]; then
+	echo "Output key exists: ${KEY}" 1>&2
+	exit 1
+fi
+if [ $GEN_CA -gt 0 ]; then
+	if [ -e "${SERIAL}" ] && [ "`cat "${SERIAL}"`" != "01" ]; then
+		echo "Serial file exists and is not 0x01: ${SERIAL}" 1>&2
+		exit 1
+	fi
+	if [ -e "${CRL}" ] && [ "`cat "${CRL}"`" != "01" ]; then
+		echo "CRL number file exists and is not 0x01: ${CRL}" 1>&2
+		exit 1
+	fi
+	if [ -e "${INDEX}" ] && [ -s "${INDEX}" ]; then
+		echo "Index file exists and is not empty: ${INDEX}" 1>&2
+		exit 1
+	fi
+fi
+
+function makeDirExist {
+	if [ -d "${1}" ] || [ "${1}" == "." ]; then
+		return
+	fi
+	mkdir "${1}"
+	if [ -n "${2}" ]; then
+		chmod "${2}" "${1}"
+	fi
+	if [ -n "${3}" ]; then
+		chown "${3}" "${1}"
+	fi	
+}
+
+# Ensure the CA paths are available and valid
+if [ $GEN_CA -gt 0 ]; then
+	makeDirExist "${CERTS}" '0755'
+	makeDirExist "${KEYS}" '0700'
+
+	# Setup the DB files
+	makeDirExist "${DB_DIR}" '0755'
+	echo "01" > "${SERIAL}"
+	touch "${INDEX}"
+
+	# And CRL for good measure
+	makeDirExist "`dirname "${CRL}"`" '0755'
+	echo "01" > "${CRL}"
+fi
+
+# Generate a new, encrypted key (user is prompted for password entry)
+openssl ecparam -name secp521r1 -genkey | openssl ec -aes256 -out "${KEY}"
+if [ ! -s "${KEY}" ]; then
+	echo "Unable to generate private key" 1>&2
+	exit 3
+fi
+
+# Generate and sign the certificate
+if [ -r "${CA_CERT}" ]; then
+	CSR="${CERT}.csr"
+	if [ -e "${CSR}" ]; then
+		"CSR exists: ${CSR}" 1>&2
+		exit 3
+	fi
+
+	# Generate a CSR
+	openssl req -new \
+		-config "${CONF}" \
+		-key "${KEY}" \
+		-out "${CSR}"
+	if [ ! -r "${CSR}" ]; then
+		echo "Unable to generate CSR"
+		exit 3
+	fi
+
+	# Remind admins what is happening
+	echo
+	echo "Signing ${CSR} with ${CA_CERT}"
+	echo -en "Request:\n\t"
+	openssl req -in "${CSR}" -text | grep 'Subject:'
+	echo -en "CA:\n\t"
+	openssl x509 -in "${CA_CERT}" -text  | grep 'Subject:'
+	echo
+
+	# Sign with a CA
+	DIR="${CA_DIR}"
+	EXT="${REQ_EXT}"
+	POLICY="${REQ_POLICY}"
+	if [ "${INTER}" == "@" ] || [ $GEN_CA -gt 0 ]; then
+		DIR="."
+		EXT="${INTER_EXT}"
+		POLICY="${INTER_POLICY}"
+	fi
+	CA_DIR="${DIR}" openssl ca -notext \
+		-config "${CONF}" \
+		-policy "${POLICY}" \
+		-extensions "${EXT}" \
+		-days "${DAYS}" \
+		-in "${CERT}.csr" \
+		-out "${CERT}"
+
+	# Generate a chain certificate for validation
+	if [ $GEN_CA -gt 0 ] && [ -r "${CERT}" ]; then
+		cat "${CERT}" "${CA_CERT}" > "${INTER}/ca-chain.pem"
+	fi
+
+	# Drop the CSR
+	rm -f "${CERT}.csr"
+
+	# Generate keys ansible can decrypt
+	if [ "${INTER}" != "@" ] && [ $GEN_CA -le 0 ] && [ -s "${CERT}" ]; then
+		echo
+		echo "Encrypting ${KEY} for Ansible"
+		echo
+		ANSIBLE_KEY="`echo "${KEY}" | sed 's%.key$%.ansible%'`"
+		openssl ec -in "${KEY}" -out "${ANSIBLE_KEY}"
+		ansible-vault encrypt --encrypt-vault-id "${INTER}" "${ANSIBLE_KEY}"
+		if ! head -n 1 "${ANSIBLE_KEY}" | grep -q "^\$ANSIBLE_VAULT"; then
+			echo "Unable to generate ansible-encrypted key" 1>&2
+			rm -f "${ANSIBLE_KEY}"
+		fi
+	fi
+else
+	# Remind admins what is happening
+	echo
+	echo "Self-signing new root CA"
+	echo
+
+	# Root CA - self-sign
+	openssl req -new -x509 \
+		-config "${CONF}" \
+		-extensions "${ROOT_EXT}" \
+		-days "${DAYS}" \
+		-key "${KEY}" \
+		-out "${CERT}" 
+fi
+if [ ! -s "${CERT}" ]; then
+	echo "Unable to generate certificate" 1>&2
+	rm -f "${KEY}"
+	if [ -n "${ANSIBLE_KEY}" ]; then
+		rm -f "${ANSIBLE_KEY}"
+	fi
+	exit 3
+fi

+ 1 - 0
tls/bin/genCert.sh

@@ -0,0 +1 @@
+genCA.sh

+ 91 - 0
tls/openssl.conf

@@ -0,0 +1,91 @@
+config_diagnostics = 1
+
+####################################################################
+[ ca ]
+default_ca	= Zi3_CA
+[ Zi3_CA ]
+dir		= $ENV::CA_DIR		# Where everything is kept
+new_certs_dir	= $dir/certs		# default place for new certs.
+certs		= $dir/certs		# Where the issued certs are kept
+crl_dir		= $dir/db/crl		# Where the issued crl are kept
+crlnumber	= $crl_dir/number	# the current crl number
+crl		= $dir/db/crl/crl.pem	# The current CRL
+database	= $dir/db/index.txt	# database index file.
+serial		= $dir/db/serial 	# The current serial number
+certificate	= $dir/ca.pem	 	# The CA certificate
+private_key	= $dir/keys/ca.key	# The private key
+name_opt 	= ca_default		# Subject Name options
+cert_opt 	= ca_default		# Certificate field options
+default_days	= 1825			# how long to certify for
+default_crl_days= 30			# how long before next CRL
+default_md	= default		# use public key default MD
+preserve	= no			# keep passed DN ordering
+x509_extensions	= usr_cert		# The extensions to add to the cert
+crl_extensions	= crl_ext		# The extensiont to add to CRL certs
+policy		= policy_root		# CA must match everything, must supply email
+
+[ policy_root ]
+organizationName	= match
+organizationalUnitName	= match
+commonName		= supplied
+emailAddress		= supplied
+
+[ policy_inter ]
+organizationName	= match
+organizationalUnitName	= supplied
+commonName		= supplied
+emailAddress		= optional
+
+[ policy_req ]
+organizationName	= match
+organizationalUnitName	= match
+commonName		= supplied
+emailAddress		= optional
+
+[ v3_ca ]
+subjectKeyIdentifier	= hash
+authorityKeyIdentifier	= keyid:always, issuer
+basicConstraints	= critical, CA:true
+keyUsage		= cRLSign, keyCertSign
+subjectAltName		= email:copy
+
+[ v3_intermediate_ca ]
+subjectKeyIdentifier	= hash
+authorityKeyIdentifier	= keyid:always,issuer
+basicConstraints	= critical, CA:true, pathlen:0
+keyUsage		= critical, digitalSignature, cRLSign, keyCertSign
+subjectAltName		= email:copy
+
+[ v3_req ]
+basicConstraints	= CA:FALSE
+keyUsage 		= nonRepudiation, digitalSignature, keyEncipherment
+
+[ crl_ext ]
+authorityKeyIdentifier	= keyid:always
+
+[ usr_cert ]
+basicConstraints	= CA:FALSE
+#extendedKeyUsage	= critical, timeStamping
+subjectKeyIdentifier	= hash
+authorityKeyIdentifier	= keyid:always,issuer
+issuerAltName		= issuer:copy
+subjectAltName	 	= email:move
+
+####################################################################
+[ req ]
+default_bits		= 4096
+default_keyfile 	= key.pem
+distinguished_name	= req_distinguished_name
+#attributes		= req_attributes
+x509_extensions		= v3_ca
+string_mask		= utf8only
+
+[ req_distinguished_name ]
+0.organizationName		= Organization
+0.organizationName_default	= Zi3
+organizationalUnitName		= Service
+organizationalUnitName_default	= Testing
+commonName			= Common Name (e.g. server FQDN or YOUR name)
+commonName_max			= 64
+emailAddress			= Email Address
+emailAddress_max		= 64