# 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: - name: Ensure all authorized keys are copied to root account remote_user: root ansible.posix.authorized_key: key: | {% for key in authorized_key_files %} {{ lookup('file', key) }} {% endfor %} user: root exclusive: true # 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 # This is needed by ansible.builtin.deb822_repository to interact with the # sources lists. In ansible-core 2.20 and above, there is a builtin # install_python_debian parameter that handles that without this extra step, # but we are currently running 2.19. # TODO(debian-forky) use deb822_repository's install_python_debian parameter - name: Ensure python3-debian is installed ansible.builtin.apt: name: - python3-debian state: present update_cache: true become: true - name: Ensure debian apt repositories are configured ansible.builtin.deb822_repository: name: debian uris: "{{ debian_uri }}" suites: - "{{ debian_version }}" - "{{ debian_version }}-updates" components: main signed_by: /usr/share/keyrings/debian-archive-keyring.gpg become: true - name: Ensure debian-security apt repositories are configured ansible.builtin.deb822_repository: name: debian-security uris: "{{ debian_security_uri }}" suites: "{{ debian_version }}-security" components: main signed_by: /usr/share/keyrings/debian-archive-keyring.gpg become: true # 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: - libdbi-perl # Used by munin postgres plugins - lua-dbi-postgresql # Prosody postgres connection - lua-unbound # Prosody DNS resolution - munin-node # Graphs and monitoring - postgresql # Database - prosody # XMPP server - prosody-modules # Extra addons - python3-psycopg2 # Used by ansible postgres role - rsync # Backups - 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: - 4949 # Munin - 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 munin is configured ansible.builtin.template: src: "{{ playbook_dir }}/files/munin-node.conf.j2" dest: /etc/munin/munin-node.conf owner: root group: root mode: "0644" become: true notify: Restart munin-node - name: Ensure munin plugins are configured ansible.builtin.file: src: "/usr/share/munin/plugins/{{ item.src }}" dest: "/etc/munin/plugins/{{ item.dest }}" owner: root group: root state: link become: true with_items: - src: apt_all dest: apt_all - src: cpu dest: cpu - src: df dest: df - src: df_abs dest: df_abs - src: df_inode dest: df_inode - src: diskstats dest: diskstats - src: entropy dest: entropy - src: forks dest: forks - src: fw_conntrack dest: fw_conntrack - src: fw_forwarded_local dest: fw_forwarded_local - src: fw_packets dest: fw_packets - src: if_ dest: if_enp1s0 - src: if_err_ dest: if_err_enp1s0 - src: irqstats dest: irqstats - src: load dest: load - src: memory dest: memory - src: netstat dest: netstat - src: open_files dest: open_files - src: open_inodes dest: open_inodes - src: postgres_autovacuum dest: postgres_autovacuum - src: postgres_bgwriter dest: postgres_bgwriter - src: postgres_cache_ dest: postgres_cache_prosody - src: postgres_checkpoints dest: postgres_checkpoints - src: postgres_connections_ dest: postgres_connections_prosody - src: postgres_connections_db dest: postgres_connections_db - src: postgres_locks_ dest: postgres_locks_prosody - src: postgres_oldest_prepared_xact_ dest: postgres_oldest_prepared_xact_prosody - src: postgres_prepared_xacts_ dest: postgres_prepared_xacts_prosody - src: postgres_querylength_ dest: postgres_querylength_prosody - src: postgres_scans_ dest: postgres_scans_prosody - src: postgres_size_ dest: postgres_size_prosody - src: postgres_streaming_ dest: postgres_streaming_prosody - src: postgres_transactions_ dest: postgres_transactions_prosody - src: postgres_tuples_ dest: postgres_tuples_prosody - src: postgres_users dest: postgres_users - src: postgres_xlog dest: postgres_xlog - src: proc_pri dest: proc_pri - src: processes dest: processes - src: swap dest: swap - src: threads dest: threads - src: uptime dest: uptime - src: users dest: users - src: vmstat dest: vmstat notify: Restart munin-node - 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 created 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 - name: Restart munin-node ansible.builtin.service: name: munin-node 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 }}"