diff --git a/.ansible/.lock b/.ansible/.lock new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index d747275..eaed5dc 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,65 @@ # Free I.T. Athen's Infrastructure + This project is used to develop Ansible for deploying and maintaining websites and services operated by Free I.T. Athens (FRITA). - Requires GNU Make, Ansible, and Vagrant on the host ## Quick Start + 1. Clone this project -2. Run `make` to provision a Debian 11 base box +2. Run `make` to provision a Rocky 9 base box 3. Go to - - [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/) - - [WordPress](https://www.local.freeitathens.org) - - [Nextcloud](https://cloud.local.freeitathens.org) + - [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/) + - [WordPress](https://www.local.freeitathens.org) + - [Nextcloud](https://cloud.local.freeitathens.org) 4. Click through the HTTPS security warning ## Production + 1. Clone [production-env](https://github.com/freeitathens/production-env/) to `./environments` - ``` - mkdir -p environments - git clone git@github.com:freeitathens/production-env.git ./environments - ``` + ``` + mkdir -p environments + git clone git@github.com:freeitathens/production-env.git ./environments + ``` 2. Run `./scripts/vault-key.sh` from the root of the project to obtain the Ansible Vault password 3. Enter the Bitwarden Master Password 4. Run `ansible-playbook` against the production servers, e.g., - ``` - ansible-playbook -u root -i environments/production --vault-pass-file ./.ansible_vault webserver.yml --diff --check - ``` + ``` + ansible-playbook -u root -i environments/production --vault-pass-file ./.ansible_vault webserver.yml --diff --check + ``` 5. Delete the `.ansible_vault` file when you are done ### Using Ansible Vault to add or rotate values + Do not submit ciphertext into Ansible Vault with the indention formatting.
To submit, press `CTRL+d` twice. - Decrypt Ansible Vault values - ``` - ansible-vault decrypt --vault-pass-file .ansible_vault - ``` + ``` + ansible-vault decrypt --vault-pass-file .ansible_vault + ``` - Encrypt new Ansible Vault values - ``` - ansible-vault encrypt --vault-pass-file .ansible_vault - ``` - - e.g., `pwgen -s 100 1 | ansible-vault encrypt --vault-pass-file .ansible_vault` + ``` + ansible-vault encrypt --vault-pass-file .ansible_vault + ``` + + - e.g., `pwgen -s 100 1 | ansible-vault encrypt --vault-pass-file .ansible_vault` ## Authors -* **Kris Lamoureux** - *Project Founder* - [@krislamo](https://github.com/krislamo) + +- **Kris Lamoureux** - _Project Founder_ - [@krislamo](https://github.com/krislamo) ## Copyrights and Licenses -Copyright (C) 2019, 2020, 2022 Free I.T. Athens + +Copyright (C) 2019, 2020, 2022, 2023, 2025 Free I.T. Athens This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -60,7 +67,7 @@ Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/Vagrantfile b/Vagrantfile index 51925a0..4857a41 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -14,9 +14,8 @@ else File.write(".playbook", PLAYBOOK) end -# Debian 11 Vagrant.configure("2") do |config| - config.vm.box = "debian/bullseye64" + config.vm.box = "rockylinux/9" config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.network "private_network", type: "dhcp" @@ -29,6 +28,8 @@ Vagrant.configure("2") do |config| libvirt.cpus = 2 libvirt.memory = 4096 libvirt.default_prefix = "" + # Doesn't boot without this on rockylinux/9 + libvirt.machine_virtual_size = 10 end # Set VirtualBox settings diff --git a/dev/vars/webserver.yml b/dev/vars/webserver.yml index 3c7f583..fced150 100644 --- a/dev/vars/webserver.yml +++ b/dev/vars/webserver.yml @@ -9,10 +9,13 @@ secret: NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd! ############## -### Docker ### +### Common ### ############## -docker_users: - - vagrant +users: + oci: + uid: 2000 + gid: 2000 + home: true ################ #### MariaDB ### diff --git a/dev/webserver.yml b/dev/webserver.yml index 35832e1..f93efc9 100644 --- a/dev/webserver.yml +++ b/dev/webserver.yml @@ -5,5 +5,5 @@ - vars/webserver.yml roles: - common - - docker + - podman - webserver diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index a891384..0972c48 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -1,4 +1,5 @@ -packages: +common_packages: - dnsutils - ncdu - tree + - vim diff --git a/roles/common/tasks/debian.yml b/roles/common/tasks/debian.yml new file mode 100644 index 0000000..bfff234 --- /dev/null +++ b/roles/common/tasks/debian.yml @@ -0,0 +1,30 @@ +- name: Install useful software + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + update_cache: true + +- name: Install the Uncomplicated Firewall + ansible.builtin.apt: + name: ufw + state: present + update_cache: true + +- name: Deny incoming traffic by default + community.general.ufw: + default: deny + direction: incoming + +- name: Allow outgoing traffic by default + community.general.ufw: + default: allow + direction: outgoing + +- name: Allow OpenSSH with rate limiting + community.general.ufw: + name: ssh + rule: limit + +- name: Enable firewall + community.general.ufw: + state: enabled diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 8a6a01c..3a572fa 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,35 +2,38 @@ ansible.builtin.file: path: "~/.ansible/tmp" state: directory - mode: 0700 + mode: "700" -- name: Install useful software - ansible.builtin.apt: - name: "{{ packages }}" +- name: Create system user groups + ansible.builtin.group: + name: "{{ item.key }}" + gid: "{{ item.value.gid }}" state: present - update_cache: true + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: users is defined -- name: Install the Uncomplicated Firewall - ansible.builtin.apt: - name: ufw +- name: Create system users + ansible.builtin.user: + name: "{{ item.key }}" state: present - update_cache: true + uid: "{{ item.value.uid }}" + group: "{{ item.value.gid }}" + groups: "{{ item.value.groups | default([]) }}" + shell: "{{ item.value.shell | default('/bin/bash') }}" + create_home: "{{ item.value.home | default(false) }}" + home: "{{ item.value.homedir | default('/home/' + item.key) }}" + system: "{{ item.value.system | default(false) }}" + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: users is defined -- name: Deny incoming traffic by default - community.general.ufw: - default: deny - direction: incoming +- name: Include Debian-specific tasks + ansible.builtin.include_tasks: debian.yml + when: ansible_os_family == "Debian" -- name: Allow outgoing traffic by default - community.general.ufw: - default: allow - direction: outgoing - -- name: Allow OpenSSH with rate limiting - community.general.ufw: - name: ssh - rule: limit - -- name: Enable firewall - community.general.ufw: - state: enabled +- name: Include Rocky Linux-specific tasks + ansible.builtin.include_tasks: rocky.yml + when: ansible_os_family == "RedHat" diff --git a/roles/common/tasks/rocky.yml b/roles/common/tasks/rocky.yml new file mode 100644 index 0000000..ade3100 --- /dev/null +++ b/roles/common/tasks/rocky.yml @@ -0,0 +1,42 @@ +- name: Install EPEL repository + ansible.builtin.dnf: + name: epel-release + state: present + update_cache: true + +- name: Install useful software + ansible.builtin.dnf: + name: "{{ common_packages }}" + state: present + update_cache: true + +- name: Install firewalld + ansible.builtin.dnf: + name: firewalld + state: present + +- name: Start and enable firewalld service + ansible.builtin.systemd: + name: firewalld + state: started + enabled: true + +- name: Set default zone to drop (deny incoming by default) + ansible.posix.firewalld: + zone: drop + state: enabled + permanent: true + immediate: true + +- name: Allow SSH in drop zone with rate limiting via rich rule + ansible.posix.firewalld: + zone: drop + rich_rule: 'rule service name="ssh" accept limit value="10/m"' + permanent: true + immediate: true + state: enabled + +- name: Set drop as the default zone + ansible.builtin.command: + cmd: firewall-cmd --set-default-zone=drop + changed_when: false diff --git a/roles/podman/handlers/main.yml b/roles/podman/handlers/main.yml new file mode 100644 index 0000000..81d3d77 --- /dev/null +++ b/roles/podman/handlers/main.yml @@ -0,0 +1,4 @@ +- name: Restart systemd-logind + ansible.builtin.systemd: + name: systemd-logind + state: restarted diff --git a/roles/podman/tasks/main.yml b/roles/podman/tasks/main.yml new file mode 100644 index 0000000..90bca09 --- /dev/null +++ b/roles/podman/tasks/main.yml @@ -0,0 +1,50 @@ +- name: Install Podman + ansible.builtin.dnf: + name: ["podman", "podman-docker", "podman-compose"] + state: present + +- name: Configure Podman to default to docker.io registry + ansible.builtin.copy: + dest: /etc/containers/registries.conf.d/docker-default.conf + content: | + [[registry]] + prefix = "docker.io" + location = "docker.io" + + [[registry]] + prefix = "" + location = "docker.io" + + [registries] + short-name-mode = "disabled" + mode: "644" + +- name: Create logind.conf.d directory + ansible.builtin.file: + path: /etc/systemd/logind.conf.d + state: directory + mode: "755" + +- name: Create linger directory + ansible.builtin.file: + path: /var/lib/systemd/linger + state: directory + mode: "755" + +- name: Enable lingering for oci user + ansible.builtin.file: + path: /var/lib/systemd/linger/oci + state: touch + mode: "644" + notify: Restart systemd-logind + +- name: Force handler execution for user lingering + ansible.builtin.meta: flush_handlers + +- name: Create user systemd directory + ansible.builtin.file: + path: "/home/oci/.config/systemd/user" + state: directory + mode: "755" + owner: oci + group: oci diff --git a/roles/webserver/files/docker-compose.yml b/roles/webserver/files/docker-compose.yml index c9e564c..578ea54 100644 --- a/roles/webserver/files/docker-compose.yml +++ b/roles/webserver/files/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.5' - volumes: wordpress: nextcloud: @@ -10,7 +8,7 @@ networks: services: traefik: - image: traefik:${TRAEFIK_VERSION:-latest} + image: ${TRAEFIK_IMAGE:-docker.io/library/traefik}:${TRAEFIK_VERSION:-latest} restart: always command: - --api.dashboard=${TRAEFIK_DASHBOARD:-true} @@ -20,7 +18,7 @@ services: - --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false} - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - - --entrypoints.local.address=:8443 + - --entrypoints.local.address=:9443 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.web.http.redirections.entrypoint.permanent=true @@ -33,9 +31,9 @@ services: environment: DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY} ports: - - 80:80 - - 443:443 - - "127.0.0.1:8443:8443" + - 8080:80 + - 8443:443 + - "127.0.0.1:9443:9443" volumes: - /var/run/docker.sock:/var/run/docker.sock - ./.acme:/etc/letsencrypt @@ -52,7 +50,7 @@ services: - traefik wordpress: - image: wordpress:${WORDPRESS_VERSION:-latest} + image: ${WORDPRESS_IMAGE:-docker.io/library/wordpress}:${WORDPRESS_VERSION:-latest} restart: always environment: WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal} @@ -81,7 +79,7 @@ services: - host.docker.internal:host-gateway nextcloud: - image: nextcloud:${NEXTCLOUD_VERSION:-stable} + image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud}:${NEXTCLOUD_VERSION:-stable} restart: always environment: MYSQL_HOST: ${NEXTCLOUD_MYSQL_HOST:-host.docker.internal:3306} diff --git a/roles/webserver/handlers/main.yml b/roles/webserver/handlers/main.yml index 0bc6198..6b333f8 100644 --- a/roles/webserver/handlers/main.yml +++ b/roles/webserver/handlers/main.yml @@ -4,11 +4,10 @@ state: restarted listen: restart_mariadb -- name: Compose up on webserver stack - ansible.builtin.command: "docker-compose up -d" - args: - chdir: "{{ webserver_root }}" - listen: composeup_webserver +- name: Start podman compose project + ansible.builtin.command: + cmd: podman compose up -d + chdir: /home/oci/webserver - name: Grab Nextcloud container information community.docker.docker_container_info: @@ -22,11 +21,12 @@ port: 80 listen: composeup_webserver -- name: Check Nextcloud status - ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 - php occ status" - listen: composeup_webserver - register: nextcloud_status +# - name: Check Nextcloud status +# ansible.builtin.command: +# "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 +# php occ status" +# listen: composeup_webserver +# register: nextcloud_status - name: Import Nextcloud installation handlers ansible.builtin.import_tasks: nextcloud.yml @@ -34,3 +34,15 @@ when: - nextcloud_status.stderr[:26] == "Nextcloud is not installed" - nextcloud_autoinstall + +- name: Import Webserver project handlers + ansible.builtin.import_tasks: webserver.yml + +- name: Install webserver docker-compose.yml + ansible.builtin.copy: + src: docker-compose.yml + dest: /home/oci/webserver/compose.yml + mode: "600" + owner: oci + group: oci + notify: Generate systemd service files diff --git a/roles/webserver/handlers/webserver.yml b/roles/webserver/handlers/webserver.yml new file mode 100644 index 0000000..c6464d0 --- /dev/null +++ b/roles/webserver/handlers/webserver.yml @@ -0,0 +1,27 @@ +- name: Start podman compose project + ansible.builtin.command: + cmd: podman compose up -d + chdir: /home/oci/webserver + notify: Generate systemd service files + become_user: oci + +- name: Generate systemd service files + ansible.builtin.command: + cmd: podman generate systemd --new --files --file /home/oci/webserver/compose.yml + chdir: "/home/oci/.config/systemd/user" + notify: Reload systemd user daemon + become_user: oci + +- name: Reload systemd user daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + become_user: oci + notify: Enable systemd user service + +- name: Enable systemd user service + ansible.builtin.systemd: + name: webserver + enabled: true + scope: user + become_user: oci \ No newline at end of file diff --git a/roles/webserver/tasks/debian.yml b/roles/webserver/tasks/debian.yml new file mode 100644 index 0000000..8e8fd3e --- /dev/null +++ b/roles/webserver/tasks/debian.yml @@ -0,0 +1,48 @@ +- name: Install MariaDB Server + ansible.builtin.apt: + name: mariadb-server + state: present + +- name: Change the bind-address to allow Docker + ansible.builtin.lineinfile: + path: /etc/mysql/mariadb.conf.d/50-server.cnf + regex: "^bind-address" + line: "bind-address = 0.0.0.0" + notify: restart_mariadb + +- name: Install MySQL Support for Python 3 + ansible.builtin.apt: + name: python3-pymysql + state: present + +- name: Create MariaDB databases + community.mysql.mysql_db: + name: "{{ item.name }}" + state: present + login_unix_socket: /var/run/mysqld/mysqld.sock + loop: "{{ databases }}" + no_log: true + +- name: Create MariaDB users + community.mysql.mysql_user: + name: "{{ item.name }}" + password: "{{ item.pass }}" + host: "%" + state: present + priv: "{{ item.name }}.*:ALL" + login_unix_socket: /var/run/mysqld/mysqld.sock + loop: "{{ databases }}" + no_log: true + +- name: Create webserver docker-compose directory + ansible.builtin.file: + path: "{{ webserver_root }}" + state: directory + mode: "600" + +- name: Install webserver docker-compose.yml + ansible.builtin.copy: + src: docker-compose.yml + dest: "{{ webserver_root }}/docker-compose.yml" + mode: "600" + notify: composeup_webserver diff --git a/roles/webserver/tasks/main.yml b/roles/webserver/tasks/main.yml index 1760726..7aa5888 100644 --- a/roles/webserver/tasks/main.yml +++ b/roles/webserver/tasks/main.yml @@ -1,72 +1,7 @@ -- name: Install MariaDB Server - ansible.builtin.apt: - name: mariadb-server - state: present +- name: Include Debian-specific tasks + ansible.builtin.include_tasks: debian.yml + when: ansible_os_family == "Debian" -- name: Change the bind-address to allow Docker - ansible.builtin.lineinfile: - path: /etc/mysql/mariadb.conf.d/50-server.cnf - regex: "^bind-address" - line: "bind-address = 0.0.0.0" - notify: restart_mariadb - -- name: Install MySQL Support for Python 3 - ansible.builtin.apt: - name: python3-pymysql - state: present - -- name: Create MariaDB databases - community.mysql.mysql_db: - name: "{{ item.name }}" - state: present - login_unix_socket: /var/run/mysqld/mysqld.sock - loop: "{{ databases }}" - no_log: "{{ item.pass is defined }}" - -- name: Create MariaDB users - community.mysql.mysql_user: - name: "{{ item.name }}" - password: "{{ item.pass }}" - host: '%' - state: present - priv: "{{ item.name }}.*:ALL" - login_unix_socket: /var/run/mysqld/mysqld.sock - loop: "{{ databases }}" - no_log: "{{ item.pass is defined }}" - -- name: Create webserver docker-compose directory - ansible.builtin.file: - path: "{{ webserver_root }}" - state: directory - mode: 0600 - -- name: Install webserver docker-compose.yml - ansible.builtin.copy: - src: docker-compose.yml - dest: "{{ webserver_root }}/docker-compose.yml" - mode: 0600 - notify: composeup_webserver - -- name: Install docker-compose .env - ansible.builtin.template: - src: compose-env.j2 - dest: "{{ webserver_root }}/.env" - mode: 0600 - notify: composeup_webserver - -- name: Allow MariaDB database connections - community.general.ufw: - rule: allow - port: 3306 - proto: tcp - src: "{{ item }}" - loop: "{{ mariadb_trust }}" - -- name: Add HTTP and HTTPS firewall rule - community.general.ufw: - rule: allow - port: "{{ item }}" - proto: tcp - loop: - - "80" - - "443" +- name: Include Rocky Linux-specific tasks + ansible.builtin.include_tasks: rocky.yml + when: ansible_os_family == "RedHat" diff --git a/roles/webserver/tasks/rocky.yml b/roles/webserver/tasks/rocky.yml new file mode 100644 index 0000000..1f82cbd --- /dev/null +++ b/roles/webserver/tasks/rocky.yml @@ -0,0 +1,102 @@ +- name: Install MariaDB Server + ansible.builtin.dnf: + name: mariadb-server + state: present + +- name: Change the bind-address to allow Docker + ansible.builtin.lineinfile: + path: /etc/my.cnf.d/mariadb-server.cnf + regex: "^bind-address" + line: "bind-address = 0.0.0.0" + notify: restart_mariadb + +- name: Start and enable MariaDB service + ansible.builtin.systemd: + name: mariadb + state: started + enabled: true + +- name: Install MySQL Support for Python 3 + ansible.builtin.dnf: + name: python3-PyMySQL + state: present + +- name: Create MariaDB databases + community.mysql.mysql_db: + name: "{{ item.name }}" + state: present + login_unix_socket: /var/lib/mysql/mysql.sock + loop: "{{ databases }}" + no_log: true + +- name: Create MariaDB users + community.mysql.mysql_user: + name: "{{ item.name }}" + password: "{{ item.pass }}" + host: "%" + state: present + priv: "{{ item.name }}.*:ALL" + login_unix_socket: /var/lib/mysql/mysql.sock + loop: "{{ databases }}" + no_log: true + +- name: Create webserver stack directory + ansible.builtin.file: + path: /home/oci/webserver + state: directory + mode: "700" + owner: oci + group: oci + +- name: Install webserver compose file + ansible.builtin.copy: + src: docker-compose.yml + dest: /home/oci/webserver/compose.yml + mode: "600" + owner: oci + group: oci + notify: Start podman compose project + +- name: Generate webserver environment configuration + ansible.builtin.template: + src: compose-env.j2 + dest: /home/oci/webserver/.env + mode: "644" + owner: oci + group: oci + notify: Start podman compose project + +- name: Enable IP forwarding + ansible.posix.sysctl: + name: net.ipv4.ip_forward + value: '1' + state: present + reload: true + +- name: Allow port 80 in firewall + ansible.posix.firewalld: + port: 80/tcp + permanent: true + state: enabled + immediate: true + +- name: Forward port 80 to 8080 + ansible.posix.firewalld: + rich_rule: 'rule family="ipv4" forward-port port="80" protocol="tcp" to-port="8080"' + permanent: true + state: enabled + immediate: true + +- name: Allow port 443 in firewall + ansible.posix.firewalld: + port: 443/tcp + permanent: true + state: enabled + immediate: true + +- name: Forward port 443 to 8443 + ansible.posix.firewalld: + rich_rule: 'rule family="ipv4" forward-port port="443" protocol="tcp" to-port="8443"' + permanent: true + state: enabled + immediate: true diff --git a/webserver.yml b/webserver.yml index 6878092..e660d63 100644 --- a/webserver.yml +++ b/webserver.yml @@ -3,5 +3,5 @@ become: true roles: - common - - docker + - podman - webserver