This commit is contained in:
Kris Lamoureux 2025-06-06 12:36:28 -04:00
parent 236ec455cc
commit 6776923048
18 changed files with 408 additions and 145 deletions

0
.ansible/.lock Normal file
View File

View File

@ -1,58 +1,65 @@
# Free I.T. Athen's Infrastructure # Free I.T. Athen's Infrastructure
This project is used to develop Ansible for deploying and maintaining websites This project is used to develop Ansible for deploying and maintaining websites
and services operated by Free I.T. Athens (FRITA). and services operated by Free I.T. Athens (FRITA).
- Requires GNU Make, Ansible, and Vagrant on the host - Requires GNU Make, Ansible, and Vagrant on the host
## Quick Start ## Quick Start
1. Clone this project 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 3. Go to
- [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/) - [Traefik Dashboard](https://traefik.local.freeitathens.org:8443/dashboard/#/)
- [WordPress](https://www.local.freeitathens.org) - [WordPress](https://www.local.freeitathens.org)
- [Nextcloud](https://cloud.local.freeitathens.org) - [Nextcloud](https://cloud.local.freeitathens.org)
4. Click through the HTTPS security warning 4. Click through the HTTPS security warning
## Production ## Production
1. Clone [production-env](https://github.com/freeitathens/production-env/) to `./environments` 1. Clone [production-env](https://github.com/freeitathens/production-env/) to `./environments`
``` ```
mkdir -p environments mkdir -p environments
git clone git@github.com:freeitathens/production-env.git ./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 2. Run `./scripts/vault-key.sh` from the root of the project to obtain the Ansible Vault password
3. Enter the Bitwarden Master Password 3. Enter the Bitwarden Master Password
4. Run `ansible-playbook` against the production servers, e.g., 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 5. Delete the `.ansible_vault` file when you are done
### Using Ansible Vault to add or rotate values ### Using Ansible Vault to add or rotate values
Do not submit ciphertext into Ansible Vault with the indention formatting.<br /> Do not submit ciphertext into Ansible Vault with the indention formatting.<br />
To submit, press `CTRL+d` twice. To submit, press `CTRL+d` twice.
- Decrypt Ansible Vault values - 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 - Encrypt new Ansible Vault values
``` ```
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`
- e.g., `pwgen -s 100 1 | ansible-vault encrypt --vault-pass-file .ansible_vault`
## Authors ## Authors
* **Kris Lamoureux** - *Project Founder* - [@krislamo](https://github.com/krislamo)
- **Kris Lamoureux** - _Project Founder_ - [@krislamo](https://github.com/krislamo)
## Copyrights and Licenses ## 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 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 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 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 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 You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>. this program. If not, see <https://www.gnu.org/licenses/>.

5
Vagrantfile vendored
View File

@ -14,9 +14,8 @@ else
File.write(".playbook", PLAYBOOK) File.write(".playbook", PLAYBOOK)
end end
# Debian 11
Vagrant.configure("2") do |config| Vagrant.configure("2") do |config|
config.vm.box = "debian/bullseye64" config.vm.box = "rockylinux/9"
config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.network "private_network", type: "dhcp" config.vm.network "private_network", type: "dhcp"
@ -29,6 +28,8 @@ Vagrant.configure("2") do |config|
libvirt.cpus = 2 libvirt.cpus = 2
libvirt.memory = 4096 libvirt.memory = 4096
libvirt.default_prefix = "" libvirt.default_prefix = ""
# Doesn't boot without this on rockylinux/9
libvirt.machine_virtual_size = 10
end end
# Set VirtualBox settings # Set VirtualBox settings

View File

@ -9,10 +9,13 @@ secret:
NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd! NEXTCLOUD_ADMIN_PASSWORD: NCadm1npa55w0rd!
############## ##############
### Docker ### ### Common ###
############## ##############
docker_users: users:
- vagrant oci:
uid: 2000
gid: 2000
home: true
################ ################
#### MariaDB ### #### MariaDB ###

View File

@ -5,5 +5,5 @@
- vars/webserver.yml - vars/webserver.yml
roles: roles:
- common - common
- docker - podman
- webserver - webserver

View File

@ -1,4 +1,5 @@
packages: common_packages:
- dnsutils - dnsutils
- ncdu - ncdu
- tree - tree
- vim

View File

@ -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

View File

@ -2,35 +2,38 @@
ansible.builtin.file: ansible.builtin.file:
path: "~/.ansible/tmp" path: "~/.ansible/tmp"
state: directory state: directory
mode: 0700 mode: "700"
- name: Install useful software - name: Create system user groups
ansible.builtin.apt: ansible.builtin.group:
name: "{{ packages }}" name: "{{ item.key }}"
gid: "{{ item.value.gid }}"
state: present state: present
update_cache: true loop: "{{ users | dict2items }}"
loop_control:
label: "{{ item.key }}"
when: users is defined
- name: Install the Uncomplicated Firewall - name: Create system users
ansible.builtin.apt: ansible.builtin.user:
name: ufw name: "{{ item.key }}"
state: present 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 - name: Include Debian-specific tasks
community.general.ufw: ansible.builtin.include_tasks: debian.yml
default: deny when: ansible_os_family == "Debian"
direction: incoming
- name: Allow outgoing traffic by default - name: Include Rocky Linux-specific tasks
community.general.ufw: ansible.builtin.include_tasks: rocky.yml
default: allow when: ansible_os_family == "RedHat"
direction: outgoing
- name: Allow OpenSSH with rate limiting
community.general.ufw:
name: ssh
rule: limit
- name: Enable firewall
community.general.ufw:
state: enabled

View File

@ -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

View File

@ -0,0 +1,4 @@
- name: Restart systemd-logind
ansible.builtin.systemd:
name: systemd-logind
state: restarted

View File

@ -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

View File

@ -1,5 +1,3 @@
version: '3.5'
volumes: volumes:
wordpress: wordpress:
nextcloud: nextcloud:
@ -10,7 +8,7 @@ networks:
services: services:
traefik: traefik:
image: traefik:${TRAEFIK_VERSION:-latest} image: ${TRAEFIK_IMAGE:-docker.io/library/traefik}:${TRAEFIK_VERSION:-latest}
restart: always restart: always
command: command:
- --api.dashboard=${TRAEFIK_DASHBOARD:-true} - --api.dashboard=${TRAEFIK_DASHBOARD:-true}
@ -20,7 +18,7 @@ services:
- --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false} - --providers.docker.exposedbydefault=${TRAEFIK_EXPOSED_DEFAULT:-false}
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443 - --entrypoints.websecure.address=:443
- --entrypoints.local.address=:8443 - --entrypoints.local.address=:9443
- --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true - --entrypoints.web.http.redirections.entrypoint.permanent=true
@ -33,9 +31,9 @@ services:
environment: environment:
DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY} DREAMHOST_API_KEY: ${TRAEFIK_DREAMHOST_APIKEY}
ports: ports:
- 80:80 - 8080:80
- 443:443 - 8443:443
- "127.0.0.1:8443:8443" - "127.0.0.1:9443:9443"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./.acme:/etc/letsencrypt - ./.acme:/etc/letsencrypt
@ -52,7 +50,7 @@ services:
- traefik - traefik
wordpress: wordpress:
image: wordpress:${WORDPRESS_VERSION:-latest} image: ${WORDPRESS_IMAGE:-docker.io/library/wordpress}:${WORDPRESS_VERSION:-latest}
restart: always restart: always
environment: environment:
WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal} WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST:-host.docker.internal}
@ -81,7 +79,7 @@ services:
- host.docker.internal:host-gateway - host.docker.internal:host-gateway
nextcloud: nextcloud:
image: nextcloud:${NEXTCLOUD_VERSION:-stable} image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud}:${NEXTCLOUD_VERSION:-stable}
restart: always restart: always
environment: environment:
MYSQL_HOST: ${NEXTCLOUD_MYSQL_HOST:-host.docker.internal:3306} MYSQL_HOST: ${NEXTCLOUD_MYSQL_HOST:-host.docker.internal:3306}

View File

@ -4,11 +4,10 @@
state: restarted state: restarted
listen: restart_mariadb listen: restart_mariadb
- name: Compose up on webserver stack - name: Start podman compose project
ansible.builtin.command: "docker-compose up -d" ansible.builtin.command:
args: cmd: podman compose up -d
chdir: "{{ webserver_root }}" chdir: /home/oci/webserver
listen: composeup_webserver
- name: Grab Nextcloud container information - name: Grab Nextcloud container information
community.docker.docker_container_info: community.docker.docker_container_info:
@ -22,11 +21,12 @@
port: 80 port: 80
listen: composeup_webserver listen: composeup_webserver
- name: Check Nextcloud status # - name: Check Nextcloud status
ansible.builtin.command: "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1 # ansible.builtin.command:
php occ status" # "docker exec --user www-data {{ webserver_root | basename }}_nextcloud_1
listen: composeup_webserver # php occ status"
register: nextcloud_status # listen: composeup_webserver
# register: nextcloud_status
- name: Import Nextcloud installation handlers - name: Import Nextcloud installation handlers
ansible.builtin.import_tasks: nextcloud.yml ansible.builtin.import_tasks: nextcloud.yml
@ -34,3 +34,15 @@
when: when:
- nextcloud_status.stderr[:26] == "Nextcloud is not installed" - nextcloud_status.stderr[:26] == "Nextcloud is not installed"
- nextcloud_autoinstall - 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

View File

@ -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

View File

@ -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

View File

@ -1,72 +1,7 @@
- name: Install MariaDB Server - name: Include Debian-specific tasks
ansible.builtin.apt: ansible.builtin.include_tasks: debian.yml
name: mariadb-server when: ansible_os_family == "Debian"
state: present
- name: Change the bind-address to allow Docker - name: Include Rocky Linux-specific tasks
ansible.builtin.lineinfile: ansible.builtin.include_tasks: rocky.yml
path: /etc/mysql/mariadb.conf.d/50-server.cnf when: ansible_os_family == "RedHat"
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"

View File

@ -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

View File

@ -3,5 +3,5 @@
become: true become: true
roles: roles:
- common - common
- docker - podman
- webserver - webserver