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..632cfd5 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,21 @@ Vagrant.configure("2") do |config| vbox.memory = 4096 end + # Expand XFS rootfs + config.vm.provision "shell", inline: <<-SHELL + set -xe + dnf install -y cloud-utils-growpart + df -h / + growpart /dev/vda 4 + 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/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/main.yml b/roles/common/tasks/main.yml index 8a6a01c..3046919 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,35 +2,85 @@ ansible.builtin.file: path: "~/.ansible/tmp" state: directory - mode: 0700 + mode: "700" + +- 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: "~/.ansible/tmp" + state: directory + mode: "700" + become_user: "{{ item.key }}" + become: true + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: users is defined + +- 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..bb87448 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,11 +31,11 @@ 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 + - ${OCI_SOCK:-/run/user/2000/podman/podman.sock}:/var/run/docker.sock:ro - ./.acme:/etc/letsencrypt labels: traefik.http.routers.api.rule: Host(`${TRAEFIK_DOMAIN:-traefik.local.freeitathens.org}`) @@ -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..8c4be2b 100644 --- a/roles/webserver/handlers/main.yml +++ b/roles/webserver/handlers/main.yml @@ -4,12 +4,6 @@ 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: Grab Nextcloud container information community.docker.docker_container_info: name: "{{ webserver_root | basename }}_nextcloud_1" @@ -22,11 +16,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 +29,96 @@ 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 + +#- 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 +# become: true +# vars: +# ansible_become_user: oci +# environment: +# XDG_RUNTIME_DIR: "/run/user/2000" +# HOME: "/home/oci" + +- name: Start podman compose project + ansible.builtin.shell: + cmd: | + source /etc/profile + cd /home/oci/webserver + podman compose up -d + #notify: Generate systemd service files + notify: Debug handler environment + become: true + become_user: oci + become_flags: "-i" + +- name: Debug handler environment + ansible.builtin.shell: | + echo "=== HANDLER DEBUG ===" + whoami + id + env | grep -E "(USER|HOME|XDG|SUDO)" | sort + echo "=== PODMAN INFO ===" + podman info --format json | jq '.store.graphRoot, .store.runRoot' + become: true + become_user: oci + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + listen: "Generate systemd service files" + +- 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 + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + +- name: Reload systemd user daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + notify: Enable systemd user service + become_user: oci + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + +- name: Enable systemd user service + ansible.builtin.systemd: + name: webserver + enabled: true + scope: user + become_user: oci + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" diff --git a/roles/webserver/handlers/webserver.yml b/roles/webserver/handlers/webserver.yml new file mode 100644 index 0000000..b446365 --- /dev/null +++ b/roles/webserver/handlers/webserver.yml @@ -0,0 +1,80 @@ +#- 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 +# become: true +# vars: +# ansible_become_user: oci +# environment: +# XDG_RUNTIME_DIR: "/run/user/2000" +# HOME: "/home/oci" + +- name: Start podman compose project + ansible.builtin.shell: + cmd: | + source /etc/profile + cd /home/oci/webserver + podman compose up -d + #notify: Generate systemd service files + notify: Debug handler environment + become: true + become_user: oci + become_flags: '-i' + +- name: Debug handler environment + ansible.builtin.shell: | + echo "=== HANDLER DEBUG ===" + whoami + id + env | grep -E "(USER|HOME|XDG|SUDO)" | sort + echo "=== PODMAN INFO ===" + podman info --format json | jq '.store.graphRoot, .store.runRoot' + become: true + become_user: oci + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + listen: "Generate systemd service files" + +- 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 + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + +- name: Reload systemd user daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + notify: Enable systemd user service + become_user: oci + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" + +- name: Enable systemd user service + ansible.builtin.systemd: + name: webserver + enabled: true + scope: user + become_user: oci + become: true + vars: + ansible_become_user: oci + environment: + XDG_RUNTIME_DIR: "/run/user/2000" + HOME: "/home/oci" diff --git a/roles/webserver/tasks/main.yml b/roles/webserver/tasks/main.yml index 1760726..d83def6 100644 --- a/roles/webserver/tasks/main.yml +++ b/roles/webserver/tasks/main.yml @@ -1,72 +1,102 @@ - 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: "644" + 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: Enable IP forwarding + ansible.posix.sysctl: + name: net.ipv4.ip_forward + value: "1" + state: present + reload: true -- name: Add HTTP and HTTPS firewall rule - community.general.ufw: - rule: allow - port: "{{ item }}" - proto: tcp - loop: - - "80" - - "443" +- 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