diff --git a/.gitignore b/.gitignore index 961961f..3705713 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.ansible_vault +.ansible* .bitwarden environments *.log diff --git a/Makefile b/Makefile index 970a6b0..b480eb8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ +SHELL := /bin/bash + all: vagrant vagrant: - vagrant up --no-destroy-on-error --no-color | tee ./vagrantup.log + set -o pipefail; vagrant up --no-destroy-on-error --no-color | tee ./vagrantup.log ./scripts/forward-ssh.sh clean: diff --git a/README.md b/README.md index d747275..5fe449d 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:9443/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..a717572 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -14,9 +14,13 @@ else File.write(".playbook", PLAYBOOK) end -# Debian 11 +# Optionally allow more verbosity in Ansible +VAGRANT_ANSIBLE_VERBOSE=ENV["VAGRANT_ANSIBLE_VERBOSE"] || false + Vagrant.configure("2") do |config| - config.vm.box = "debian/bullseye64" + config.vm.box = "rockylinux/9" + config.vm.hostname = "fritadev" + config.vm.disk :disk, size: "100GB", primary: true config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.network "private_network", type: "dhcp" @@ -29,6 +33,7 @@ Vagrant.configure("2") do |config| libvirt.cpus = 2 libvirt.memory = 4096 libvirt.default_prefix = "" + libvirt.machine_virtual_size = 100 end # Set VirtualBox settings @@ -37,11 +42,24 @@ Vagrant.configure("2") do |config| vbox.memory = 4096 end + # Expand XFS rootfs + config.vm.provision "shell", inline: <<-SHELL + set -xe + df -h / + dnf install -y cloud-utils-growpart + PART="$(findmnt -n -o SOURCE /)" + DISK="$(lsblk -n -o PKNAME "$PART")" + NUM="$(lsblk -n -o KNAME "$PART" | sed 's/.*[^0-9]//')" + growpart "/dev/$DISK" "$NUM" && \ + xfs_growfs / + df -h / + SHELL + # Provision with Ansible config.vm.provision "ansible" do |ansible| ENV['ANSIBLE_ROLES_PATH'] = File.dirname(__FILE__) + "/roles" ansible.compatibility_mode = "2.0" ansible.playbook = "dev/" + PLAYBOOK + ".yml" + ansible.verbose = VAGRANT_ANSIBLE_VERBOSE end - end diff --git a/ansible.cfg b/ansible.cfg index ddb62ee..a987b17 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -3,4 +3,4 @@ inventory = ./environments/development interpreter_python = /usr/bin/python3 [ssh_connection] -pipelining=True +pipelining = True diff --git a/dev/vars/webserver.yml b/dev/vars/webserver.yml index 3c7f583..3995bb4 100644 --- a/dev/vars/webserver.yml +++ b/dev/vars/webserver.yml @@ -9,10 +9,14 @@ secret: NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd! ############## -### Docker ### +### Common ### ############## -docker_users: - - vagrant +users: + oci: + uid: 2000 + gid: 2000 + home: true + ansible_temp: true ################ #### MariaDB ### @@ -30,12 +34,12 @@ webserver: ############### ### Traefik ### ############### - #TRAEFIK_VERSION: latest - #TRAEFIK_ROOT_DOMAIN: local.freeitathens.org - #TRAEFIK_DOMAIN: traefik.local.freeitathens.org - #TRAEFIK_DASHBOARD: true - #TRAEFIK_EXPOSED_DEFAULT: false - #TRAEFIK_WEB_ENABLED: true + # TRAEFIK_VERSION: latest + # TRAEFIK_ROOT_DOMAIN: local.freeitathens.org + # TRAEFIK_DOMAIN: traefik.local.freeitathens.org + # TRAEFIK_DASHBOARD: true + # TRAEFIK_EXPOSED_DEFAULT: false + # TRAEFIK_WEB_ENABLED: true TRAEFIK_DEBUG: true TRAEFIK_ACME_PROVIDER: dreamhost TRAEFIK_ACME_CASERVER: https://localhost/directory @@ -45,23 +49,23 @@ webserver: ################# ### WordPress ### ################# - #WORDPRESS_VERSION: latest - #WORDPRESS_DOMAIN: www.local.freeitathens.org - #WORDPRESS_DB_HOST: host.docker.internal - #WORDPRESS_DB_NAME: wordpress - #WORDPRESS_DB_USER: wordpress - #WORDPRESS_WEB_ENABLED: true + # WORDPRESS_VERSION: latest + # WORDPRESS_DOMAIN: www.local.freeitathens.org + # WORDPRESS_DB_HOST: host.docker.internal + # WORDPRESS_DB_NAME: wordpress + # WORDPRESS_DB_USER: wordpress + # WORDPRESS_WEB_ENABLED: true WORDPRESS_DB_PASSWORD: "{{ secret.WORDPRESS_DB_PASSWORD }}" ################# ### Nextcloud ### ################# - #NEXTCLOUD_VERSION: stable - #NEXTCLOUD_DOMAIN: cloud.local.freeitathens.org - #NEXTCLOUD_MYSQL_HOST: host.docker.internal - #NEXTCLOUD_MYSQL_DATABASE: nextcloud - #NEXTCLOUD_MYSQL_USER: nextcloud - #NEXTCLOUD_WEB_ENABLED: true - #NEXTCLOUD_ADMIN: admin + # NEXTCLOUD_VERSION: stable + # NEXTCLOUD_DOMAIN: cloud.local.freeitathens.org + # NEXTCLOUD_MYSQL_HOST: host.docker.internal + # NEXTCLOUD_MYSQL_DATABASE: nextcloud + # NEXTCLOUD_MYSQL_USER: nextcloud + # NEXTCLOUD_WEB_ENABLED: true + # NEXTCLOUD_ADMIN: admin NEXTCLOUD_ADMIN_PASSWORD: "{{ secret.NEXTCLOUD_ADMIN_PASSWORD }}" NEXTCLOUD_MYSQL_PASSWORD: "{{ secret.NEXTCLOUD_MYSQL_PASSWORD }}" 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/main.yml b/roles/common/tasks/main.yml index 8a6a01c..55eeae4 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,35 +2,87 @@ ansible.builtin.file: path: "~/.ansible/tmp" state: directory - mode: 0700 + mode: "755" + +- name: Create system user groups + ansible.builtin.group: + name: "{{ item.key }}" + gid: "{{ item.value.gid }}" + state: present + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: users is defined + +- name: Create system users + ansible.builtin.user: + name: "{{ item.key }}" + state: present + 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: Create Ansible's temporary remote directory for users + ansible.builtin.file: + path: "{{ item.value.homedir | default('/home/' + item.key) }}/.ansible/tmp" + state: directory + mode: "755" + owner: "{{ item.key }}" + group: "{{ item.value.gid }}" + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: + - users is defined + - item.value.ansible_temp | default(false) + +- name: Install EPEL repository + ansible.builtin.dnf: + name: epel-release + state: present + update_cache: true - name: Install useful software - ansible.builtin.apt: - name: "{{ packages }}" + ansible.builtin.dnf: + name: "{{ common_packages }}" state: present update_cache: true -- name: Install the Uncomplicated Firewall - ansible.builtin.apt: - name: ufw +- name: Install firewalld + ansible.builtin.dnf: + name: firewalld state: present - update_cache: true -- name: Deny incoming traffic by default - community.general.ufw: - default: deny - direction: incoming +- name: Start and enable firewalld service + ansible.builtin.systemd: + name: firewalld + state: started + enabled: true -- 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: +- 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..97402de --- /dev/null +++ b/roles/podman/tasks/main.yml @@ -0,0 +1,49 @@ +- name: Install Podman + ansible.builtin.dnf: + name: ["podman", "podman-docker", "podman-compose"] + state: present + +- name: Create /etc/containers/nodocker to quiet CLI emulation notice + ansible.builtin.file: + path: /etc/containers/nodocker + state: touch + 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 + +- name: Enable oci's podman socket + ansible.builtin.systemd: + name: podman.socket + enabled: true + state: started + scope: user + become_user: oci + become: true diff --git a/roles/webserver/files/docker-compose.yml b/roles/webserver/files/docker-compose.yml index c9e564c..2d4043c 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,8 +8,10 @@ networks: services: traefik: - image: traefik:${TRAEFIK_VERSION:-latest} + image: ${TRAEFIK_IMAGE:-docker.io/library/traefik}:${TRAEFIK_VERSION:-latest} restart: always + security_opt: + - label=type:container_runtime_t command: - --api.dashboard=${TRAEFIK_DASHBOARD:-true} - --api.debug=${TRAEFIK_DEBUG:-false} @@ -20,7 +20,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,11 +33,11 @@ services: environment: DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY} ports: - - 80:80 - - 443:443 - - "127.0.0.1:8443:8443" + - "${ENTRYWEB:-127.0.0.1:8080}:80" + - "${ENTRYSECURE:-127.0.0.1:8443}:443" + - "${ENTRYLOCAL:-127.0.0.1:9443}:9443" volumes: - - /var/run/docker.sock:/var/run/docker.sock + - ${OCI_SOCK:-/run/user/2000/podman/podman.sock}:/var/run/docker.sock:ro,Z - ./.acme:/etc/letsencrypt labels: traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`) @@ -52,7 +52,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 +81,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/files/nginx.conf b/roles/webserver/files/nginx.conf new file mode 100644 index 0000000..c853287 --- /dev/null +++ b/roles/webserver/files/nginx.conf @@ -0,0 +1,22 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +stream { + server { + listen 80; + proxy_pass 127.0.0.1:8080; + } + + server { + listen 443; + proxy_pass 127.0.0.1:8443; + } +} diff --git a/roles/webserver/handlers/main.yml b/roles/webserver/handlers/main.yml index 0bc6198..75fe368 100644 --- a/roles/webserver/handlers/main.yml +++ b/roles/webserver/handlers/main.yml @@ -1,14 +1,38 @@ +- name: Restart nginx + ansible.builtin.systemd: + name: nginx + state: restarted + - name: Restart MariaDB ansible.builtin.service: name: mariadb 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" + notify: Generate systemd service files + changed_when: false + become_user: oci + become: true + +- name: Reload systemd user daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + notify: Enable systemd user service + become_user: oci + become: true + +- name: Enable systemd user service + ansible.builtin.systemd: + name: webserver + enabled: true + scope: user + become_user: oci + become: true - name: Grab Nextcloud container information community.docker.docker_container_info: @@ -23,8 +47,9 @@ listen: composeup_webserver - name: Check Nextcloud status - ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 - php occ status" + ansible.builtin.command: + "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 + php occ status" listen: composeup_webserver register: nextcloud_status @@ -34,3 +59,12 @@ when: - nextcloud_status.stderr[:26] == "Nextcloud is not installed" - nextcloud_autoinstall + +- 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/tasks/main.yml b/roles/webserver/tasks/main.yml index 1760726..d52d053 100644 --- a/roles/webserver/tasks/main.yml +++ b/roles/webserver/tasks/main.yml @@ -1,72 +1,96 @@ - name: Install MariaDB Server - ansible.builtin.apt: + ansible.builtin.dnf: 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 + 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.apt: - name: python3-pymysql + 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/run/mysqld/mysqld.sock + login_unix_socket: /var/lib/mysql/mysql.sock loop: "{{ databases }}" - no_log: "{{ item.pass is defined }}" + no_log: true - name: Create MariaDB users community.mysql.mysql_user: name: "{{ item.name }}" password: "{{ item.pass }}" - host: '%' + host: "%" state: present priv: "{{ item.name }}.*:ALL" - login_unix_socket: /var/run/mysqld/mysqld.sock + login_unix_socket: /var/lib/mysql/mysql.sock loop: "{{ databases }}" - no_log: "{{ item.pass is defined }}" + no_log: true -- name: Create webserver docker-compose directory +- name: Create webserver stack directory ansible.builtin.file: - path: "{{ webserver_root }}" + path: /home/oci/webserver state: directory - mode: 0600 + mode: "700" + owner: oci + group: oci -- name: Install webserver docker-compose.yml +- name: Install webserver compose file ansible.builtin.copy: src: docker-compose.yml - dest: "{{ webserver_root }}/docker-compose.yml" - mode: 0600 - notify: composeup_webserver + dest: /home/oci/webserver/compose.yml + mode: "600" + owner: oci + group: oci + notify: Start podman compose project -- name: Install docker-compose .env +- name: Generate webserver environment configuration ansible.builtin.template: src: compose-env.j2 - dest: "{{ webserver_root }}/.env" - mode: 0600 - notify: composeup_webserver + dest: /home/oci/webserver/.env + mode: "400" + owner: oci + group: oci + notify: Start podman compose project -- name: Allow MariaDB database connections - community.general.ufw: - rule: allow - port: 3306 - proto: tcp - src: "{{ item }}" - loop: "{{ mariadb_trust }}" +- name: Install nginx + ansible.builtin.dnf: + name: ["nginx", "nginx-mod-stream"] + state: present + update_cache: true -- name: Add HTTP and HTTPS firewall rule - community.general.ufw: - rule: allow - port: "{{ item }}" - proto: tcp +- name: Deploy nginx proxy config + ansible.builtin.copy: + src: nginx.conf + dest: /etc/nginx/nginx.conf + mode: "644" + notify: Restart nginx + +- name: Allow HTTP and HTTPS in firewall + ansible.posix.firewalld: + service: "{{ item }}" + permanent: true + state: enabled + immediate: true loop: - - "80" - - "443" + - http + - https + +- name: Start and enable nginx + ansible.builtin.systemd: + name: nginx + state: started + enabled: true diff --git a/scripts/forward-ssh.sh b/scripts/forward-ssh.sh index 471b800..9646aa8 100755 --- a/scripts/forward-ssh.sh +++ b/scripts/forward-ssh.sh @@ -8,7 +8,7 @@ MATCH_PATTERN="ssh -fNT -i ${PRIVATE_KEY}.*vagrant@" function ssh_connect { sudo ssh -fNT -i "$PRIVATE_KEY" \ - -L 8443:localhost:8443 \ + -L 9443:localhost:9443 \ -L 80:localhost:80 \ -L 443:localhost:443 \ -o UserKnownHostsFile=/dev/null \ 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