From 6e814d2827737bfda84902a00a84d02e097c1076 Mon Sep 17 00:00:00 2001 From: Mike Teehan Date: Tue, 16 Jun 2026 03:36:55 +0000 Subject: [PATCH] refactor: update system config, sudoers, and install script --- install.sh | 399 ++++++++-------------------- system/acme-deploy.py | 3 +- system/dnsmasq.conf | 6 +- system/nginx/server_block.conf | 67 ++--- system/sudoers.d/vacuum-walld | 3 + system/systemd/vacuum-walld.service | 2 +- 6 files changed, 162 insertions(+), 318 deletions(-) diff --git a/install.sh b/install.sh index deeebc7..2870c2f 100755 --- a/install.sh +++ b/install.sh @@ -286,171 +286,13 @@ render_template "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" \ install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.timer" /etc/systemd/system/vacuum-wall-acme.timer systemctl daemon-reload -# --- 7. Enable IP forwarding --- +# --- 7. Enable IP forwarding (persistent via sysctl.conf) --- log "Enabling IP forwarding..." if ! grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf fi -sysctl -w net.ipv4.ip_forward=1 2>/dev/null || warn "Could not enable IP forwarding (may need kernel access)" -# --- 8. Start and configure firewalld --- -log "Enabling firewalld..." -systemctl enable firewalld >/dev/null 2>&1 || warn "Could not enable firewalld (already running?)" -systemctl start firewalld >/dev/null 2>&1 || warn "Could not start firewalld (may need D-Bus)" - -firewall-cmd --permanent --add-service=http >/dev/null 2>&1 && \ - log "Added service http to public zone" || \ - warn "Could not add service http to public zone (already exists?)" -firewall-cmd --permanent --add-service=https >/dev/null 2>&1 && \ - log "Added service https to public zone" || \ - warn "Could not add service https to public zone (already exists?)" -firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 && \ - log "Added service ssh to public zone" || \ - warn "Could not add service ssh to public zone (already exists?)" -firewall-cmd --reload >/dev/null 2>&1 && \ - log "Firewalld rules reloaded" || \ - warn "Could not reload firewalld rules" - -# --- 9. Configure dnsmasq --- -log "Configuring dnsmasq..." -systemctl enable dnsmasq >/dev/null 2>&1 && log "Enabled dnsmasq" || warn "Could not enable dnsmasq" -systemctl start dnsmasq >/dev/null 2>&1 || warn "Could not start dnsmasq (no interfaces configured yet)" -log "dnsmasq configured (will fully start after DHCP ranges are set)" - -# --- 10. Setup nginx management proxy --- -mkdir -p "$ACME_HOME/$DOMAIN" - -if [[ -f "$ACME_HOME/$DOMAIN/$DOMAIN.key" ]]; then - log "SSL certificate already exists for $DOMAIN, skipping." -else - log "Generating self-signed certificate for management domain..." - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout "$ACME_HOME/$DOMAIN/$DOMAIN.key" \ - -out "$ACME_HOME/$DOMAIN/fullchain.cer" \ - -subj "/CN=$DOMAIN" \ - -addext "subjectAltName=DNS:$DOMAIN" -fi - -chown -R "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME" - -# Generate/update htpasswd directly in data/nginx/ -HTPASSWD_FILE="${PROJECT_DIR}/data/nginx/.htpasswd" -if [[ -f "$HTPASSWD_FILE" ]]; then - htpasswd -b "$HTPASSWD_FILE" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \ - warn "Could not update htpasswd (install apache2-utils)" -else - htpasswd -cb "$HTPASSWD_FILE" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \ - MGMT_USER="$MGMT_USER" MGMT_PASS="$MGMT_PASS" HTFILE="$HTPASSWD_FILE" python3 -c " -import os, crypt, base64 -password = os.environ['MGMT_PASS'] -user = os.environ['MGMT_USER'] -salt = '\$6\$' + base64.b64encode(os.urandom(16)).decode().rstrip('=')[:16] -hashed = crypt.crypt(password, salt) -with open(os.environ['HTFILE'], 'w') as f: - f.write(user + ':' + hashed + '\n') -" 2>/dev/null || \ - warn "Could not generate htpasswd (install apache2-utils or python3-crypt)" -fi - -chown "$USER_NAME:$USER_GROUP" "${PROJECT_DIR}/data/nginx/.htpasswd" - -# Remove default nginx site so vacuum-wall management config takes precedence -rm -f /etc/nginx/sites-enabled/default - -# Write initial management proxy config directly to /etc/nginx/conf.d/. -# This bootstrap config is needed before the WebUI is running. Once the -# WebUI is up, it manages proxy configs from config/nginx/config.json -# and renders them to data/nginx/sites-enabled/. -# Write WebSocket upgrade map (nginx conf.d/ is already inside http {} context) -cat > /etc/nginx/conf.d/vacuum-wall-map.conf <<'MAPEOF' -# Vacuum Wall - WebSocket upgrade map -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} -MAPEOF - -# Write the management site block (conf.d/ is inside http {}, no extra http {} needed) -cat > /etc/nginx/conf.d/vacuum-wall-mgmt.conf </dev/null 2>&1 && log "Enabled firewalld" || warn "Could not enable firewalld" +systemctl enable nginx >/dev/null 2>&1 && log "Enabled nginx" || warn "Could not enable nginx" +systemctl enable dnsmasq >/dev/null 2>&1 && log "Enabled dnsmasq" || warn "Could not enable dnsmasq" +systemctl enable vacuum-walld >/dev/null 2>&1 && log "Enabled vacuum-walld" || warn "Could not enable vacuum-walld" +systemctl enable vacuum-wall >/dev/null 2>&1 && log "Enabled vacuum-wall" || warn "Could not enable vacuum-wall" +systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 && log "Enabled vacuum-wall-acme.timer" || warn "Could not enable vacuum-wall-acme.timer" +systemctl enable avahi-daemon >/dev/null 2>&1 && log "Enabled avahi-daemon" || warn "Could not enable avahi-daemon" + +# Clean up old nginx bootstrap configs (replaced by daemon-generated config) +rm -f /etc/nginx/conf.d/vacuum-wall-map.conf /etc/nginx/conf.d/vacuum-wall-mgmt.conf + +# Stop all services to ensure clean start order +systemctl stop vacuum-wall >/dev/null 2>&1 || true +systemctl stop vacuum-walld >/dev/null 2>&1 || true + +# Start services in dependency order +systemctl start firewalld >/dev/null 2>&1 && log "Started firewalld" || warn "Could not start firewalld" +systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start avahi-daemon" +systemctl start dnsmasq >/dev/null 2>&1 || warn "Could not start dnsmasq (no interfaces configured yet)" + +# Start daemon and wait for socket +systemctl start vacuum-walld >/dev/null 2>&1 && log "Started vacuum-walld daemon" || warn "Could not start vacuum-walld daemon" + +_SOCKET="$PROJECT_DIR/data/daemon.sock" +for _i in $(seq 1 30); do + [[ -S "$_SOCKET" ]] && break + sleep 0.5 +done +if [[ ! -S "$_SOCKET" ]]; then + warn "Daemon socket not found at $_SOCKET — skipping API configuration" else - log "Writing initial firewall configuration..." + chown "$USER_DAEMON_NAME:$USER_GROUP" "$_SOCKET" 2>/dev/null || true + chmod 0660 "$_SOCKET" 2>/dev/null || true + + # --- 10. Configure subsystems via daemon API --- + log "Configuring subsystems via daemon API..." WAN_IFACE="$WAN_IFACE" \ LAN_IFACES="$LAN_IFACES" \ - INSTALL_DIR="$PROJECT_DIR" \ + MGMT_DOMAIN="$DOMAIN" \ + MGMT_USER="$MGMT_USER" \ + MGMT_PASS="$MGMT_PASS" \ + ACME_EMAIL="$ACME_EMAIL" \ "${PROJECT_DIR}/.venv/bin/python3" -c " -import json, os +import daemon.client as c +from daemon.iface import ( + POST_ACME_SELF_SIGNED, POST_NGINX_MANAGEMENT, POST_NGINX_APPLY, + POST_FIREWALL_CONFIG, POST_FIREWALL_CONFIG_APPLY, POST_NETWORK_SYSCTL_SET, + POST_ACME_EMAIL, GET_NETWORK_INFER_DHCP_RANGES, +) +import sys -wan = os.environ.get('WAN_IFACE', '').strip() or None -lan = os.environ.get('LAN_IFACES', '').strip() or None -p = os.environ['INSTALL_DIR'] +domain = '${DOMAIN}' +mgmt_user = '${MGMT_USER}' +mgmt_pass = '${MGMT_PASS}' +acme_email = '${ACME_EMAIL}' +wan_iface = '${WAN_IFACE}' +lan_ifaces = '${LAN_IFACES}' -cfg = {'zones': {}} +# Self-signed cert for management domain +try: + res = c.post(POST_ACME_SELF_SIGNED, {'domain': domain, 'days': 365}) + print(f' [cert] Self-signed: {\"generated\" if res.get(\"generated\") else \"exists\"}') +except Exception as e: + print(f' [cert] Warning: {e}', file=sys.stderr) -if wan: - cfg['zones']['public'] = { +# Management proxy + htpasswd +try: + c.post(POST_NGINX_MANAGEMENT, { + 'domain': domain, + 'flask_host': '127.0.0.1', + 'flask_port': 9090, + 'auth_user': mgmt_user, + 'auth_pass': mgmt_pass, + }) + c.post(POST_NGINX_APPLY) + print(f' [proxy] Management proxy configured for {domain}') +except Exception as e: + print(f' [proxy] Warning: {e}', file=sys.stderr) + +# Firewall config (interface detection done in bash above) +import json as _json +zones = {} + +if wan_iface: + zones['public'] = { 'target': 'DEFAULT', - 'interfaces': [i for i in wan.split(',') if i], + 'interfaces': [i for i in wan_iface.split(',') if i], 'services': ['http', 'https', 'ssh'], 'masquerade': True, } -if lan: - cfg['zones']['internal'] = { +if lan_ifaces: + zones['internal'] = { 'target': 'ACCEPT', - 'interfaces': [i for i in lan.split(',') if i], + 'interfaces': [i for i in lan_ifaces.split(',') if i], 'services': ['dhcp', 'dns', 'ntp'], 'masquerade': False, } # Always create vpn zone skeleton for later WireGuard setup -cfg['zones']['vpn'] = { +zones['vpn'] = { 'target': 'ACCEPT', 'interfaces': [], 'services': [], 'masquerade': False, } -with open(os.path.join(p, 'config/firewall/config.json'), 'w') as f: - json.dump(cfg, f, indent=2) - f.write('\n') +try: + c.post(POST_FIREWALL_CONFIG, {'zones': zones}) + c.post(POST_FIREWALL_CONFIG_APPLY) + print(' [firewall] Zones configured and applied') +except Exception as e: + print(f' [firewall] Warning: {e}', file=sys.stderr) + +# IP forwarding +try: + c.post(POST_NETWORK_SYSCTL_SET, {'name': 'net.ipv4.ip_forward', 'value': '1'}) + print(' [network] IP forwarding enabled') +except Exception as e: + print(f' [network] Warning: {e}', file=sys.stderr) + +# ACME email (optional) +if acme_email: + try: + c.post(POST_ACME_EMAIL, {'email': acme_email}) + print(f' [acme] Email set to {acme_email}') + except Exception as e: + print(f' [acme] Warning: {e}', file=sys.stderr) + +# Infer DHCP ranges (logged for user reference) +try: + ranges = c.get(GET_NETWORK_INFER_DHCP_RANGES) + for iface, rng in ranges.get('ranges', {}).items(): + print(f' [suggestion] DHCP range for {iface}: {rng.get(\"start\")}-{rng.get(\"end\")}') +except Exception: + pass " -fi - -# Apply zones via firewall-cmd (Python venv not yet fully available for apply_config) -firewall-cmd --permanent --new-zone=internal >/dev/null 2>&1 && \ - log "Created firewalld zone: internal" || \ - warn "firewalld zone 'internal' may already exist" -firewall-cmd --permanent --zone=internal --set-target=ACCEPT >/dev/null 2>&1 || \ - warn "Could not set target ACCEPT on internal zone" -firewall-cmd --permanent --zone=internal --add-service=dhcp >/dev/null 2>&1 && \ - log "Added service dhcp to internal zone" || \ - warn "Could not add service dhcp to internal zone" -firewall-cmd --permanent --zone=internal --add-service=dns >/dev/null 2>&1 && \ - log "Added service dns to internal zone" || \ - warn "Could not add service dns to internal zone" -firewall-cmd --permanent --zone=internal --add-service=ntp >/dev/null 2>&1 && \ - log "Added service ntp to internal zone" || \ - warn "Could not add service ntp to internal zone" - -firewall-cmd --permanent --new-zone=vpn >/dev/null 2>&1 && \ - log "Created firewalld zone: vpn" || \ - warn "firewalld zone 'vpn' may already exist" -firewall-cmd --permanent --zone=vpn --set-target=ACCEPT >/dev/null 2>&1 || \ - warn "Could not set target ACCEPT on vpn zone" - -# Apply masquerade on public/WAN -if [[ -n "$WAN_IFACE" ]]; then - firewall-cmd --permanent --zone=public --add-masquerade >/dev/null 2>&1 && \ - log "Enabled masquerade on public zone ($WAN_IFACE)" || \ - warn "Could not enable masquerade on public zone" - firewall-cmd --permanent --zone=public --add-interface="$WAN_IFACE" >/dev/null 2>&1 && \ - log "Assigned $WAN_IFACE to public zone" || \ - warn "Could not assign $WAN_IFACE to public zone" -fi - -# Assign LAN interfaces to internal zone -if [[ -n "$LAN_IFACES" ]]; then - IFS=',' read -ra LAN_ARRAY <<< "$LAN_IFACES" - for iface in "${LAN_ARRAY[@]}"; do - iface=$(echo "$iface" | xargs) - [[ -z "$iface" ]] && continue - firewall-cmd --permanent --zone=internal --add-interface="$iface" >/dev/null 2>&1 && \ - log "Assigned $iface to internal zone" || \ - warn "Could not assign $iface to internal zone" - done -fi - -firewall-cmd --reload >/dev/null 2>&1 && \ - log "Firewalld rules reloaded" || \ - warn "Could not reload firewalld rules" - -# --- 13. Enable and start services --- -log "Enabling services..." -systemctl enable nginx >/dev/null 2>&1 && log "Enabled nginx" || warn "Could not enable nginx" -systemctl enable vacuum-walld >/dev/null 2>&1 && log "Enabled vacuum-walld" || warn "Could not enable vacuum-walld" -systemctl enable vacuum-wall >/dev/null 2>&1 && log "Enabled vacuum-wall" || warn "Could not enable vacuum-wall" -systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 && log "Enabled vacuum-wall-acme.timer" || warn "Could not enable vacuum-wall-acme.timer" - -systemctl enable avahi-daemon >/dev/null 2>&1 && log "Enabled avahi-daemon" || warn "Could not enable avahi-daemon" -systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start avahi-daemon" - -# Start daemon first, then web UI -# Stop vacuum-wall first. vacuum-wall.service Requires=vacuum-walld.service, -# so stopping vacuum-wall triggers a cascade stop of vacuum-walld. The explicit -# stop of vacuum-walld below is redundant but ensures clean teardown. -systemctl stop vacuum-wall >/dev/null 2>&1 || true -systemctl stop vacuum-walld >/dev/null 2>&1 || true -systemctl start vacuum-walld >/dev/null 2>&1 && log "Started vacuum-walld daemon" || warn "Could not start vacuum-walld daemon" - -# Wait for daemon socket -_SOCKET="$PROJECT_DIR/data/daemon.sock" -for _i in $(seq 1 10); do - [[ -S "$_SOCKET" ]] && break - sleep 0.5 -done -if [[ ! -S "$_SOCKET" ]]; then - warn "Daemon socket not found at $_SOCKET" -fi - -# Set socket ownership so web UI user can connect -if [[ -S "$_SOCKET" ]]; then - chown "$USER_DAEMON_NAME:$USER_GROUP" "$_SOCKET" 2>/dev/null || true - chmod 0660 "$_SOCKET" 2>/dev/null || true + log "Subsystem configuration complete" fi systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI" @@ -615,25 +469,6 @@ nginx -t 2>/dev/null && nginx -s reload 2>/dev/null && log "Reloaded nginx" || \ systemctl restart nginx >/dev/null 2>&1 && log "Restarted nginx" || \ warn "Could not restart nginx (check config)" -# --- 14. Write initial ACME config (skip if user has customized it) --- -ACME_CFG="${PROJECT_DIR}/config/acme/config.json" -if [[ -f "$ACME_CFG" ]]; then - log "ACME config already exists, skipping." -else - log "Writing initial ACME configuration..." - mkdir -p "${PROJECT_DIR}/config/acme" - ACME_EMAIL="$ACME_EMAIL" \ - ACME_CFG="$ACME_CFG" \ - "${PROJECT_DIR}/.venv/bin/python3" -c " -import json, os -cfg = {'email': os.environ.get('ACME_EMAIL', '') or ''} -with open(os.environ['ACME_CFG'], 'w') as f: - json.dump(cfg, f, indent=4) - f.write('\n') -" - log "ACME config written (register via WebUI to activate)" -fi - # --- Done --- echo "" echo "============================================" diff --git a/system/acme-deploy.py b/system/acme-deploy.py index d8c31ae..711aa64 100644 --- a/system/acme-deploy.py +++ b/system/acme-deploy.py @@ -13,6 +13,7 @@ try: import requests_unixsocket from daemon.client import post + from daemon.iface import POST_NGINX_RELOAD logging.basicConfig(level=logging.INFO) project_dir = os.environ.get("INSTALL_DIR", os.path.dirname(os.path.dirname(__file__))) @@ -20,7 +21,7 @@ try: "VACUUM_WALLD_SOCKET", os.path.join(project_dir, "data", "daemon.sock"), ) - post("/nginx/reload", socket_path=socket_path) + post(POST_NGINX_RELOAD, socket_path=socket_path) sys.exit(0) except Exception as exc: logging.error("acme-deploy hook failed: %s", exc) diff --git a/system/dnsmasq.conf b/system/dnsmasq.conf index c290ced..3436a6b 100644 --- a/system/dnsmasq.conf +++ b/system/dnsmasq.conf @@ -1,9 +1,13 @@ # ---- vacuum-wall managed dnsmasq configuration ---- # generated {{ timestamp }} +bind-interfaces {% if interfaces %} interface={{ interfaces | join(',') }} -bind-interfaces +{% elif listen_addresses %} +{% for addr in listen_addresses %} +listen-address={{ addr }} +{% endfor %} {% endif %} {% for srv in dns.upstreams %} server={{ srv }} diff --git a/system/nginx/server_block.conf b/system/nginx/server_block.conf index 4702200..06ff1ef 100644 --- a/system/nginx/server_block.conf +++ b/system/nginx/server_block.conf @@ -39,8 +39,8 @@ server { {% endif %} {% elif is_management %} - ssl_certificate {{ certs_dir }}/{{ domain }}.crt; - ssl_certificate_key {{ certs_dir }}/{{ domain }}.key; + ssl_certificate {{ acme_home }}/{{ domain }}/fullchain.cer; + ssl_certificate_key {{ acme_home }}/{{ domain }}/{{ domain }}.key; {% endif %} # Shared SSL settings @@ -57,48 +57,49 @@ server { add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # Proxy headers - 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 $scheme; +{% endif %} + location / { + # Proxy headers + 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 $scheme; +{% if not is_management %} {% for hname, hval in headers.items() %} - proxy_set_header {{ hname }} {{ hval }}; + proxy_set_header {{ hname }} {{ hval }}; {% endfor %} {% endif %} - # Proxy pass to backend - proxy_pass {{ backend.proto }}://{{ backend.host }}:{{ backend.port }}; - proxy_http_version 1.1; + # Proxy pass to backend + proxy_pass {{ backend.proto }}://{{ backend.host }}:{{ backend.port }}; + proxy_http_version 1.1; + + # Timeouts + proxy_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + proxy_buffering off; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } +{% if is_management %} + location /ws { + auth_basic off; + proxy_pass http://127.0.0.1:9091; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +{% endif %} {% if not is_management %} - # Timeouts - proxy_connect_timeout 30s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - proxy_buffering off; - # Access / error logs access_log /var/log/nginx/{{ domain }}_access.log; error_log /var/log/nginx/{{ domain }}_error.log warn; {% else %} - 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 $scheme; - - proxy_connect_timeout 30s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - proxy_buffering off; - access_log /var/log/nginx/wall_mgmt_access.log; error_log /var/log/nginx/wall_mgmt_error.log warn; {% endif %} - location / { - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } } diff --git a/system/sudoers.d/vacuum-walld b/system/sudoers.d/vacuum-walld index 1b51cd8..b1400c7 100644 --- a/system/sudoers.d/vacuum-walld +++ b/system/sudoers.d/vacuum-walld @@ -41,6 +41,9 @@ Defaults:{{ USER_DAEMON_NAME }} secure_path="/usr/local/sbin:/usr/local/bin:/usr {{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/rm /etc/systemd/network/*.network {{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/systemd/network +# Sysctl +{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/sbin/sysctl -w * + # Misc {{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n * {{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/* diff --git a/system/systemd/vacuum-walld.service b/system/systemd/vacuum-walld.service index ef15459..bef7d42 100644 --- a/system/systemd/vacuum-walld.service +++ b/system/systemd/vacuum-walld.service @@ -19,7 +19,7 @@ Environment=HOME={{ PROJECT_DIR }} # Security hardening ProtectSystem=strict -ReadWritePaths={{ PROJECT_DIR }} /tmp /etc/systemd/network +ReadWritePaths={{ PROJECT_DIR }} /tmp /etc/systemd/network /etc/nginx /etc/dnsmasq.d /etc/wireguard /run/sudo /run/firewalld PrivateTmp=yes ProtectKernelTunables=yes ProtectKernelModules=yes