install: two-user model ownership and deployment

This commit is contained in:
2026-05-29 22:29:54 +00:00
parent 200e078bc5
commit cb683f7e61
4 changed files with 123 additions and 86 deletions
+32 -5
View File
@@ -26,7 +26,7 @@ Download the Vacuum Wall repository onto the target machine, then run the instal
MGMT_DOMAIN=wall.example.com \ MGMT_DOMAIN=wall.example.com \
MGMT_PASS="strongpassword" \ MGMT_PASS="strongpassword" \
ACME_EMAIL="admin@example.com" \ ACME_EMAIL="admin@example.com" \
bash install.sh ./install.sh --user vacuum-wall
# Dev mode: CLI flags, auto-detects repo owner # Dev mode: CLI flags, auto-detects repo owner
./install.sh --dev --mgmt-pass strongpassword --acme-email "admin@example.com" ./install.sh --dev --mgmt-pass strongpassword --acme-email "admin@example.com"
@@ -46,7 +46,7 @@ All settings that can be passed as an environment variable also have a CLI flag
| `--mgmt-pass` | `MGMT_PASS` | Yes | Password for HTTP basic auth protecting the WebUI. | | `--mgmt-pass` | `MGMT_PASS` | Yes | Password for HTTP basic auth protecting the WebUI. |
| `--mgmt-user` | `MGMT_USER` | No | Username for WebUI access. Defaults to `admin`. | | `--mgmt-user` | `MGMT_USER` | No | Username for WebUI access. Defaults to `admin`. |
| `--acme-email` | `ACME_EMAIL` | Yes | Email for ACME provider (ZeroSSL by default). | | `--acme-email` | `ACME_EMAIL` | Yes | Email for ACME provider (ZeroSSL by default). |
| `--user, -u` | `USER_NAME` | No | System user for the WebUI service. Defaults to `vacuum-wall`. | | `--user, -u` | `USER_NAME` | Yes* | WebUI service user (created if it does not exist). Required for non-dev mode. In `--dev` mode, auto-detected from repo owner. |
| `--path, -p` | `INSTALL_DIR` | No | Install directory. Defaults to repo root. Set to deploy from a custom path (e.g., `/opt/vacuum-wall`). | | `--path, -p` | `INSTALL_DIR` | No | Install directory. Defaults to repo root. Set to deploy from a custom path (e.g., `/opt/vacuum-wall`). |
| `--dev` | -- | No | Development mode: auto-detects repo owner as service user, skips safety warning. | | `--dev` | -- | No | Development mode: auto-detects repo owner as service user, skips safety warning. |
| `--wan-iface` | `WAN_IFACE` | No | WAN interface name. Auto-detected from default gateway. | | `--wan-iface` | `WAN_IFACE` | No | WAN interface name. Auto-detected from default gateway. |
@@ -57,6 +57,33 @@ Run `./install.sh --help` for full usage.
--- ---
## Dev Mode
The `--dev` flag is designed for developers working in a git clone. It auto-detects the repo owner and uses that user as the WebUI service user.
### Ownership Model
In dev mode, the ownership model preserves the developer's ability to work with the repository:
- **Project directory**: Owned by the repo owner (e.g., `wall`), group is the repo owner's primary group (e.g., `wall`). The developer retains full control — `git add`, `git commit`, editing code and config files all work normally.
- **Daemon access**: The daemon user (`vacuum-walld`) has the repo owner's primary group as its own primary group, granting read access to all project files. The project directory has the setgid bit (`g+s`) on all subdirectories, ensuring new files inherit the group.
- **`.venv/` and `data/`**: Owned by the repo owner, group is the repo owner's primary group. The developer can run `pip install`, inspect logs, and manage runtime artifacts. The daemon reads `.venv/` (Python interpreter) and writes to `data/` (runtime files) via group permissions.
- **Daemon socket** (`data/daemon.sock`): Owned by `vacuum-walld:<group>` (mode `0660`). The repo owner accesses it via primary group membership.
### Running the Installer in Dev Mode
```bash
./install.sh --dev --mgmt-pass strongpassword --acme-email "dev@example.com"
```
The script detects the repo owner (e.g., `wall`), creates the `vacuum-walld` daemon user with the repo owner's primary group, and sets up the ownership model described above.
### Idempotent Re-Runs
Running `--dev` again is safe. The ownership is idempotent (`chown -R` to the same owner), supplementary group membership is deduplicated by the OS, and setgid bits are applied recursively each time.
---
## Container / Custom Deployment ## Container / Custom Deployment
You can deploy Vacuum Wall in a container or at any custom path. Use `--path` (or `INSTALL_DIR`) for the mount or bind path, and `--user` (or `USER_NAME`) for whatever system user exists: You can deploy Vacuum Wall in a container or at any custom path. Use `--path` (or `INSTALL_DIR`) for the mount or bind path, and `--user` (or `USER_NAME`) for whatever system user exists:
@@ -77,14 +104,14 @@ The systemd service unit files and sudoers whitelist are rendered from Jinja2 te
The installer performs the following steps automatically: The installer performs the following steps automatically:
- **Package installation**: Installs firewalld, nginx, dnsmasq, avahi-daemon, wireguard-tools, python3, python3-pip, jq, curl, iptables, nftables, and apache2-utils. - **Package installation**: Installs firewalld, nginx, dnsmasq, avahi-daemon, wireguard-tools, python3, python3-pip, jq, curl, iptables, nftables, and apache2-utils.
- **Shared group creation**: Creates a shared system group (`vacuum-wall`) both service users belong to. - **WebUI user creation**: Creates the WebUI user (from `--user`) as a system user if it does not exist.
- **Shared group**: Uses the WebUI user's primary group as the shared group between both service users.
- **Daemon user creation**: Creates `vacuum-walld` (derived from WebUI user name) — a system user with `NOPASSWD` sudo access for privileged operations. Owns the project directory and daemon socket. - **Daemon user creation**: Creates `vacuum-walld` (derived from WebUI user name) — a system user with `NOPASSWD` sudo access for privileged operations. Owns the project directory and daemon socket.
- **WebUI user creation**: Creates a dedicated system user (default: `vacuum-wall`, configurable via `USER_NAME`) with zero sudo access. Communicates with the daemon via Unix socket.
- **Python venv**: Creates the Python virtual environment and installs project dependencies. Skips if already present (use `--force-venv` to recreate). - **Python venv**: Creates the Python virtual environment and installs project dependencies. Skips if already present (use `--force-venv` to recreate).
- **acme.sh installation**: Copies the vendored acme.sh client to the data directory for ACME certificate management. Skips if already installed. - **acme.sh installation**: Copies the vendored acme.sh client to the data directory for ACME certificate management. Skips if already installed.
- **Directory setup**: Creates config directories under `config/` for each subsystem's declarative JSON, and data directories under `data/` for generated files (nginx sites, dnsmasq fragments, firewall backup, WireGuard config). - **Directory setup**: Creates config directories under `config/` for each subsystem's declarative JSON, and data directories under `data/` for generated files (nginx sites, dnsmasq fragments, firewall backup, WireGuard config).
- **Template rendering**: Renders system template files (`systemd/*.service`, `sudoers.d/`) via Jinja2, substituting `USER_NAME`, `INSTALL_DIR`, and `ACME_HOME`. Installed systemd and sudoers files contain no hardcoded values. - **Template rendering**: Renders system template files (`systemd/*.service`, `sudoers.d/`) via Jinja2, substituting `USER_NAME`, `INSTALL_DIR`, and `ACME_HOME`. Installed systemd and sudoers files contain no hardcoded values.
- **Sudoers whitelist**: Installs a restrictive sudoers file at `/etc/sudoers.d/vacuum-walld` granting the daemon user `NOPASSWD` sudo for only the specific privileged commands needed for firewall, nginx, dnsmasq, and acme.sh management. Validates syntax with `visudo -cf`. The WebUI user's sudoers file (`/etc/sudoers.d/vacuum-wall`) is empty — it has no sudo access. - **Sudoers whitelist**: Installs a restrictive sudoers file at `/etc/sudoers.d/vacuum-walld` granting the daemon user `NOPASSWD` sudo for only the specific privileged commands needed for firewall, nginx, dnsmasq, and acme.sh management. Validates syntax with `visudo -cf`.
- **IP forwarding**: Enables `net.ipv4.ip_forward=1` in sysctl.conf and applies it at runtime, required for routing traffic between zones. Appends only if not already present. - **IP forwarding**: Enables `net.ipv4.ip_forward=1` in sysctl.conf and applies it at runtime, required for routing traffic between zones. Appends only if not already present.
- **Firewalld initialization**: Starts and enables firewalld. Opens HTTP, HTTPS, and SSH services on the public zone for management access. - **Firewalld initialization**: Starts and enables firewalld. Opens HTTP, HTTPS, and SSH services on the public zone for management access.
- **Dnsmasq initialization**: Starts and enables dnsmasq for future DHCP/DNS serving on internal interfaces. - **Dnsmasq initialization**: Starts and enables dnsmasq for future DHCP/DNS serving on internal interfaces.
+90 -78
View File
@@ -40,7 +40,7 @@ while [[ $# -gt 0 ]]; do
"Usage: install.sh [OPTIONS]" \ "Usage: install.sh [OPTIONS]" \
"" \ "" \
"Options:" \ "Options:" \
" --user, -u USER System user for service (default: vacuum-wall)" \ " --user, -u USER WebUI user (created if it does not exist, required for non-dev mode)" \
" --path, -p DIR Install directory (default: repo root)" \ " --path, -p DIR Install directory (default: repo root)" \
" --dev Dev mode: auto-detect repo owner, skip safety warning" \ " --dev Dev mode: auto-detect repo owner, skip safety warning" \
" --mgmt-pass PASS WebUI basic auth password (required)" \ " --mgmt-pass PASS WebUI basic auth password (required)" \
@@ -60,7 +60,7 @@ while [[ $# -gt 0 ]]; do
" ./install.sh --dev --mgmt-pass pass --acme-email me@example.com" \ " ./install.sh --dev --mgmt-pass pass --acme-email me@example.com" \
"" \ "" \
"Example (prod):" \ "Example (prod):" \
" MGMT_PASS=pass ACME_EMAIL=me@example.com ./install.sh" " MGMT_PASS=pass ACME_EMAIL=me@example.com ./install.sh --user vacuum-wall"
exit 0 exit 0
;; ;;
*) *)
@@ -137,25 +137,31 @@ if [[ "$_cli_is_dev" == true ]]; then
fi fi
fi fi
# Optional settings with defaults # Resolve USER_NAME: dev mode auto-detects, non-dev requires --user
USER_NAME="${_cli_user:-${USER_NAME:-vacuum-wall}}" USER_NAME="${_cli_user:-${USER_NAME:-}}"
if [[ -z "$USER_NAME" ]]; then
err "WebUI user is required. Use --dev to auto-detect repo owner, or set --user / USER_NAME."
fi
# Daemon user name (derived from web UI user name) # Daemon user name (derived from web UI user name)
USER_DAEMON_NAME="${USER_NAME}d" USER_DAEMON_NAME="${USER_NAME}d"
# --- Safety check: running service as a non-system regular user --- # --- Safety check: running service as a regular user ---
if [[ "$_cli_is_dev" != true ]] && [[ "$USER_NAME" != "vacuum-wall" ]] && id "$USER_NAME" &>/dev/null; then if [[ "$_cli_is_dev" != true ]] && id "$USER_NAME" &>/dev/null; then
_uid=$(id -u "$USER_NAME") _uid=$(id -u "$USER_NAME")
_gid=$(id -g "$USER_NAME")
_shell=$(getent passwd "$USER_NAME" | cut -d: -f7) _shell=$(getent passwd "$USER_NAME" | cut -d: -f7)
if [[ "$_uid" -ge 1000 ]] && [[ "$_shell" != "/usr/sbin/nologin" && "$_shell" != "/bin/false" ]]; then if [[ "$_uid" -ge 1000 ]] && [[ "$_shell" != "/usr/sbin/nologin" && "$_shell" != "/bin/false" ]]; then
warn "USER_NAME='$USER_NAME' is a regular user (UID=$_uid, shell=$_shell)!" warn "USER_NAME='$USER_NAME' is a regular user (UID=$_uid, shell=$_shell)!"
warn "This runs the web service as your login account." warn "This runs the web service as your login account."
warn "Sudo access is held only by the daemon user ($USER_DAEMON_NAME)." warn "Sudo access is held only by the daemon user ($USER_DAEMON_NAME)."
warn "Only use for development. For production, use --user vacuum-wall."
fi fi
fi fi
# Shared group for both users to access project files and socket # Create WebUI user if it does not exist
USER_GROUP="vacuum-wall" if ! id "$USER_NAME" &>/dev/null; then
log "Creating system user $USER_NAME..."
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin "$USER_NAME"
fi
# Shared group: use the WebUI user's primary group
USER_GROUP=$(id -gn "$USER_NAME")
echo "============================================" echo "============================================"
echo " Vacuum Wall Appliance Installer" echo " Vacuum Wall Appliance Installer"
@@ -183,13 +189,8 @@ apt-get install -y -qq \
apache2-utils \ apache2-utils \
avahi-daemon avahi-daemon
# --- 2. Create shared group --- # --- 2. Setup users ---
if ! getent group "$USER_GROUP" &>/dev/null; then log "WebUI user: $USER_NAME (group: $USER_GROUP)"
log "Creating shared group $USER_GROUP..."
groupadd --system "$USER_GROUP"
else
log "Group $USER_GROUP already exists."
fi
# --- 2a. Create daemon user (has sudo for privileged operations) --- # --- 2a. Create daemon user (has sudo for privileged operations) ---
if ! id "$USER_DAEMON_NAME" &>/dev/null; then if ! id "$USER_DAEMON_NAME" &>/dev/null; then
@@ -201,17 +202,7 @@ else
usermod -g "$USER_GROUP" "$USER_DAEMON_NAME" 2>/dev/null || true usermod -g "$USER_GROUP" "$USER_DAEMON_NAME" 2>/dev/null || true
fi fi
# --- 2b. Create web UI user (no sudo, communicates with daemon) --- # --- 2b. Setup Python venv ---
if ! id "$USER_NAME" &>/dev/null; then
log "Creating system user $USER_NAME..."
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin \
--gid "$USER_GROUP" "$USER_NAME"
else
log "User $USER_NAME already exists."
usermod -g "$USER_GROUP" "$USER_NAME" 2>/dev/null || true
fi
# --- 2c. Setup Python venv ---
if [[ -x "${PROJECT_DIR}/.venv/bin/python3" ]] && [[ "$_cli_force_venv" != true ]]; then if [[ -x "${PROJECT_DIR}/.venv/bin/python3" ]] && [[ "$_cli_force_venv" != true ]]; then
log "Python venv already exists, skipping (use --force-venv to recreate)." log "Python venv already exists, skipping (use --force-venv to recreate)."
else else
@@ -220,9 +211,10 @@ else
python3 -m venv "${PROJECT_DIR}/.venv" python3 -m venv "${PROJECT_DIR}/.venv"
"${PROJECT_DIR}/.venv/bin/pip" install -qe "${PROJECT_DIR}" "${PROJECT_DIR}/.venv/bin/pip" install -qe "${PROJECT_DIR}"
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/.venv" chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/.venv"
chmod -R g+x "${PROJECT_DIR}/.venv"
fi fi
# --- 2d. Install acme.sh (vendored) --- # --- 2c. Install acme.sh (vendored) ---
if [[ ! -x "$ACME_HOME/acme.sh" ]]; then if [[ ! -x "$ACME_HOME/acme.sh" ]]; then
log "Installing acme.sh (vendored)..." log "Installing acme.sh (vendored)..."
mkdir -p "$ACME_HOME" mkdir -p "$ACME_HOME"
@@ -242,11 +234,19 @@ mkdir -p "${PROJECT_DIR}/config"/{dnsmasq,nginx,wireguard,firewall}
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard,acme} 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
# Set ownership: daemon owns project dir, web UI user is group member # Set ownership: daemon owns project dir in prod, repo owner keeps ownership in dev
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "$PROJECT_DIR" if [[ "$_cli_is_dev" == true ]]; then
chmod -R g+rX "$PROJECT_DIR" _dev_owner="$USER_NAME"
else
_dev_owner="$USER_DAEMON_NAME"
fi
chown -R "$_dev_owner:$USER_GROUP" "$PROJECT_DIR"
chmod -R g+rwX "$PROJECT_DIR"
find "$PROJECT_DIR" -type d -exec chmod g+s '{}' +
# --- 4. Template rendering function --- # --- 4. Template rendering function ---
# Renders Jinja2 templates by injecting env vars as template context.
# Used for systemd units and sudoers files.
render_template() { render_template() {
export USER_NAME USER_DAEMON_NAME USER_GROUP PROJECT_DIR ACME_HOME export USER_NAME USER_DAEMON_NAME USER_GROUP PROJECT_DIR ACME_HOME
"${PROJECT_DIR}/.venv/bin/python3" -c " "${PROJECT_DIR}/.venv/bin/python3" -c "
@@ -272,12 +272,6 @@ render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-walld" \
visudo -cf /etc/sudoers.d/vacuum-walld || err "Invalid sudoers file!" visudo -cf /etc/sudoers.d/vacuum-walld || err "Invalid sudoers file!"
# WebUI user no longer has sudo — write minimal sudoers file
install -m 0440 /dev/null /etc/sudoers.d/vacuum-wall 2>/dev/null || true
echo "# WebUI user ($USER_NAME) has no sudo access." > /etc/sudoers.d/vacuum-wall
echo "# Privileged operations are handled by $USER_DAEMON_NAME via the daemon API." >> /etc/sudoers.d/vacuum-wall
visudo -cf /etc/sudoers.d/vacuum-wall || true
# --- 6. Install systemd units --- # --- 6. Install systemd units ---
log "Installing systemd units..." log "Installing systemd units..."
export USER_GROUP export USER_GROUP
@@ -305,18 +299,22 @@ log "Enabling firewalld..."
systemctl enable firewalld >/dev/null 2>&1 || warn "Could not enable firewalld (already running?)" 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)" 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 || true firewall-cmd --permanent --add-service=http >/dev/null 2>&1 && \
log "Added service http to public zone" log "Added service http to public zone" || \
firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true warn "Could not add service http to public zone (already exists?)"
log "Added service https to public zone" firewall-cmd --permanent --add-service=https >/dev/null 2>&1 && \
firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 || true log "Added service https to public zone" || \
log "Added service ssh to public zone" warn "Could not add service https to public zone (already exists?)"
firewall-cmd --reload >/dev/null 2>&1 || true firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1 && \
log "Firewalld rules reloaded" 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 --- # --- 9. Configure dnsmasq ---
log "Configuring dnsmasq..." log "Configuring dnsmasq..."
systemctl enable dnsmasq >/dev/null 2>&1 || true 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)" 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)" log "dnsmasq configured (will fully start after DHCP ranges are set)"
@@ -360,6 +358,10 @@ chown "$USER_NAME:$USER_GROUP" "${PROJECT_DIR}/data/nginx/.htpasswd"
# Remove default nginx site so vacuum-wall management config takes precedence # Remove default nginx site so vacuum-wall management config takes precedence
rm -f /etc/nginx/sites-enabled/default 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) # 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'
# Vacuum Wall - WebSocket upgrade map # Vacuum Wall - WebSocket upgrade map
@@ -455,7 +457,8 @@ log "Detecting network interfaces..."
# Auto-detect WAN (interface with default gateway) # Auto-detect WAN (interface with default gateway)
WAN_IFACE="${WAN_IFACE:-}" WAN_IFACE="${WAN_IFACE:-}"
if [[ -z "$WAN_IFACE" ]]; then if [[ -z "$WAN_IFACE" ]]; then
DETECTED_WAN=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}') # Strip @if<port> suffix — physical port index can change on reboot
DETECTED_WAN=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}' | cut -d'@' -f1)
if [[ -n "$DETECTED_WAN" ]]; then if [[ -n "$DETECTED_WAN" ]]; then
WAN_IFACE="$DETECTED_WAN" WAN_IFACE="$DETECTED_WAN"
log "Auto-detected WAN interface: $WAN_IFACE" log "Auto-detected WAN interface: $WAN_IFACE"
@@ -526,26 +529,35 @@ with open(os.path.join(p, 'config/firewall/config.json'), 'w') as f:
fi fi
# Apply zones via firewall-cmd (Python venv not yet fully available for apply_config) # 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 firewall-cmd --permanent --new-zone=internal >/dev/null 2>&1 && \
log "Created firewalld zone: internal" log "Created firewalld zone: internal" || \
firewall-cmd --permanent --zone=internal --set-target=ACCEPT >/dev/null 2>&1 || true warn "firewalld zone 'internal' may already exist"
firewall-cmd --permanent --zone=internal --add-service=dhcp >/dev/null 2>&1 || true firewall-cmd --permanent --zone=internal --set-target=ACCEPT >/dev/null 2>&1 || \
log "Added service dhcp to internal zone" warn "Could not set target ACCEPT on internal zone"
firewall-cmd --permanent --zone=internal --add-service=dns >/dev/null 2>&1 || true firewall-cmd --permanent --zone=internal --add-service=dhcp >/dev/null 2>&1 && \
log "Added service dns to internal zone" log "Added service dhcp to internal zone" || \
firewall-cmd --permanent --zone=internal --add-service=ntp >/dev/null 2>&1 || true warn "Could not add service dhcp to internal zone"
log "Added service ntp 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 || true firewall-cmd --permanent --new-zone=vpn >/dev/null 2>&1 && \
log "Created firewalld zone: vpn" log "Created firewalld zone: vpn" || \
firewall-cmd --permanent --zone=vpn --set-target=ACCEPT >/dev/null 2>&1 || true 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 # Apply masquerade on public/WAN
if [[ -n "$WAN_IFACE" ]]; then if [[ -n "$WAN_IFACE" ]]; then
firewall-cmd --permanent --zone=public --add-masquerade >/dev/null 2>&1 || true firewall-cmd --permanent --zone=public --add-masquerade >/dev/null 2>&1 && \
log "Enabled masquerade on public zone ($WAN_IFACE)" log "Enabled masquerade on public zone ($WAN_IFACE)" || \
firewall-cmd --permanent --zone=public --add-interface="$WAN_IFACE" >/dev/null 2>&1 || true warn "Could not enable masquerade on public zone"
log "Assigned $WAN_IFACE to 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 fi
# Assign LAN interfaces to internal zone # Assign LAN interfaces to internal zone
@@ -554,27 +566,24 @@ if [[ -n "$LAN_IFACES" ]]; then
for iface in "${LAN_ARRAY[@]}"; do for iface in "${LAN_ARRAY[@]}"; do
iface=$(echo "$iface" | xargs) iface=$(echo "$iface" | xargs)
[[ -z "$iface" ]] && continue [[ -z "$iface" ]] && continue
firewall-cmd --permanent --zone=internal --add-interface="$iface" >/dev/null 2>&1 || true firewall-cmd --permanent --zone=internal --add-interface="$iface" >/dev/null 2>&1 && \
log "Assigned $iface to internal zone" log "Assigned $iface to internal zone" || \
warn "Could not assign $iface to internal zone"
done done
fi fi
firewall-cmd --reload >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 && \
log "Firewalld rules reloaded" log "Firewalld rules reloaded" || \
warn "Could not reload firewalld rules"
# --- 13. Enable and start services --- # --- 13. Enable and start services ---
log "Enabling services..." log "Enabling services..."
systemctl enable nginx >/dev/null 2>&1 || true systemctl enable nginx >/dev/null 2>&1 && log "Enabled nginx" || warn "Could not enable nginx"
log "Enabled nginx" systemctl enable vacuum-walld >/dev/null 2>&1 && log "Enabled vacuum-walld" || warn "Could not enable vacuum-walld"
systemctl enable vacuum-walld >/dev/null 2>&1 || true systemctl enable vacuum-wall >/dev/null 2>&1 && log "Enabled vacuum-wall" || warn "Could not enable vacuum-wall"
log "Enabled vacuum-walld" 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 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 enable avahi-daemon >/dev/null 2>&1 || true systemctl enable avahi-daemon >/dev/null 2>&1 && log "Enabled avahi-daemon" || warn "Could not enable avahi-daemon"
log "Enabled avahi-daemon"
systemctl start avahi-daemon >/dev/null 2>&1 && log "Started avahi-daemon" || warn "Could not start 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 # Start daemon first, then web UI
@@ -611,6 +620,9 @@ nginx -t 2>/dev/null && nginx -s reload 2>/dev/null && log "Reloaded nginx" || \
if [[ -f "$ACME_HOME/account.conf" ]] && grep -q '^ACME_LEEMAIL=' "$ACME_HOME/account.conf" 2>/dev/null; then if [[ -f "$ACME_HOME/account.conf" ]] && grep -q '^ACME_LEEMAIL=' "$ACME_HOME/account.conf" 2>/dev/null; then
log "acme.sh account already registered, skipping." log "acme.sh account already registered, skipping."
else else
# acme.sh must never run as root — always as the service user via sudo -u.
# This prevents acme.sh from running any command as root and limits its
# ability to modify system files.
log "Registering acme.sh account with email $ACME_EMAIL..." log "Registering acme.sh account with email $ACME_EMAIL..."
mkdir -p "$ACME_HOME/www" mkdir -p "$ACME_HOME/www"
chown "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME/www" chown "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME/www"
-2
View File
@@ -1,2 +0,0 @@
# WebUI user ({{ USER_NAME }}) no longer has sudo access.
# Privileged operations are handled by vacuum-walld via the daemon API.
+1 -1
View File
@@ -33,7 +33,7 @@ LockPersonality=yes
SystemCallFilter=@system-service SystemCallFilter=@system-service
PrivateDevices=yes PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
IPAddressDeny=any IPAddressDeny=any
IPAddressAllow=localhost IPAddressAllow=localhost
NoNewPrivileges=yes NoNewPrivileges=yes