refactor: update system config, sudoers, and install script
This commit is contained in:
+117
-282
@@ -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
|
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.timer" /etc/systemd/system/vacuum-wall-acme.timer
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
||||||
# --- 7. Enable IP forwarding ---
|
# --- 7. Enable IP forwarding (persistent via sysctl.conf) ---
|
||||||
log "Enabling IP forwarding..."
|
log "Enabling IP forwarding..."
|
||||||
if ! grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then
|
if ! grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then
|
||||||
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
|
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
|
||||||
fi
|
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 ---
|
# --- 8. Detect network interfaces ---
|
||||||
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 <<MGMTSITEEOF
|
|
||||||
# Vacuum Wall - Management Proxy
|
|
||||||
# Auto-generated by install.sh
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name $DOMAIN;
|
|
||||||
return 301 https://\$host\$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name $DOMAIN;
|
|
||||||
|
|
||||||
ssl_certificate $ACME_HOME/$DOMAIN/fullchain.cer;
|
|
||||||
ssl_certificate_key $ACME_HOME/$DOMAIN/$DOMAIN.key;
|
|
||||||
|
|
||||||
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;
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
auth_basic "Vacuum Wall";
|
|
||||||
auth_basic_user_file ${PROJECT_DIR}/data/nginx/.htpasswd;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:9090;
|
|
||||||
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;
|
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection \$connection_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MGMTSITEEOF
|
|
||||||
|
|
||||||
# --- 11. Write initial nginx config.json (skip if user has customized it) ---
|
|
||||||
NGINX_CFG="${PROJECT_DIR}/config/nginx/config.json"
|
|
||||||
if [[ -f "$NGINX_CFG" ]]; then
|
|
||||||
log "Nginx config already exists, skipping initial write."
|
|
||||||
else
|
|
||||||
log "Writing initial nginx configuration..."
|
|
||||||
MGMT_DOMAIN="$DOMAIN" \
|
|
||||||
MGMT_USER="$MGMT_USER" \
|
|
||||||
INSTALL_DIR="$PROJECT_DIR" \
|
|
||||||
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
|
||||||
import json, os
|
|
||||||
d = os.environ['MGMT_DOMAIN']
|
|
||||||
u = os.environ['MGMT_USER']
|
|
||||||
p = os.environ['INSTALL_DIR']
|
|
||||||
cfg = {
|
|
||||||
'domains': {},
|
|
||||||
'management': {
|
|
||||||
'domain': d,
|
|
||||||
'backend': {
|
|
||||||
'host': '127.0.0.1',
|
|
||||||
'port': 9090,
|
|
||||||
'proto': 'http'
|
|
||||||
},
|
|
||||||
'auth': {
|
|
||||||
'user': u,
|
|
||||||
'htpasswd': p + '/data/nginx/.htpasswd'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'ssl': {
|
|
||||||
'protocols': 'TLSv1.2 TLSv1.3',
|
|
||||||
'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',
|
|
||||||
'prefer_server_ciphers': False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
with open(os.path.join(p, 'config/nginx/config.json'), 'w') as f:
|
|
||||||
json.dump(cfg, f, indent=4)
|
|
||||||
f.write('\n')
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- 12. Auto-detect interfaces and setup initial firewalld zones ---
|
|
||||||
log "Detecting network interfaces..."
|
log "Detecting network interfaces..."
|
||||||
|
|
||||||
# Auto-detect WAN (interface with default gateway)
|
# Auto-detect WAN (interface with default gateway)
|
||||||
@@ -479,134 +321,146 @@ if [[ -z "$LAN_IFACES" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate config/firewall/config.json (skip if user has customized it)
|
# --- 9. Enable and start core services ---
|
||||||
FIREWALL_CFG="${PROJECT_DIR}/config/firewall/config.json"
|
log "Enabling services..."
|
||||||
if [[ -f "$FIREWALL_CFG" ]]; then
|
systemctl enable firewalld >/dev/null 2>&1 && log "Enabled firewalld" || warn "Could not enable firewalld"
|
||||||
log "Firewall config already exists, skipping initial write."
|
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
|
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" \
|
WAN_IFACE="$WAN_IFACE" \
|
||||||
LAN_IFACES="$LAN_IFACES" \
|
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 "
|
"${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
|
domain = '${DOMAIN}'
|
||||||
lan = os.environ.get('LAN_IFACES', '').strip() or None
|
mgmt_user = '${MGMT_USER}'
|
||||||
p = os.environ['INSTALL_DIR']
|
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:
|
# Management proxy + htpasswd
|
||||||
cfg['zones']['public'] = {
|
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',
|
'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'],
|
'services': ['http', 'https', 'ssh'],
|
||||||
'masquerade': True,
|
'masquerade': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if lan:
|
if lan_ifaces:
|
||||||
cfg['zones']['internal'] = {
|
zones['internal'] = {
|
||||||
'target': 'ACCEPT',
|
'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'],
|
'services': ['dhcp', 'dns', 'ntp'],
|
||||||
'masquerade': False,
|
'masquerade': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Always create vpn zone skeleton for later WireGuard setup
|
# Always create vpn zone skeleton for later WireGuard setup
|
||||||
cfg['zones']['vpn'] = {
|
zones['vpn'] = {
|
||||||
'target': 'ACCEPT',
|
'target': 'ACCEPT',
|
||||||
'interfaces': [],
|
'interfaces': [],
|
||||||
'services': [],
|
'services': [],
|
||||||
'masquerade': False,
|
'masquerade': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(os.path.join(p, 'config/firewall/config.json'), 'w') as f:
|
try:
|
||||||
json.dump(cfg, f, indent=2)
|
c.post(POST_FIREWALL_CONFIG, {'zones': zones})
|
||||||
f.write('\n')
|
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
|
log "Subsystem configuration complete"
|
||||||
|
|
||||||
# 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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI"
|
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" || \
|
systemctl restart nginx >/dev/null 2>&1 && log "Restarted nginx" || \
|
||||||
warn "Could not restart nginx (check config)"
|
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 ---
|
# --- Done ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ try:
|
|||||||
import requests_unixsocket
|
import requests_unixsocket
|
||||||
|
|
||||||
from daemon.client import post
|
from daemon.client import post
|
||||||
|
from daemon.iface import POST_NGINX_RELOAD
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
project_dir = os.environ.get("INSTALL_DIR", os.path.dirname(os.path.dirname(__file__)))
|
project_dir = os.environ.get("INSTALL_DIR", os.path.dirname(os.path.dirname(__file__)))
|
||||||
@@ -20,7 +21,7 @@ try:
|
|||||||
"VACUUM_WALLD_SOCKET",
|
"VACUUM_WALLD_SOCKET",
|
||||||
os.path.join(project_dir, "data", "daemon.sock"),
|
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)
|
sys.exit(0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logging.error("acme-deploy hook failed: %s", exc)
|
logging.error("acme-deploy hook failed: %s", exc)
|
||||||
|
|||||||
+5
-1
@@ -1,9 +1,13 @@
|
|||||||
# ---- vacuum-wall managed dnsmasq configuration ----
|
# ---- vacuum-wall managed dnsmasq configuration ----
|
||||||
# generated {{ timestamp }}
|
# generated {{ timestamp }}
|
||||||
|
|
||||||
|
bind-interfaces
|
||||||
{% if interfaces %}
|
{% if interfaces %}
|
||||||
interface={{ interfaces | join(',') }}
|
interface={{ interfaces | join(',') }}
|
||||||
bind-interfaces
|
{% elif listen_addresses %}
|
||||||
|
{% for addr in listen_addresses %}
|
||||||
|
listen-address={{ addr }}
|
||||||
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for srv in dns.upstreams %}
|
{% for srv in dns.upstreams %}
|
||||||
server={{ srv }}
|
server={{ srv }}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ server {
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif is_management %}
|
{% elif is_management %}
|
||||||
ssl_certificate {{ certs_dir }}/{{ domain }}.crt;
|
ssl_certificate {{ acme_home }}/{{ domain }}/fullchain.cer;
|
||||||
ssl_certificate_key {{ certs_dir }}/{{ domain }}.key;
|
ssl_certificate_key {{ acme_home }}/{{ domain }}/{{ domain }}.key;
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
# Shared SSL settings
|
# Shared SSL settings
|
||||||
@@ -59,12 +59,14 @@ server {
|
|||||||
add_header X-XSS-Protection "1; mode=block" 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;
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
{% endif %}
|
||||||
|
location / {
|
||||||
# Proxy headers
|
# Proxy headers
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
{% if not is_management %}
|
||||||
{% for hname, hval in headers.items() %}
|
{% for hname, hval in headers.items() %}
|
||||||
proxy_set_header {{ hname }} {{ hval }};
|
proxy_set_header {{ hname }} {{ hval }};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -73,32 +75,31 @@ server {
|
|||||||
proxy_pass {{ backend.proto }}://{{ backend.host }}:{{ backend.port }};
|
proxy_pass {{ backend.proto }}://{{ backend.host }}:{{ backend.port }};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
{% if not is_management %}
|
|
||||||
# Timeouts
|
# Timeouts
|
||||||
proxy_connect_timeout 30s;
|
proxy_connect_timeout 30s;
|
||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 60s;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
proxy_buffering off;
|
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 %}
|
||||||
# Access / error logs
|
# Access / error logs
|
||||||
access_log /var/log/nginx/{{ domain }}_access.log;
|
access_log /var/log/nginx/{{ domain }}_access.log;
|
||||||
error_log /var/log/nginx/{{ domain }}_error.log warn;
|
error_log /var/log/nginx/{{ domain }}_error.log warn;
|
||||||
{% else %}
|
{% 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;
|
access_log /var/log/nginx/wall_mgmt_access.log;
|
||||||
error_log /var/log/nginx/wall_mgmt_error.log warn;
|
error_log /var/log/nginx/wall_mgmt_error.log warn;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
location / {
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/rm /etc/systemd/network/*.network
|
||||||
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/systemd/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
|
# Misc
|
||||||
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
|
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/journalctl --unit=* -n *
|
||||||
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
|
{{ USER_DAEMON_NAME }} ALL=(root) NOPASSWD: /usr/bin/cat /var/log/nginx/*
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Environment=HOME={{ PROJECT_DIR }}
|
|||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
ProtectSystem=strict
|
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
|
PrivateTmp=yes
|
||||||
ProtectKernelTunables=yes
|
ProtectKernelTunables=yes
|
||||||
ProtectKernelModules=yes
|
ProtectKernelModules=yes
|
||||||
|
|||||||
Reference in New Issue
Block a user