refactor: introduce two-user daemon architecture with socket-based communication
- Add daemon/ module with aiohttp server, sync client, and handler registry - Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard) - Add system/acme-deploy.py, vacuum-walld sudoers and systemd service - Update API routes to use daemon client instead of lib/ directly - Update lib/, tests/, and webui/ for new architecture - Update docs and deployment scripts
This commit is contained in:
+91
-22
@@ -139,6 +139,8 @@ fi
|
||||
|
||||
# Optional settings with defaults
|
||||
USER_NAME="${_cli_user:-${USER_NAME:-vacuum-wall}}"
|
||||
# Daemon user name (derived from web UI user name)
|
||||
USER_DAEMON_NAME="${USER_NAME}d"
|
||||
|
||||
# --- Safety check: running service as a non-system regular user ---
|
||||
if [[ "$_cli_is_dev" != true ]] && [[ "$USER_NAME" != "vacuum-wall" ]] && id "$USER_NAME" &>/dev/null; then
|
||||
@@ -147,15 +149,20 @@ if [[ "$_cli_is_dev" != true ]] && [[ "$USER_NAME" != "vacuum-wall" ]] && id "$U
|
||||
_shell=$(getent passwd "$USER_NAME" | cut -d: -f7)
|
||||
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 "This grants NOPASSWD sudo and 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 "Only use for development. For production, use --user vacuum-wall."
|
||||
fi
|
||||
fi
|
||||
# Shared group for both users to access project files and socket
|
||||
USER_GROUP="vacuum-wall"
|
||||
|
||||
echo "============================================"
|
||||
echo " Vacuum Wall Appliance Installer"
|
||||
echo " Install dir: $PROJECT_DIR"
|
||||
echo " System user: $USER_NAME"
|
||||
echo " Install dir: $PROJECT_DIR"
|
||||
echo " WebUI user: $USER_NAME"
|
||||
echo " Daemon user: $USER_DAEMON_NAME"
|
||||
echo " Shared group: $USER_GROUP"
|
||||
echo " Management domain: $DOMAIN"
|
||||
echo "============================================"
|
||||
|
||||
@@ -176,15 +183,35 @@ apt-get install -y -qq \
|
||||
apache2-utils \
|
||||
avahi-daemon
|
||||
|
||||
# --- 2. Create system user (home set to PROJECT_DIR for env, no-create) ---
|
||||
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"
|
||||
# --- 2. Create shared group ---
|
||||
if ! getent group "$USER_GROUP" &>/dev/null; then
|
||||
log "Creating shared group $USER_GROUP..."
|
||||
groupadd --system "$USER_GROUP"
|
||||
else
|
||||
log "User $USER_NAME already exists."
|
||||
log "Group $USER_GROUP already exists."
|
||||
fi
|
||||
|
||||
# --- 2b. Setup Python venv ---
|
||||
# --- 2a. Create daemon user (has sudo for privileged operations) ---
|
||||
if ! id "$USER_DAEMON_NAME" &>/dev/null; then
|
||||
log "Creating system user $USER_DAEMON_NAME..."
|
||||
useradd --system --home-dir "$PROJECT_DIR" --no-create-home --shell /usr/sbin/nologin \
|
||||
--gid "$USER_GROUP" "$USER_DAEMON_NAME"
|
||||
else
|
||||
log "User $USER_DAEMON_NAME already exists."
|
||||
usermod -g "$USER_GROUP" "$USER_DAEMON_NAME" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- 2b. Create web UI user (no sudo, communicates with daemon) ---
|
||||
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
|
||||
log "Python venv already exists, skipping (use --force-venv to recreate)."
|
||||
else
|
||||
@@ -192,16 +219,16 @@ else
|
||||
rm -rf "${PROJECT_DIR}/.venv"
|
||||
python3 -m venv "${PROJECT_DIR}/.venv"
|
||||
"${PROJECT_DIR}/.venv/bin/pip" install -qe "${PROJECT_DIR}"
|
||||
chown -R "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/.venv"
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/.venv"
|
||||
fi
|
||||
|
||||
# --- 2c. Install acme.sh (vendored) ---
|
||||
# --- 2d. Install acme.sh (vendored) ---
|
||||
if [[ ! -x "$ACME_HOME/acme.sh" ]]; then
|
||||
log "Installing acme.sh (vendored)..."
|
||||
mkdir -p "$ACME_HOME"
|
||||
cp "${PROJECT_DIR}/vendor/acme.sh" "$ACME_HOME/acme.sh"
|
||||
chmod +x "$ACME_HOME/acme.sh"
|
||||
chown -R "$USER_NAME:$USER_NAME" "$ACME_HOME"
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME"
|
||||
else
|
||||
log "acme.sh already installed."
|
||||
fi
|
||||
@@ -215,16 +242,21 @@ mkdir -p "${PROJECT_DIR}/config"/{dnsmasq,nginx,wireguard,firewall}
|
||||
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard,acme}
|
||||
mkdir -p /etc/wireguard
|
||||
mkdir -p /etc/dnsmasq
|
||||
# Set ownership: daemon owns project dir, web UI user is group member
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "$PROJECT_DIR"
|
||||
chmod -R g+rX "$PROJECT_DIR"
|
||||
|
||||
# --- 4. Template rendering function ---
|
||||
render_template() {
|
||||
export USER_NAME PROJECT_DIR ACME_HOME
|
||||
export USER_NAME USER_DAEMON_NAME USER_GROUP PROJECT_DIR ACME_HOME
|
||||
"${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'],
|
||||
'USER_DAEMON_NAME': os.environ.get('USER_DAEMON_NAME', ''),
|
||||
'USER_GROUP': os.environ.get('USER_GROUP', ''),
|
||||
'PROJECT_DIR': os.environ['PROJECT_DIR'],
|
||||
'ACME_HOME': os.environ['ACME_HOME'],
|
||||
}
|
||||
@@ -234,13 +266,24 @@ print(Template(text).render(**env), end='')
|
||||
|
||||
# --- 5. Install sudoers ---
|
||||
log "Installing sudoers whitelist..."
|
||||
render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-wall" \
|
||||
| install -m 0440 /dev/stdin /etc/sudoers.d/vacuum-wall
|
||||
export USER_DAEMON_NAME
|
||||
render_template "${PROJECT_DIR}/system/sudoers.d/vacuum-walld" \
|
||||
| install -m 0440 /dev/stdin /etc/sudoers.d/vacuum-walld
|
||||
|
||||
visudo -cf /etc/sudoers.d/vacuum-wall || 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 ---
|
||||
log "Installing systemd units..."
|
||||
export USER_GROUP
|
||||
render_template "${PROJECT_DIR}/system/systemd/vacuum-walld.service" \
|
||||
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-walld.service
|
||||
|
||||
render_template "${PROJECT_DIR}/system/systemd/vacuum-wall.service" \
|
||||
| install -m 0644 /dev/stdin /etc/systemd/system/vacuum-wall.service
|
||||
|
||||
@@ -291,7 +334,7 @@ else
|
||||
-addext "subjectAltName=DNS:$DOMAIN"
|
||||
fi
|
||||
|
||||
chown -R "$USER_NAME:$USER_NAME" "$ACME_HOME"
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME"
|
||||
|
||||
# Generate/update htpasswd directly in data/nginx/
|
||||
HTPASSWD_FILE="${PROJECT_DIR}/data/nginx/.htpasswd"
|
||||
@@ -312,7 +355,7 @@ with open(os.environ['HTFILE'], 'w') as f:
|
||||
warn "Could not generate htpasswd (install apache2-utils or python3-crypt)"
|
||||
fi
|
||||
|
||||
chown "$USER_NAME:$USER_NAME" "${PROJECT_DIR}/data/nginx/.htpasswd"
|
||||
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
|
||||
@@ -523,6 +566,8 @@ log "Firewalld rules reloaded"
|
||||
log "Enabling services..."
|
||||
systemctl enable nginx >/dev/null 2>&1 || true
|
||||
log "Enabled nginx"
|
||||
systemctl enable vacuum-walld >/dev/null 2>&1 || true
|
||||
log "Enabled vacuum-walld"
|
||||
systemctl enable vacuum-wall >/dev/null 2>&1 || true
|
||||
log "Enabled vacuum-wall"
|
||||
systemctl enable vacuum-wall-acme.timer >/dev/null 2>&1 || true
|
||||
@@ -532,7 +577,30 @@ systemctl enable avahi-daemon >/dev/null 2>&1 || true
|
||||
log "Enabled 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
|
||||
|
||||
systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI"
|
||||
|
||||
nginx -t 2>/dev/null && nginx -s reload 2>/dev/null && log "Reloaded nginx" || \
|
||||
@@ -545,8 +613,8 @@ if [[ -f "$ACME_HOME/account.conf" ]] && grep -q '^ACME_LEEMAIL=' "$ACME_HOME/ac
|
||||
else
|
||||
log "Registering acme.sh account with email $ACME_EMAIL..."
|
||||
mkdir -p "$ACME_HOME/www"
|
||||
chown "$USER_NAME:$USER_NAME" "$ACME_HOME/www"
|
||||
sudo -u "$USER_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \
|
||||
chown "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME/www"
|
||||
sudo -u "$USER_DAEMON_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \
|
||||
"$ACME_HOME/acme.sh" --home "$ACME_HOME" --config-home "$ACME_HOME" \
|
||||
--register-account -m "$ACME_EMAIL" 2>/dev/null || \
|
||||
warn "Could not register acme.sh account (will be done from WebUI)"
|
||||
@@ -560,8 +628,9 @@ echo "============================================"
|
||||
echo ""
|
||||
echo " Management UI: https://$DOMAIN"
|
||||
echo " User: $MGMT_USER"
|
||||
echo " WebUI service: vacuum-wall.service"
|
||||
echo " ACME renewal: vacuum-wall-acme.timer"
|
||||
echo " Daemon service: vacuum-walld.service"
|
||||
echo " WebUI service: vacuum-wall.service"
|
||||
echo " ACME renewal: vacuum-wall-acme.timer"
|
||||
echo ""
|
||||
echo " Firewall zones:"
|
||||
if [[ -n "$WAN_IFACE" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user