Overhaul install.sh for /opt/vacuum-wall deployment and templated system files
Deploy via rsync to /opt/vacuum-wall with --no-create-home. Auto-detect MGMT_DOMAIN from hostname. Add avahi-daemon dependency. Render systemd units and sudoers from Jinja2 templates with USER_NAME, PROJECT_DIR, ACME_HOME substituted at install time.
This commit is contained in:
+295
-93
@@ -12,12 +12,12 @@ log() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|||||||
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
|
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
|
||||||
err() { echo -e "${RED}[!!]${NC} $*"; exit 1; }
|
err() { echo -e "${RED}[!!]${NC} $*"; exit 1; }
|
||||||
|
|
||||||
PROJECT_DIR="/home/wall/vacuum-wall"
|
# --- Configurable via environment ---
|
||||||
USER_NAME="vacuum-wall"
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
USER_HOME="/home/$USER_NAME"
|
USER_NAME="${USER_NAME:-vacuum-wall}"
|
||||||
|
|
||||||
# --- Validate required env vars ---
|
# --- Validate required env vars ---
|
||||||
missing=()
|
missing=()
|
||||||
[[ -z "${MGMT_DOMAIN:-}" ]] && missing+=(MGMT_DOMAIN)
|
|
||||||
[[ -z "${MGMT_PASS:-}" ]] && missing+=(MGMT_PASS)
|
[[ -z "${MGMT_PASS:-}" ]] && missing+=(MGMT_PASS)
|
||||||
[[ -z "${ACME_EMAIL:-}" ]] && missing+=(ACME_EMAIL)
|
[[ -z "${ACME_EMAIL:-}" ]] && missing+=(ACME_EMAIL)
|
||||||
|
|
||||||
@@ -25,24 +25,66 @@ if (( ${#missing[@]} )); then
|
|||||||
echo -e "${RED}[!!]${NC} Missing required environment variables:"
|
echo -e "${RED}[!!]${NC} Missing required environment variables:"
|
||||||
for v in "${missing[@]}"; do
|
for v in "${missing[@]}"; do
|
||||||
case "$v" in
|
case "$v" in
|
||||||
MGMT_DOMAIN) echo ' export MGMT_DOMAIN="wall.lan" # Management UI hostname';;
|
|
||||||
MGMT_PASS) echo ' export MGMT_PASS="your-password" # WebUI basic auth password';;
|
MGMT_PASS) echo ' export MGMT_PASS="your-password" # WebUI basic auth password';;
|
||||||
ACME_EMAIL) echo " export ACME_EMAIL=\"you@example.com\" # Let's Encrypt email";;
|
ACME_EMAIL) echo " export ACME_EMAIL=\"you@example.com\" # ACME (ZeroSSL) registration email";;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
printf '\nTo run: MGMT_DOMAIN=wall.lan MGMT_PASS=pass ACME_EMAIL=you@example.com ./install.sh\n'
|
printf '\nTo run: MGMT_PASS=pass ACME_EMAIL=you@example.com ./install.sh\n'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auto-detect MGMT_DOMAIN from system hostname if not provided
|
||||||
|
if [[ -z "${MGMT_DOMAIN:-}" ]]; then
|
||||||
|
HOSTNAME_F=$(hostname -f 2>/dev/null || hostname 2>/dev/null || true)
|
||||||
|
if [[ -z "$HOSTNAME_F" ]]; then
|
||||||
|
err "Cannot determine system hostname — set MGMT_DOMAIN env var."
|
||||||
|
fi
|
||||||
|
DOMAIN="${HOSTNAME_F}.local"
|
||||||
|
else
|
||||||
DOMAIN="$MGMT_DOMAIN"
|
DOMAIN="$MGMT_DOMAIN"
|
||||||
|
fi
|
||||||
MGMT_USER="${MGMT_USER:-admin}"
|
MGMT_USER="${MGMT_USER:-admin}"
|
||||||
|
|
||||||
# --- Pre-flight checks ---
|
# --- Pre-flight checks ---
|
||||||
[[ $EUID -eq 0 ]] || err "This script must be run as root."
|
[[ $EUID -eq 0 ]] || err "This script must be run as root."
|
||||||
[[ -f /etc/debian_version ]] || warn "This script is designed for Debian/Ubuntu."
|
[[ -f /etc/debian_version ]] || warn "This script is designed for Debian/Ubuntu."
|
||||||
|
|
||||||
|
# --- Deploy to /opt/vacuum-wall ---
|
||||||
|
INSTALL_DIR="/opt/vacuum-wall"
|
||||||
|
|
||||||
|
# Install rsync first if not available (needed for deploy)
|
||||||
|
if ! command -v rsync &>/dev/null; then
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq rsync
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$INSTALL_DIR" ]]; then
|
||||||
|
if [[ -L "$INSTALL_DIR" ]]; then
|
||||||
|
log "Symbolic link already exists at $INSTALL_DIR, skipping deploy."
|
||||||
|
elif [[ "$INSTALL_DIR" == "$REPO_DIR" ]]; then
|
||||||
|
log "Installed from repo location, skipping deploy."
|
||||||
|
else
|
||||||
|
err "Installation directory $INSTALL_DIR already exists."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Deploying $REPO_DIR → $INSTALL_DIR"
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude='.venv' \
|
||||||
|
--exclude='__pycache__' \
|
||||||
|
--exclude='*.pyc' \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='build' \
|
||||||
|
"$REPO_DIR/" "$INSTALL_DIR/"
|
||||||
|
chown -R "$USER_NAME:$USER_NAME" "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_DIR="$INSTALL_DIR"
|
||||||
|
ACME_HOME="$PROJECT_DIR/data/acme"
|
||||||
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " Vacuum Wall Appliance Installer"
|
echo " Vacuum Wall Appliance Installer"
|
||||||
|
echo " Install dir: $PROJECT_DIR"
|
||||||
|
echo " System user: $USER_NAME"
|
||||||
echo " Management domain: $DOMAIN"
|
echo " Management domain: $DOMAIN"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|
||||||
@@ -60,110 +102,133 @@ apt-get install -y -qq \
|
|||||||
curl \
|
curl \
|
||||||
iptables \
|
iptables \
|
||||||
nftables \
|
nftables \
|
||||||
apache2-utils
|
apache2-utils \
|
||||||
|
avahi-daemon
|
||||||
|
|
||||||
# --- 1b. Setup Python venv ---
|
# --- 2. Create system user (home set to PROJECT_DIR for env, no-create) ---
|
||||||
log "Setting up Python virtual environment..."
|
|
||||||
python3 -m venv "${PROJECT_DIR}/.venv"
|
|
||||||
"${PROJECT_DIR}/.venv/bin/pip" install -q "${PROJECT_DIR}"
|
|
||||||
chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/.venv"
|
|
||||||
|
|
||||||
# --- 2. Create system user ---
|
|
||||||
if ! id "$USER_NAME" &>/dev/null; then
|
if ! id "$USER_NAME" &>/dev/null; then
|
||||||
log "Creating system user $USER_NAME..."
|
log "Creating system user $USER_NAME..."
|
||||||
useradd --system --home-dir "$USER_HOME" --shell /usr/sbin/nologin "$USER_NAME"
|
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin "$USER_NAME"
|
||||||
else
|
else
|
||||||
log "User $USER_NAME already exists."
|
log "User $USER_NAME already exists."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 2b. Install acme.sh as the vacuum-wall user ---
|
# --- 2b. Setup Python venv ---
|
||||||
if [[ ! -d "$USER_HOME/.acme.sh" ]]; then
|
log "Setting up Python virtual environment..."
|
||||||
log "Installing acme.sh for $USER_NAME..."
|
python3 -m venv "${PROJECT_DIR}/.venv"
|
||||||
su -s /bin/sh "$USER_NAME" -c \
|
"${PROJECT_DIR}/.venv/bin/pip" install -q "${PROJECT_DIR}"
|
||||||
"ACME_HOME='$USER_HOME/.acme.sh' curl -sS https://get.acme.sh | sh"
|
|
||||||
|
# --- 2c. Install acme.sh ---
|
||||||
|
if [[ ! -d "$ACME_HOME" ]]; then
|
||||||
|
log "Installing acme.sh..."
|
||||||
|
mkdir -p "$ACME_HOME"
|
||||||
|
chown "$USER_NAME:$USER_NAME" "$ACME_HOME"
|
||||||
|
sudo -u "$USER_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \
|
||||||
|
sh -c 'curl -sS https://get.acme.sh | sh'
|
||||||
else
|
else
|
||||||
log "acme.sh already installed."
|
log "acme.sh already installed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure the vacuum-wall user owns the acme.sh directory
|
|
||||||
chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/.acme.sh"
|
|
||||||
|
|
||||||
# Ensure the acme deploy hook script has correct permissions
|
# Ensure the acme deploy hook script has correct permissions
|
||||||
chmod 0755 "${PROJECT_DIR}/system/acme-deploy.sh"
|
chmod 0755 "${PROJECT_DIR}/system/acme-deploy.sh"
|
||||||
|
|
||||||
# --- 3. Setup directories ---
|
# --- 3. Setup directories ---
|
||||||
log "Creating data directories..."
|
log "Creating config and data directories..."
|
||||||
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard}
|
mkdir -p "${PROJECT_DIR}/config"/{dnsmasq,nginx,wireguard,firewall}
|
||||||
mkdir -p "$USER_HOME/vacuum-wall"
|
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard,acme}
|
||||||
mkdir -p /etc/wireguard
|
mkdir -p /etc/wireguard
|
||||||
mkdir -p /etc/dnsmasq
|
mkdir -p /etc/dnsmasq
|
||||||
|
|
||||||
chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data"
|
# --- 4. Template rendering function ---
|
||||||
chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/lib"
|
render_template() {
|
||||||
chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/webui"
|
export USER_NAME PROJECT_DIR ACME_HOME
|
||||||
chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/vacuum-wall"
|
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
||||||
|
import sys, os
|
||||||
|
from jinja2 import Template
|
||||||
|
text = open(sys.argv[1]).read()
|
||||||
|
env = {
|
||||||
|
'USER_NAME': os.environ['USER_NAME'],
|
||||||
|
'PROJECT_DIR': os.environ['PROJECT_DIR'],
|
||||||
|
'ACME_HOME': os.environ['ACME_HOME'],
|
||||||
|
}
|
||||||
|
print(Template(text).render(**env), end='')
|
||||||
|
" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# --- 4. Install sudoers ---
|
# --- 5. Install sudoers ---
|
||||||
log "Installing sudoers whitelist..."
|
log "Installing sudoers whitelist..."
|
||||||
install -m 0440 "${PROJECT_DIR}/system/sudoers.d/vacuum-wall" /etc/sudoers.d/vacuum-wall
|
render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-wall" \
|
||||||
|
| install -m 0440 /dev/stdin /etc/sudoers.d/vacuum-wall
|
||||||
|
|
||||||
# Validate sudoers syntax
|
|
||||||
visudo -cf /etc/sudoers.d/vacuum-wall || err "Invalid sudoers file!"
|
visudo -cf /etc/sudoers.d/vacuum-wall || err "Invalid sudoers file!"
|
||||||
|
|
||||||
# --- 5. Enable IP forwarding ---
|
# --- 6. Install systemd units ---
|
||||||
|
log "Installing systemd units..."
|
||||||
|
render_template "${PROJECT_DIR}/system/systemd/vacuum-wall.service" \
|
||||||
|
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-wall.service
|
||||||
|
|
||||||
|
render_template "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" \
|
||||||
|
| install -m 0644 /dev/stdin /etc/systemd/system/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 ---
|
||||||
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)"
|
sysctl -w net.ipv4.ip_forward=1 2>/dev/null || warn "Could not enable IP forwarding (may need kernel access)"
|
||||||
|
|
||||||
# --- 6. Start and configure firewalld ---
|
# --- 8. Start and configure firewalld ---
|
||||||
log "Enabling firewalld..."
|
log "Enabling firewalld..."
|
||||||
systemctl enable firewalld || warn "Could not enable firewalld (already running?)"
|
systemctl enable firewalld >/dev/null 2>&1 || warn "Could not enable firewalld (already running?)"
|
||||||
systemctl start firewalld || warn "Could not start firewalld (may need D-Bus)"
|
systemctl start firewalld >/dev/null 2>&1 || warn "Could not start firewalld (may need D-Bus)"
|
||||||
|
|
||||||
# Allow management access (HTTP/HTTPS for the proxy)
|
firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true
|
||||||
firewall-cmd --permanent --add-service=http 2>/dev/null || true
|
log "Added service http to public zone"
|
||||||
firewall-cmd --permanent --add-service=https 2>/dev/null || true
|
firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true
|
||||||
firewall-cmd --permanent --add-service=ssh 2>/dev/null || true
|
log "Added service https to public zone"
|
||||||
firewall-cmd --reload 2>/dev/null || true
|
firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 || true
|
||||||
|
log "Added service ssh to public zone"
|
||||||
|
firewall-cmd --reload >/dev/null 2>&1 || true
|
||||||
|
log "Firewalld rules reloaded"
|
||||||
|
|
||||||
# --- 7. Configure dnsmasq ---
|
# --- 9. Configure dnsmasq ---
|
||||||
log "Configuring dnsmasq..."
|
log "Configuring dnsmasq..."
|
||||||
systemctl enable dnsmasq || true
|
systemctl enable dnsmasq >/dev/null 2>&1 || true
|
||||||
systemctl start dnsmasq || warn "Could not start dnsmasq (no interfaces configured yet)"
|
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)"
|
||||||
|
|
||||||
# --- 8. Setup nginx management proxy ---
|
# --- 10. Setup nginx management proxy ---
|
||||||
log "Generating self-signed certificate for management domain..."
|
log "Generating self-signed certificate for management domain..."
|
||||||
mkdir -p "$USER_HOME/.acme.sh/$DOMAIN"
|
mkdir -p "$ACME_HOME/$DOMAIN"
|
||||||
|
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||||
-keyout "$USER_HOME/.acme.sh/$DOMAIN/$DOMAIN.key" \
|
-keyout "$ACME_HOME/$DOMAIN/$DOMAIN.key" \
|
||||||
-out "$USER_HOME/.acme.sh/$DOMAIN/fullchain.cer" \
|
-out "$ACME_HOME/$DOMAIN/fullchain.cer" \
|
||||||
-subj "/CN=$DOMAIN" \
|
-subj "/CN=$DOMAIN" \
|
||||||
-addext "subjectAltName=DNS:$DOMAIN"
|
-addext "subjectAltName=DNS:$DOMAIN"
|
||||||
|
|
||||||
chown -R "$USER_NAME:$USER_NAME" "$USER_HOME/.acme.sh"
|
chown -R "$USER_NAME:$USER_NAME" "$ACME_HOME"
|
||||||
|
|
||||||
# Generate htpasswd (credentials passed via environment to avoid shell injection)
|
# Generate htpasswd directly in data/nginx/
|
||||||
mkdir -p "$USER_HOME/vacuum-wall"
|
htpasswd -cb "${PROJECT_DIR}/data/nginx/.htpasswd" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \
|
||||||
htpasswd -cb "$USER_HOME/vacuum-wall/.htpasswd" "$MGMT_USER" "$MGMT_PASS" 2>/dev/null || \
|
MGMT_USER="$MGMT_USER" MGMT_PASS="$MGMT_PASS" HTFILE="${PROJECT_DIR}/data/nginx/.htpasswd" python3 -c "
|
||||||
MGMT_USER="$MGMT_USER" MGMT_PASS="$MGMT_PASS" python3 -c "
|
|
||||||
import os, crypt, base64
|
import os, crypt, base64
|
||||||
password = os.environ['MGMT_PASS']
|
password = os.environ['MGMT_PASS']
|
||||||
user = os.environ['MGMT_USER']
|
user = os.environ['MGMT_USER']
|
||||||
salt = '\$6\$' + base64.b64encode(os.urandom(16)).decode().rstrip('=')[:16]
|
salt = '\$6\$' + base64.b64encode(os.urandom(16)).decode().rstrip('=')[:16]
|
||||||
hashed = crypt.crypt(password, salt)
|
hashed = crypt.crypt(password, salt)
|
||||||
with open('$USER_HOME/vacuum-wall/.htpasswd', 'w') as f:
|
with open(os.environ['HTFILE'], 'w') as f:
|
||||||
f.write(user + ':' + hashed + '\n')
|
f.write(user + ':' + hashed + '\n')
|
||||||
" 2>/dev/null || \
|
" 2>/dev/null || \
|
||||||
warn "Could not generate htpasswd (install apache2-utils or python3-crypt)"
|
warn "Could not generate htpasswd (install apache2-utils or python3-crypt)"
|
||||||
|
|
||||||
chown "$USER_NAME:$USER_NAME" "$USER_HOME/vacuum-wall/.htpasswd" 2>/dev/null
|
chown "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data/nginx/.htpasswd"
|
||||||
|
|
||||||
# Copy .htpasswd to the path expected by the running app (data/nginx/.htpasswd)
|
# Remove default nginx site so vacuum-wall management config takes precedence
|
||||||
cp "$USER_HOME/vacuum-wall/.htpasswd" "${PROJECT_DIR}/data/nginx/.htpasswd" 2>/dev/null || true
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
chown "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data/nginx/.htpasswd" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Write WebSocket upgrade map (nginx conf.d/ is already inside http {} context)
|
# Write WebSocket upgrade map (nginx conf.d/ is already inside http {} context)
|
||||||
cat > /etc/nginx/conf.d/vacuum-wall-map.conf <<'MAPEOF'
|
cat > /etc/nginx/conf.d/vacuum-wall-map.conf <<'MAPEOF'
|
||||||
@@ -189,15 +254,15 @@ server {
|
|||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name $DOMAIN;
|
server_name $DOMAIN;
|
||||||
|
|
||||||
ssl_certificate $USER_HOME/.acme.sh/$DOMAIN/fullchain.cer;
|
ssl_certificate $ACME_HOME/$DOMAIN/fullchain.cer;
|
||||||
ssl_certificate_key $USER_HOME/.acme.sh/$DOMAIN/$DOMAIN.key;
|
ssl_certificate_key $ACME_HOME/$DOMAIN/$DOMAIN.key;
|
||||||
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
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_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;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
auth_basic "Vacuum Wall";
|
auth_basic "Vacuum Wall";
|
||||||
auth_basic_user_file $USER_HOME/vacuum-wall/.htpasswd;
|
auth_basic_user_file ${PROJECT_DIR}/data/nginx/.htpasswd;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:9090;
|
proxy_pass http://127.0.0.1:9090;
|
||||||
@@ -214,44 +279,169 @@ server {
|
|||||||
}
|
}
|
||||||
MGMTSITEEOF
|
MGMTSITEEOF
|
||||||
|
|
||||||
# --- 9. Install systemd units ---
|
# --- 11. Write initial nginx config.json ---
|
||||||
log "Installing systemd units..."
|
log "Writing initial nginx configuration..."
|
||||||
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall.service" /etc/systemd/system/vacuum-wall.service
|
MGMT_DOMAIN="$DOMAIN" \
|
||||||
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" /etc/systemd/system/vacuum-wall-acme.service
|
MGMT_USER="$MGMT_USER" \
|
||||||
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.timer" /etc/systemd/system/vacuum-wall-acme.timer
|
INSTALL_DIR="$PROJECT_DIR" \
|
||||||
systemctl daemon-reload
|
"${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')
|
||||||
|
"
|
||||||
|
|
||||||
# --- 10. Setup initial firewalld zones ---
|
# --- 12. Auto-detect interfaces and setup initial firewalld zones ---
|
||||||
log "Setting up default firewalld zones..."
|
log "Detecting network interfaces..."
|
||||||
|
|
||||||
# Internal zone (LAN)
|
# Auto-detect WAN (interface with default gateway)
|
||||||
firewall-cmd --permanent --new-zone=internal 2>/dev/null || true
|
WAN_IFACE="${WAN_IFACE:-}"
|
||||||
firewall-cmd --permanent --zone=internal --set-target=ACCEPT 2>/dev/null || true
|
if [[ -z "$WAN_IFACE" ]]; then
|
||||||
firewall-cmd --permanent --zone=internal --add-service=dhcp 2>/dev/null || true
|
DETECTED_WAN=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
|
||||||
firewall-cmd --permanent --zone=internal --add-service=dns 2>/dev/null || true
|
if [[ -n "$DETECTED_WAN" ]]; then
|
||||||
firewall-cmd --permanent --zone=internal --add-service=ntp 2>/dev/null || true
|
WAN_IFACE="$DETECTED_WAN"
|
||||||
|
log "Auto-detected WAN interface: $WAN_IFACE"
|
||||||
|
else
|
||||||
|
warn "Could not auto-detect WAN interface — set WAN_IFACE env var"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# VPN zone
|
# Auto-detect LAN (all non-loopback, non-Docker, non-WAN, non-virtual interfaces)
|
||||||
firewall-cmd --permanent --new-zone=vpn 2>/dev/null || true
|
LAN_IFACES="${LAN_IFACES:-}"
|
||||||
firewall-cmd --permanent --zone=vpn --set-target=ACCEPT 2>/dev/null || true
|
if [[ -z "$LAN_IFACES" ]]; then
|
||||||
|
DETECTED_LANS=$(ls /sys/class/net/ 2>/dev/null \
|
||||||
|
| grep -vE "^(lo|docker|br-|virbr|${WAN_IFACE})$")
|
||||||
|
if [[ -n "$DETECTED_LANS" ]]; then
|
||||||
|
LAN_IFACES=$(echo "$DETECTED_LANS" | paste -sd ',' -)
|
||||||
|
log "Auto-detected LAN interfaces: $LAN_IFACES"
|
||||||
|
else
|
||||||
|
warn "Could not auto-detect LAN interfaces — set LAN_IFACES env var"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Public/external zone defaults are fine
|
# Generate config/firewall/config.json
|
||||||
|
log "Writing initial firewall configuration..."
|
||||||
|
WAN_IFACE="$WAN_IFACE" \
|
||||||
|
LAN_IFACES="$LAN_IFACES" \
|
||||||
|
INSTALL_DIR="$PROJECT_DIR" \
|
||||||
|
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
||||||
|
import json, os
|
||||||
|
|
||||||
firewall-cmd --reload 2>/dev/null || true
|
wan = os.environ.get('WAN_IFACE', '').strip() or None
|
||||||
|
lan = os.environ.get('LAN_IFACES', '').strip() or None
|
||||||
|
p = os.environ['INSTALL_DIR']
|
||||||
|
|
||||||
# --- 11. Enable and start services ---
|
cfg = {'zones': {}}
|
||||||
|
|
||||||
|
if wan:
|
||||||
|
cfg['zones']['public'] = {
|
||||||
|
'target': 'DEFAULT',
|
||||||
|
'interfaces': [i for i in wan.split(',') if i],
|
||||||
|
'services': ['http', 'https', 'ssh'],
|
||||||
|
'masquerade': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if lan:
|
||||||
|
cfg['zones']['internal'] = {
|
||||||
|
'target': 'ACCEPT',
|
||||||
|
'interfaces': [i for i in lan.split(',') if i],
|
||||||
|
'services': ['dhcp', 'dns', 'ntp'],
|
||||||
|
'masquerade': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always create vpn zone skeleton for later WireGuard setup
|
||||||
|
cfg['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')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Apply zones via firewall-cmd (Python venv not yet fully available for apply_config)
|
||||||
|
firewall-cmd --permanent --new-zone=internal >/dev/null 2>&1 || true
|
||||||
|
log "Created firewalld zone: internal"
|
||||||
|
firewall-cmd --permanent --zone=internal --set-target=ACCEPT >/dev/null 2>&1 || true
|
||||||
|
firewall-cmd --permanent --zone=internal --add-service=dhcp >/dev/null 2>&1 || true
|
||||||
|
log "Added service dhcp to internal zone"
|
||||||
|
firewall-cmd --permanent --zone=internal --add-service=dns >/dev/null 2>&1 || true
|
||||||
|
log "Added service dns to internal zone"
|
||||||
|
firewall-cmd --permanent --zone=internal --add-service=ntp >/dev/null 2>&1 || true
|
||||||
|
log "Added service ntp to internal zone"
|
||||||
|
|
||||||
|
firewall-cmd --permanent --new-zone=vpn >/dev/null 2>&1 || true
|
||||||
|
log "Created firewalld zone: vpn"
|
||||||
|
firewall-cmd --permanent --zone=vpn --set-target=ACCEPT >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Apply masquerade on public/WAN
|
||||||
|
if [[ -n "$WAN_IFACE" ]]; then
|
||||||
|
firewall-cmd --permanent --zone=public --add-masquerade >/dev/null 2>&1 || true
|
||||||
|
log "Enabled masquerade on public zone ($WAN_IFACE)"
|
||||||
|
firewall-cmd --permanent --zone=public --add-interface="$WAN_IFACE" >/dev/null 2>&1 || true
|
||||||
|
log "Assigned $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 || true
|
||||||
|
log "Assigned $iface to internal zone"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
firewall-cmd --reload >/dev/null 2>&1 || true
|
||||||
|
log "Firewalld rules reloaded"
|
||||||
|
|
||||||
|
# --- 13. Enable and start services ---
|
||||||
log "Enabling services..."
|
log "Enabling services..."
|
||||||
systemctl enable nginx
|
systemctl enable nginx >/dev/null 2>&1 || true
|
||||||
systemctl enable vacuum-wall
|
log "Enabled nginx"
|
||||||
systemctl enable vacuum-wall-acme.timer
|
systemctl enable vacuum-wall >/dev/null 2>&1 || true
|
||||||
|
log "Enabled vacuum-wall"
|
||||||
|
systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 || true
|
||||||
|
log "Enabled vacuum-wall-acme.timer"
|
||||||
|
|
||||||
systemctl start nginx 2>/dev/null || warn "Could not start nginx (check config)"
|
systemctl enable avahi-daemon >/dev/null 2>&1 || true
|
||||||
systemctl start vacuum-wall 2>/dev/null || warn "Could not start vacuum-wall WebUI"
|
log "Enabled avahi-daemon"
|
||||||
|
systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start avahi-daemon"
|
||||||
|
|
||||||
# --- 12. Configure acme.sh default email ---
|
systemctl start nginx >/dev/null 2>&1 && log "Started nginx" || warn "Could not start nginx (check config)"
|
||||||
|
systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI"
|
||||||
|
|
||||||
|
# --- 14. Configure acme.sh default email ---
|
||||||
log "Configuring acme.sh default email..."
|
log "Configuring acme.sh default email..."
|
||||||
su -s /bin/sh "$USER_NAME" -c \
|
sudo -u "$USER_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \
|
||||||
"$USER_HOME/.acme.sh/acme.sh --register-account -m '$ACME_EMAIL'" 2>/dev/null || \
|
"$ACME_HOME/acme.sh" --register-account -m "$ACME_EMAIL" 2>/dev/null || \
|
||||||
warn "Could not register acme.sh account (will be done from WebUI)"
|
warn "Could not register acme.sh account (will be done from WebUI)"
|
||||||
|
|
||||||
# --- Done ---
|
# --- Done ---
|
||||||
@@ -265,10 +455,22 @@ echo " User: $MGMT_USER"
|
|||||||
echo " WebUI service: vacuum-wall.service"
|
echo " WebUI service: vacuum-wall.service"
|
||||||
echo " ACME renewal: vacuum-wall-acme.timer"
|
echo " ACME renewal: vacuum-wall-acme.timer"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo " Firewall zones:"
|
||||||
|
if [[ -n "$WAN_IFACE" ]]; then
|
||||||
|
echo " public (WAN) → $WAN_IFACE"
|
||||||
|
else
|
||||||
|
echo " public (WAN) → not assigned"
|
||||||
|
fi
|
||||||
|
if [[ -n "$LAN_IFACES" ]]; then
|
||||||
|
echo " internal (LAN) → $LAN_IFACES"
|
||||||
|
else
|
||||||
|
echo " internal (LAN) → not assigned"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
echo " Next steps:"
|
echo " Next steps:"
|
||||||
echo " 1. Assign interfaces to zones from the WebUI"
|
echo " 1. Verify zone assignments at https://$DOMAIN/interfaces"
|
||||||
echo " 2. Configure DHCP ranges for your LAN"
|
echo " 2. Configure DHCP ranges for your LAN"
|
||||||
echo " 3. Add proxy domains with Let's Encrypt certs"
|
echo " 3. Add proxy domains with ACME certificates"
|
||||||
echo " 4. Set up WireGuard tunnel (optional)"
|
echo " 4. Set up WireGuard tunnel (optional)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " NOTE: A self-signed certificate was generated."
|
echo " NOTE: A self-signed certificate was generated."
|
||||||
|
|||||||
Reference in New Issue
Block a user