aboutsummaryrefslogtreecommitdiff
path: root/roles/host
diff options
context:
space:
mode:
authorRoman Ilin <me@romanilin.is>2026-06-15 12:59:09 +0300
committerRoman Ilin <me@romanilin.is>2026-06-15 22:04:41 +0300
commit5e4bf1268c266e63d0e92e845ad910a2103b86ff (patch)
tree532c01a9658a05048ef1ba76d4f30fca84005643 /roles/host
downloadinfrastructure-5e4bf1268c266e63d0e92e845ad910a2103b86ff.tar.gz
Diffstat (limited to 'roles/host')
-rw-r--r--roles/host/files/60-machinectl-fast-user-auth.rules6
-rw-r--r--roles/host/handlers/main.yaml25
-rw-r--r--roles/host/tasks/main.yaml195
-rw-r--r--roles/host/templates/nginx-reload.sh.j23
-rw-r--r--roles/host/templates/nginx.conf.j267
-rw-r--r--roles/host/templates/nspawn.j210
6 files changed, 306 insertions, 0 deletions
diff --git a/roles/host/files/60-machinectl-fast-user-auth.rules b/roles/host/files/60-machinectl-fast-user-auth.rules
new file mode 100644
index 0000000..eee39ea
--- /dev/null
+++ b/roles/host/files/60-machinectl-fast-user-auth.rules
@@ -0,0 +1,6 @@
+polkit.addRule(function(action, subject) {
+ if(action.id == "org.freedesktop.machine1.host-shell" &&
+ subject.isInGroup("wheel")) {
+ return polkit.Result.YES;
+ }
+});
diff --git a/roles/host/handlers/main.yaml b/roles/host/handlers/main.yaml
new file mode 100644
index 0000000..e643580
--- /dev/null
+++ b/roles/host/handlers/main.yaml
@@ -0,0 +1,25 @@
+- name: Reload Firewalld
+ ansible.builtin.systemd:
+ name: firewalld
+ state: reloaded
+
+- name: Restart Host systemd-networkd
+ ansible.builtin.systemd:
+ name: systemd-networkd
+ state: restarted
+
+- name: Restart Containers
+ ansible.builtin.systemd:
+ name: "systemd-nspawn@{{ item.key }}"
+ state: restarted
+ loop: "{{ containers | dict2items }}"
+
+- name: Restart Nginx
+ ansible.builtin.systemd:
+ name: nginx
+ state: restarted
+
+- name: Restart Polkit
+ ansible.builtin.systemd:
+ name: polkit
+ state: restarted
diff --git a/roles/host/tasks/main.yaml b/roles/host/tasks/main.yaml
new file mode 100644
index 0000000..007d44d
--- /dev/null
+++ b/roles/host/tasks/main.yaml
@@ -0,0 +1,195 @@
+- name: Install firewalld and systemd-networkd packages
+ ansible.builtin.dnf:
+ name:
+ - firewalld
+ - systemd-container
+ - systemd-networkd
+ - openssl
+ - python3-libsemanage
+ - policycoreutils
+ state: present
+
+- name: Configure Polkit to allow machinectl fast user auth
+ ansible.builtin.copy:
+ src: 60-machinectl-fast-user-auth.rules
+ dest: /etc/polkit-1/rules.d/60-machinectl-fast-user-auth.rules
+ mode: "0644"
+ notify: Restart Polkit
+
+- name: Ensure firewalld and systemd-networkd are running
+ ansible.builtin.systemd:
+ name: "{{ item }}"
+ state: started
+ enabled: yes
+ loop:
+ - firewalld
+ - systemd-networkd
+
+- name: Enable masquerading for container internet access
+ ansible.posix.firewalld:
+ masquerade: yes
+ state: enabled
+ permanent: yes
+ zone: public
+ notify: Reload Firewalld
+
+- name: Open HTTP and HTTPS (TCP) for Nginx
+ ansible.posix.firewalld:
+ service: "{{ item }}"
+ state: enabled
+ permanent: yes
+ zone: public
+ loop:
+ - http
+ - https
+ notify: Reload Firewalld
+
+- name: Open HTTPS (UDP) for HTTP/3 QUIC support
+ ansible.posix.firewalld:
+ port: 443/udp
+ state: enabled
+ permanent: yes
+ zone: public
+ notify: Reload Firewalld
+
+- name: Configure Host Firewall Port Forwarding dynamically
+ ansible.posix.firewalld:
+ port_forward:
+ - port: "{{ item.1 }}"
+ proto: tcp
+ toaddr: "{{ item.0.value.ip }}"
+ toport: "{{ item.1 }}"
+ permanent: yes
+ state: enabled
+ loop: "{{ containers | dict2items | subelements('value.forward_ports', skip_missing=True) }}"
+ notify: Reload Firewalld
+
+- name: Create bridge netdev on host
+ ansible.builtin.copy:
+ dest: /etc/systemd/network/10-nspawn-br0.netdev
+ content: |
+ [NetDev]
+ Name=nspawn-br0
+ Kind=bridge
+ notify: Restart Host systemd-networkd
+
+- name: Assign IP to the host bridge
+ ansible.builtin.copy:
+ dest: /etc/systemd/network/10-nspawn-br0.network
+ content: |
+ [Match]
+ Name=nspawn-br0
+ [Network]
+ Address={{ bridge_ip }}/24
+ notify: Restart Host systemd-networkd
+
+- name: Apply networking changes immediately
+ ansible.builtin.meta: flush_handlers
+
+- name: Ensure containers are bootstrapped (AlmaLinux 10)
+ ansible.builtin.dnf:
+ installroot: "/var/lib/machines/{{ item.key }}"
+ releasever: "10"
+ name:
+ - almalinux-release
+ - systemd
+ - dbus
+ - systemd-networkd
+ - passwd
+ - dnf
+ - iproute
+ state: present
+ loop: "{{ containers | dict2items }}"
+
+- name: Initialize machine-id for containers
+ ansible.builtin.command: systemd-machine-id-setup --root=/var/lib/machines/{{ item.key }}
+ args:
+ creates: "/var/lib/machines/{{ item.key }}/etc/machine-id"
+ loop: "{{ containers | dict2items }}"
+
+- name: Fix SELinux contexts in container rootfs
+ ansible.builtin.command: restorecon -R /var/lib/machines/{{ item.key }}
+ register: restorecon_result
+ changed_when: "'Relabeled' in restorecon_result.stdout"
+ loop: "{{ containers | dict2items }}"
+
+- name: Ensure systemd-nspawn directory exists
+ ansible.builtin.file:
+ path: /etc/systemd/nspawn
+ state: directory
+ mode: "0755"
+
+- name: Ensure Let's Encrypt mock directories exist
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: directory
+ mode: '0755'
+ loop:
+ - "/etc/letsencrypt/archive/{{ vault_public_domain }}"
+ - "/etc/letsencrypt/live/{{ vault_public_domain }}"
+
+- name: Check if Let's Encrypt certificate exists
+ ansible.builtin.stat:
+ path: "/etc/letsencrypt/live/{{ vault_public_domain }}/fullchain.pem"
+ register: le_cert
+
+- name: Generate self-signed fallback cert in Let's Encrypt paths if missing
+ ansible.builtin.command: >
+ openssl req -x509 -nodes -days 365
+ -newkey ec -pkeyopt ec_paramgen_curve:prime256v1
+ -keyout /etc/letsencrypt/live/{{ vault_public_domain }}/privkey.pem
+ -out /etc/letsencrypt/live/{{ vault_public_domain }}/fullchain.pem
+ -subj "/CN={{ vault_public_domain }}"
+ args:
+ creates: "/etc/letsencrypt/live/{{ vault_public_domain }}/fullchain.pem"
+ when: not le_cert.stat.exists
+
+- name: Create systemd-nspawn configuration
+ ansible.builtin.template:
+ src: nspawn.j2
+ dest: "/etc/systemd/nspawn/{{ item.key }}.nspawn"
+ loop: "{{ containers | dict2items }}"
+ notify: Restart Containers
+
+- name: Enable and Start Containers
+ ansible.builtin.systemd:
+ name: "systemd-nspawn@{{ item.key }}"
+ state: started
+ enabled: yes
+ loop: "{{ containers | dict2items }}"
+
+- name: Install Nginx
+ ansible.builtin.dnf:
+ name: nginx
+ state: present
+
+- name: Allow Nginx to connect to upstream servers (SELinux)
+ ansible.posix.seboolean:
+ name: httpd_can_network_connect
+ state: yes
+ persistent: yes
+
+- name: Configure Nginx dynamically
+ ansible.builtin.template:
+ src: nginx.conf.j2
+ dest: /etc/nginx/nginx.conf
+ validate: nginx -t -c %s
+ notify: Restart Nginx
+
+- name: Ensure Nginx is enabled and running
+ ansible.builtin.systemd:
+ name: nginx
+ state: started
+ enabled: yes
+
+- name: Ensure Let's Encrypt renewal hook directory exists
+ ansible.builtin.file:
+ path: /etc/letsencrypt/renewal-hooks/post
+ state: directory
+ mode: '0755'
+
+- name: Deploy Let's Encrypt post-renewal hook for Nginx
+ ansible.builtin.template:
+ src: nginx-reload.sh.j2
+ dest: /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh
+ mode: '0755'
diff --git a/roles/host/templates/nginx-reload.sh.j2 b/roles/host/templates/nginx-reload.sh.j2
new file mode 100644
index 0000000..f248776
--- /dev/null
+++ b/roles/host/templates/nginx-reload.sh.j2
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+systemctl reload nginx
diff --git a/roles/host/templates/nginx.conf.j2 b/roles/host/templates/nginx.conf.j2
new file mode 100644
index 0000000..7360cae
--- /dev/null
+++ b/roles/host/templates/nginx.conf.j2
@@ -0,0 +1,67 @@
+user nginx;
+worker_processes auto;
+worker_rlimit_nofile 8192;
+error_log /var/log/nginx/error.log notice;
+pid /run/nginx.pid;
+
+events {
+ worker_connections 4096;
+}
+
+http {
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ tcp_nopush on;
+ keepalive_timeout 65;
+ types_hash_max_size 4096;
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # Modern SSL configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 1d;
+ ssl_session_tickets off;
+
+ # Redirect all HTTP traffic to HTTPS
+ server {
+ listen 80 default_server;
+ server_name _;
+ return 301 https://$host$request_uri;
+ }
+
+{% for name, config in containers.items() %}
+{% if config.web_subdomain is defined %}
+ server {
+ listen 443 ssl; # TCP for HTTP/1.1 & HTTP/2
+ listen 443 quic; # UDP for HTTP/3 QUIC
+ http2 on; # Enable HTTP/2 over TCP
+
+ server_name {{ config.web_subdomain }}.{{ vault_public_domain }};
+
+ # Nginx reads them natively, no combining needed
+ ssl_certificate /etc/letsencrypt/live/{{ vault_public_domain }}/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/{{ vault_public_domain }}/privkey.pem;
+
+ # Advertise HTTP/3 availability to browsers
+ add_header Alt-Svc 'h3=":443"; ma=2592000' always;
+
+ location / {
+ proxy_pass http://{{ config.ip }}:80;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+ }
+{% endif %}
+{% endfor %}
+}
diff --git a/roles/host/templates/nspawn.j2 b/roles/host/templates/nspawn.j2
new file mode 100644
index 0000000..15faf53
--- /dev/null
+++ b/roles/host/templates/nspawn.j2
@@ -0,0 +1,10 @@
+[Exec]
+Boot=yes
+Hostname={{ item.key }}.{{ vault_public_domain }}
+
+[Files]
+BindReadOnly=/etc/letsencrypt/live/{{ vault_public_domain }}:/etc/letsencrypt/live/{{ vault_public_domain }}
+BindReadOnly=/etc/letsencrypt/archive/{{ vault_public_domain }}:/etc/letsencrypt/archive/{{ vault_public_domain }}
+
+[Network]
+Bridge=nspawn-br0