# SPDX-FileCopyrightText: 2024 Matthew Fennell # # SPDX-License-Identifier: AGPL-3.0-or-later --- - name: Ensure XMPP server is set up gather_facts: false hosts: all tasks: # Now, we create a non-root user with sudo privileges - name: Ensure wheel group exists remote_user: root ansible.builtin.group: name: wheel state: present - name: Ensure wheel group allows passwordless sudo remote_user: root ansible.builtin.lineinfile: dest: /etc/sudoers state: present regexp: "^%wheel" line: "%wheel ALL=(ALL) NOPASSWD: ALL" validate: visudo -cf %s - name: Ensure non-root admin account is created in wheel group remote_user: root ansible.builtin.user: name: admin groups: wheel append: true - name: Ensure admin ssh directory exists remote_user: root ansible.builtin.file: path: /home/admin/.ssh state: directory owner: admin group: admin mode: "0700" - name: Copy authorised keys to admin account remote_user: root ansible.builtin.copy: src: /root/.ssh/authorized_keys dest: /home/admin/.ssh/authorized_keys remote_src: true owner: admin group: admin mode: preserve - name: Ensure cloud-init is disabled ansible.builtin.copy: content: "" dest: /etc/cloud/cloud-init.disabled force: false owner: root group: root mode: "0644" become: true - name: Ensure hostname is set ansible.builtin.hostname: name: "{{ hostname }}" become: true - name: Ensure hostname is configured in /etc/hosts ansible.builtin.template: src: "{{ playbook_dir }}/files/hosts.j2" dest: /etc/hosts owner: root group: root mode: "0644" become: true - name: Retrieve DANE hash ansible.builtin.shell: cmd: > set -o pipefail && openssl x509 -in ~/.lego/certificates/{{ virtual_host }}.crt -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | hexdump -ve '/1 "%02x"' register: dane_result changed_when: false delegate_to: localhost - name: Take note of DANE hash ansible.builtin.set_fact: dane_hash: "{{ dane_result.stdout }}" tags: - dns - name: Ensure common records exist ansible.builtin.uri: url: "https://api.mythic-beasts.com/dns/v2/zones/{{ domain }}/records\ ?select=host%3Dchat{{ env_suffix }}%26type%3DA\ &select=host%3Dchat{{ env_suffix }}%26type%3DAAAA\ &select=host%3Dconference{{ env_suffix }}%26type%3DCNAME\ &select=host%3Dupload{{ env_suffix }}%26type%3DCNAME\ &select=host%3D_xmpp-client._tcp{{ env_suffix }}%26type%3DSRV\ &select=host%3D_xmpps-client._tcp{{ env_suffix }}%26type%3DSRV\ &select=host%3D_5222._tcp.chat{{ env_suffix }}%26type%3DTLSA\ %26data%3D{{ dane_hash }}\ &select=host%3D_5223._tcp.chat{{ env_suffix }}%26type%3DTLSA\ %26data%3D{{ dane_hash }}" method: PUT body_format: json body: records: - host: "chat{{ env_suffix }}" type: "A" data: "{{ ipv4 }}" - host: "chat{{ env_suffix }}" type: "AAAA" data: "{{ ipv6 }}" - host: "conference{{ env_suffix }}" type: "CNAME" data: "chat{{ env_suffix }}.{{ domain }}." - host: "upload{{ env_suffix }}" type: "CNAME" data: "chat{{ env_suffix }}.{{ domain }}." - host: "_xmpp-client._tcp{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: 0 srv_weight: 5 srv_port: 5222 - host: "_xmpps-client._tcp{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: "0" srv_weight: "5" srv_port: "5223" - host: "_5222._tcp.chat{{ env_suffix }}" type: "TLSA" data: "{{ dane_hash }}" tlsa_usage: "3" tlsa_selector: "1" tlsa_matching: "1" - host: "_5223._tcp.chat{{ env_suffix }}" type: "TLSA" data: "{{ dane_hash }}" tlsa_usage: "3" tlsa_selector: "1" tlsa_matching: "1" delegate_to: localhost tags: - dns - name: Ensure non-transport records exist ansible.builtin.uri: url: "https://api.mythic-beasts.com/dns/v2/zones/{{ domain }}/records\ ?select=host%3D_xmpp-server._tcp{{ env_suffix }}%26type%3DSRV\ &select=host%3D_xmpps-server._tcp{{ env_suffix }}%26type%3DSRV\ &select=host%3D_xmpps-server._tcp.conference{{ env_suffix }}\ %26type%3DSRV\ &select=host%3D_xmpps-server._tcp.upload{{ env_suffix }}%26type%3DSRV\ &select=host%3D_5269._tcp.chat{{ env_suffix }}%26type%3DTLSA\ %26data%3D{{ dane_hash }}\ &select=host%3D_5270._tcp.chat{{ env_suffix }}%26type%3DTLSA\ %26data%3D{{ dane_hash }}" method: PUT body_format: json body: records: - host: "_xmpp-server._tcp{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: "0" srv_weight: "5" srv_port: "5269" - host: "_xmpps-server._tcp{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: "0" srv_weight: "5" srv_port: "5270" - host: "_xmpps-server._tcp.conference{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: "0" srv_weight: "5" srv_port: "5270" - host: "_xmpps-server._tcp.upload{{ env_suffix }}" type: "SRV" data: "chat{{ env_suffix }}.{{ domain }}." srv_priority: "0" srv_weight: "5" srv_port: "5270" - host: "_5269._tcp.chat{{ env_suffix }}" type: "TLSA" data: "{{ dane_hash }}" tlsa_usage: "3" tlsa_selector: "1" tlsa_matching: "1" - host: "_5270._tcp.chat{{ env_suffix }}" type: "TLSA" data: "{{ dane_hash }}" tlsa_usage: "3" tlsa_selector: "1" tlsa_matching: "1" delegate_to: localhost when: not is_transport_server tags: - dns # We specifically use apt instead of the more general package module here, # because we want to ensure the cache is updated before we try and install # anything. This is needed because, on a freh Debian install on AWS # Lightsail (as of 2024-02-08), nothing was returned after running apt # search borgmatic. Updating the cache before running apt install solved # this issue, but the package module does not support this functionality. - name: Ensure required packages are installed ansible.builtin.apt: name: - lua-dbi-postgresql # Prosody postgres connection - lua-unbound # Prosody DNS resolution - postgresql # Database - prosody # XMPP server - prosody-modules # Extra addons - python3-psycopg2 # Used by ansible postgres role - systemd-timesyncd # Used to make sure the date is correct - ufw # Firewall - unattended-upgrades # Not every hosting provider installs by default state: present update_cache: true become: true - name: Ensure invite-specific packages are installed ansible.builtin.apt: name: - libjs-bootstrap4 # Used by invite webpage - libjs-jquery # Used by invite webpage - nginx # Serve invite webpages state: present update_cache: true become: true when: not is_transport_server - name: Ensure turn-specific packages are installed ansible.builtin.apt: name: - coturn # Audio / video calling server state: present update_cache: true become: true when: not is_transport_server - name: Ensure required ports with ufw applications are open community.general.ufw: rule: allow name: "{{ item }}" state: enabled loop: - OpenSSH become: true - name: Ensure full XMPP ports are open community.general.ufw: rule: allow name: "{{ item }}" state: enabled loop: - XMPP become: true when: not is_transport_server - name: Ensure only c2s ports are open community.general.ufw: rule: allow port: "{{ item }}" proto: tcp state: enabled loop: - 5222 become: true when: is_transport_server - name: Ensure invite-specific ports with ufw applications are open community.general.ufw: rule: allow name: "{{ item }}" state: enabled loop: - WWW Full become: true when: not is_transport_server - name: Ensure turn-specific ports with ufw applications are open community.general.ufw: rule: allow name: "{{ item }}" state: enabled loop: - Turnserver become: true when: not is_transport_server - name: Ensure other required tcp ports are open community.general.ufw: rule: allow port: "{{ item }}" proto: tcp state: enabled loop: - 5000 # XEP-0065 - 5223 # XEP-0368 - 5270 # XEP-0368 - 5280 # XEP-0363 - 5281 # XEP-0363 # - 5432 # Postgres become: true - name: Ensure other udp ports are open community.general.ufw: rule: allow port: "{{ item }}" proto: udp state: enabled loop: - 5000 # XEP-0065 - 5280 # XEP-0363 - 5281 # XEP-0363 become: true - name: Ensure default nginx config is removed ansible.builtin.file: path: "/etc/nginx/sites-enabled/default" state: absent become: true notify: Restart nginx when: not is_transport_server - name: Ensure nginx config is installed ansible.builtin.template: src: "{{ playbook_dir }}/files/nginx_conf.j2" dest: /etc/nginx/sites-available/{{ virtual_host }} owner: root group: root mode: "0644" become: true notify: Restart nginx when: not is_transport_server - name: Ensure nginx config is enabled ansible.builtin.file: src: /etc/nginx/sites-available/{{ virtual_host }} dest: /etc/nginx/sites-enabled/{{ virtual_host }} owner: root group: root state: link become: true notify: Restart nginx when: not is_transport_server - name: Ensure turn is configured ansible.builtin.template: src: "{{ playbook_dir }}/files/turnserver.conf.j2" dest: /etc/turnserver.conf owner: root group: prosody mode: "0640" become: true notify: Restart coturn when: not is_transport_server - name: Ensure prosody database is set up community.postgresql.postgresql_db: name: prosody become: true become_user: postgres - name: Ensure prosody role is created community.postgresql.postgresql_user: login_db: prosody name: prosody become: true become_user: postgres - name: Ensure prosody schema is created community.postgresql.postgresql_schema: login_db: prosody name: prosody owner: prosody become: true become_user: postgres register: my_result - name: Ensure prosody user exists on database community.postgresql.postgresql_user: name: prosody become: true become_user: postgres - name: Ensure prosody user has permissions on database community.postgresql.postgresql_privs: type: database login_db: prosody privs: ALL roles: prosody become: true become_user: postgres - name: Ensure prosody user has permissions on schema community.postgresql.postgresql_privs: type: table login_db: prosody objs: ALL_IN_SCHEMA privs: ALL roles: prosody become: true become_user: postgres - name: Ensure top-level prosody configuration is installed ansible.builtin.template: src: "{{ playbook_dir }}/files/prosody.cfg.lua.j2" dest: /etc/prosody/prosody.cfg.lua owner: root group: prosody mode: "0640" become: true notify: Restart prosody - name: Ensure host-specific prosody configuration is available ansible.builtin.template: src: "{{ playbook_dir }}/files/virtual_host.cfg.lua.j2" dest: "/etc/prosody/conf.avail/{{ virtual_host }}.cfg.lua" owner: root group: prosody mode: "0644" become: true notify: Restart prosody - name: Ensure host-specific prosody configuration is set ansible.builtin.file: src: "/etc/prosody/conf.avail/{{ virtual_host }}.cfg.lua" dest: "/etc/prosody/conf.d/{{ virtual_host }}.cfg.lua" owner: root group: prosody state: link become: true notify: Restart prosody - name: Ensure localhost prosody configuration is removed ansible.builtin.file: path: "/etc/prosody/conf.d/localhost.cfg.lua" state: absent become: true notify: Restart prosody - name: Ensure localhost prosody configuration is not available ansible.builtin.file: path: "/etc/prosody/conf.avail/localhost.cfg.lua" state: absent become: true - name: Ensure example prosody configuration is not available ansible.builtin.file: path: "/etc/prosody/conf.avail/example.com.cfg.lua" state: absent become: true - name: Ensure prosody is enabled ansible.builtin.service: name: prosody enabled: true become: true # Vultr adds a custom sshd_config file that enabled password authentication. # I don't want this to be enabled, since I'm already copying the public key. - name: Ensure password authentication is not explicitly enabled ansible.builtin.file: path: "/etc/ssh/sshd_config.d/50-cloud-init.conf" state: absent become: true notify: Restart sshd - name: Ensure password based authentication is disabled ansible.builtin.copy: src: "{{ playbook_dir }}/files/50-disable-password-auth.conf" dest: "/etc/ssh/sshd_config.d/50-disable-password-auth.conf" owner: root group: root mode: "0644" become: true notify: Restart sshd - name: Ensure unattended upgrades config is installed ansible.builtin.copy: src: "{{ playbook_dir }}/files/50unattended-upgrades" dest: "/etc/apt/apt.conf.d/50unattended-upgrades" owner: root group: root mode: "0644" become: true handlers: - name: Restart prosody ansible.builtin.service: name: prosody state: restarted become: true - name: Restart coturn ansible.builtin.service: name: coturn state: restarted become: true - name: Restart sshd ansible.builtin.service: name: sshd state: restarted become: true - name: Restart nginx ansible.builtin.service: name: nginx state: restarted become: true vars: env_prefix: >- {{ "" if env == "" else env + "." }} env_suffix: >- {{ "" if env == "" else "." + env }} virtual_host: "{{ env_prefix }}{{ domain }}" turn_server: "chat.{{ env_prefix }}{{ turn_domain }}" is_transport_server: "{{ transports | default([]) | length > 0 }}"