Initial commit: SSL proxy / firewall appliance
Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP, WireGuard, and ACME certificate management.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# ---- vacuum-wall managed dnsmasq configuration ----
|
||||
# generated {{ timestamp }}
|
||||
|
||||
{% if interfaces %}
|
||||
interface={{ interfaces | join(',') }}
|
||||
bind-interfaces
|
||||
{% endif %}
|
||||
{% for srv in dns.upstreams %}
|
||||
server={{ srv }}
|
||||
{% else %}
|
||||
no-resolv
|
||||
{% endfor %}
|
||||
{% if dns.domain %}
|
||||
domain={{ dns.domain }}
|
||||
expand-hosts
|
||||
{% endif %}
|
||||
{% for rng in dhcp.ranges %}
|
||||
{% if rng.interface %}
|
||||
dhcp-range=set:{{ rng.interface }},{{ rng.start }},{{ rng.end }},{{ rng.lease_time }}
|
||||
{% else %}
|
||||
dhcp-range={{ rng.start }},{{ rng.end }},{{ rng.lease_time }}
|
||||
{% endif %}
|
||||
{% if rng.gateway %}
|
||||
dhcp-option=tag:{{ rng.interface }},3,{{ rng.gateway }}
|
||||
{% endif %}
|
||||
{% if rng.dns %}
|
||||
dhcp-option=tag:{{ rng.interface }},6,{{ rng.dns }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for lease in dhcp.static_leases %}
|
||||
{% if lease.hostname %}
|
||||
dhcp-host={{ lease.mac }},{{ lease.ip }},{{ lease.hostname }}
|
||||
{% else %}
|
||||
dhcp-host={{ lease.mac }},{{ lease.ip }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for rec in dns.custom_records %}
|
||||
{% if rec.hostname %}
|
||||
host-record={{ rec.name }},{{ rec.address }}
|
||||
addr/{{ rec.name }}/{{ rec.address }}
|
||||
{% else %}
|
||||
addr/{{ rec.name }}/{{ rec.address }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if fragments_dir %}
|
||||
conf-dir={{ fragments_dir }},optional
|
||||
{% endif %}# ---- end vacuum-wall config ----
|
||||
@@ -0,0 +1,11 @@
|
||||
# Vacuum Wall — auto-generated include file
|
||||
# Regenerated on every config change — do not edit manually
|
||||
|
||||
# WebSocket upgrade mapping
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Domain server blocks
|
||||
include {{ sites_glob }};
|
||||
@@ -0,0 +1,99 @@
|
||||
# Auto-generated by Vacuum Wall — do not edit manually
|
||||
# Domain: {{ domain }}
|
||||
|
||||
{% if force_ssl %}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name {{ domain }};
|
||||
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name {{ domain }};
|
||||
|
||||
{% if cert %}
|
||||
{% if cert.type == "acme" %}
|
||||
# Certificate managed by acme.sh
|
||||
{% if cert.email %} # ACME contact: {{ cert.email }}
|
||||
{% endif %} ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem;
|
||||
|
||||
{% elif cert.type == "file" %}
|
||||
ssl_certificate {{ cert.path }};
|
||||
ssl_certificate_key {{ cert.key_path }};
|
||||
|
||||
{% elif cert.type == "selfsigned" %}
|
||||
ssl_certificate /home/wall/vacuum-wall/data/certs/{{ domain }}.crt;
|
||||
ssl_certificate_key /home/wall/vacuum-wall/data/certs/{{ domain }}.key;
|
||||
|
||||
{% endif %}
|
||||
{% elif is_management %}
|
||||
ssl_certificate /home/wall/vacuum-wall/data/certs/{{ domain }}.crt;
|
||||
ssl_certificate_key /home/wall/vacuum-wall/data/certs/{{ domain }}.key;
|
||||
|
||||
{% endif %}
|
||||
# Shared SSL settings
|
||||
include snippets/vacuum-wall-ssl.conf;
|
||||
|
||||
{% if auth %}
|
||||
# HTTP basic authentication
|
||||
auth_basic "{{ "Vacuum Wall" if is_management else "Restricted" }}";
|
||||
auth_basic_user_file {{ auth.htpasswd }};
|
||||
|
||||
{% endif %}
|
||||
{% if not is_management %}
|
||||
# Security hardening headers
|
||||
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 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;
|
||||
{% for hname, hval in headers.items() %}
|
||||
proxy_set_header {{ hname }} {{ hval }};
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
# Proxy pass to backend
|
||||
proxy_pass {{ backend.proto }}://{{ backend.host }}:{{ backend.port }};
|
||||
proxy_http_version 1.1;
|
||||
|
||||
{% 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Vacuum Wall — shared SSL settings
|
||||
# Regenerated automatically — do not edit manually
|
||||
|
||||
ssl_protocols {{ ssl.protocols }};
|
||||
ssl_prefer_server_ciphers {{ "on" if ssl.prefer_server_ciphers else "off" }};
|
||||
ssl_ciphers {{ ssl.ciphers }};
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:TLS:10m;
|
||||
ssl_session_tickets off;
|
||||
@@ -0,0 +1,37 @@
|
||||
# Defaults directives
|
||||
Defaults:vacuum-wall !requiretty
|
||||
Defaults:vacuum-wall secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# Firewall management
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/firewall-cmd *
|
||||
|
||||
# Nginx management
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/sbin/nginx -t
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/conf.d/
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/nginx/snippets/
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/conf.d/vacuum-wall.conf
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/rm /etc/nginx/snippets/vacuum-wall-ssl.conf
|
||||
|
||||
# Dnsmasq management
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/systemctl reload dnsmasq
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/systemctl is-active dnsmasq
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/dnsmasq.d/
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cat /var/lib/dnsmasq/dnsmasq.leases
|
||||
|
||||
# WireGuard management
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/wg-quick *
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/wg *
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cp -- * /etc/wireguard/
|
||||
|
||||
# Acme.sh (SSL cert management)
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/bash ~/.acme.sh/acme.sh *
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/local/bin/acme.sh *
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cat /home/*/.acme.sh/*
|
||||
|
||||
# Misc
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/dnsmasq.d
|
||||
vacuum-wall ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/wireguard
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Vacuum Wall ACME Certificate Renewal
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=vacuum-wall
|
||||
WorkingDirectory=/home/wall/vacuum-wall
|
||||
ExecStart=/usr/local/bin/acme.sh --cron --home /home/vacuum-wall/.acme.sh
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Vacuum Wall ACME Certificate Renewal Timer
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 00:00:00
|
||||
OnCalendar=*-*-* 12:00:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,36 @@
|
||||
[Unit]
|
||||
Description=Vacuum Wall Management WebUI
|
||||
Documentation=https://github.com/wall/vacuum-wall
|
||||
After=network.target firewalld.service nginx.service dnsmasq.service
|
||||
Wants=firewalld.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=vacuum-wall
|
||||
Group=vacuum-wall
|
||||
WorkingDirectory=/home/wall/vacuum-wall
|
||||
ExecStart=/home/wall/vacuum-wall/.venv/bin/python webui/server.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=PATH=/usr/local/bin:/usr/bin
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/wall/vacuum-wall/data /tmp
|
||||
PrivateTmp=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictRealtime=yes
|
||||
LockPersonality=yes
|
||||
|
||||
# Network - only loopback (nginx proxies to us)
|
||||
IPAddressDeny=all
|
||||
IPAddressAllow=localhost
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
# vacuum-wall client config for {{ peer_name }} — generated {{ timestamp }}
|
||||
|
||||
[Interface]
|
||||
PrivateKey = {{ client_priv }}
|
||||
Address = {{ client_addr }}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{ server_pubkey }}
|
||||
Endpoint = {{ server_endpoint }}
|
||||
AllowedIPs = {{ allowed_ips | join(',') }}
|
||||
{% if preshared_key %}
|
||||
PresharedKey = {{ preshared_key }}
|
||||
{% endif %}
|
||||
{% if persistent_keepalive is not none %}
|
||||
PersistentKeepalive = {{ persistent_keepalive }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Auto-generated by vacuum-wall at {{ timestamp }}
|
||||
|
||||
[Interface]
|
||||
PrivateKey = {{ interface.private_key }}
|
||||
Address = {{ interface.addresses | join(',') }}
|
||||
ListenPort = {{ interface.listen_port }}
|
||||
{% if interface.post_up %}
|
||||
PostUp = {{ interface.post_up }}
|
||||
{% endif %}
|
||||
{% if interface.post_down %}
|
||||
PostDown = {{ interface.post_down }}
|
||||
{% endif %}
|
||||
{% for peer_name, peer in peers.items() %}
|
||||
|
||||
[Peer] # {{ peer_name }}
|
||||
PublicKey = {{ peer.public_key }}
|
||||
{% if peer.preshared_key %}
|
||||
PresharedKey = {{ peer.preshared_key }}
|
||||
{% endif %}
|
||||
{% if peer.endpoint %}
|
||||
Endpoint = {{ peer.endpoint }}
|
||||
{% endif %}
|
||||
{% if peer.allowed_ips %}
|
||||
AllowedIPs = {{ peer.allowed_ips | join(',') }}
|
||||
{% endif %}
|
||||
{% if peer.persistent_keepalive is not none %}
|
||||
PersistentKeepalive = {{ peer.persistent_keepalive }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Reference in New Issue
Block a user