nextwks.sh: replace full root escalation with per-command sudo, files owned by user

This commit is contained in:
Claus Lohmar 2026-07-11 00:34:21 +01:00
parent d41d1443a8
commit 3ca19403fa

View file

@ -21,16 +21,10 @@ usage() {
MODE="${1#--}"
case "$MODE" in install|update|destroy) ;; *) usage ;; esac
# --- Auto-escalate to root if not already ---
if [ "$(id -u)" -ne 0 ]; then
echo "[*] Escalating to root..."
exec sudo bash "$(realpath "$0")" "$@"
fi
# Determine the non-root user for file ownership
RUN_USER="${SUDO_USER:-${USER:-master}}"
RUN_UID=$(id -u "$RUN_USER" 2>/dev/null || echo 1000)
RUN_GID=$(id -g "$RUN_USER" 2>/dev/null || echo 1000)
# Helper: run with sudo if not already root
maybe_sudo() {
if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi
}
# --- Load existing env (if any) ---
if [ -f "$BACKUP_DIR/.env" ]; then
@ -47,18 +41,19 @@ echo "=== NextWorkspace ${MODE} ==="
# ============================================================
if [ "$MODE" = "install" ]; then
echo "[*] Installing system dependencies..."
apt-get update -qq && apt-get install -y -qq git build-essential curl podman podman-compose
maybe_sudo apt-get update -qq
maybe_sudo apt-get install -y -qq git build-essential curl podman podman-compose
if ! command -v go &>/dev/null; then
echo "[*] Installing Go..."
GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text)
GO_URL="https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
curl -sL "$GO_URL" -o /tmp/go.tar.gz
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
maybe_sudo rm -rf /usr/local/go
maybe_sudo tar -C /usr/local -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
chmod +x /etc/profile.d/go.sh
maybe_sudo sh -c 'echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/go.sh'
maybe_sudo chmod +x /etc/profile.d/go.sh
export PATH=$PATH:/usr/local/go/bin
fi
@ -70,7 +65,7 @@ if [ "$MODE" = "install" ]; then
while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done
read -p "Admin username: " ADMIN_USERNAME
while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done
ADMIN_PASSWORD=$(openssl rand -hex 6) # 12 chars, no pipefail issues
ADMIN_PASSWORD=$(openssl rand -hex 6)
echo ""
echo "========================================"
@ -92,8 +87,8 @@ if [ "$MODE" = "install" ]; then
read -p "IMAP port [993]: " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}"
# Persist config to backup vault
mkdir -p "$BACKUP_DIR"
cat > "$BACKUP_DIR/.env" <<EOF
maybe_sudo mkdir -p "$BACKUP_DIR"
maybe_sudo bash -c "cat > '$BACKUP_DIR/.env' <<EOF
# NextWorkspace Configuration — auto-generated by nextwks.sh --install
DOMAIN=$DOMAIN
TLS_EMAIL=$TLS_EMAIL
@ -105,8 +100,8 @@ SMTP_USER=$SMTP_USER
SMTP_PASS=$SMTP_PASS
IMAP_HOST=$IMAP_HOST
IMAP_PORT=$IMAP_PORT
EOF
chmod 600 "$BACKUP_DIR/.env"
EOF"
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
fi
# ============================================================
@ -119,10 +114,9 @@ cd "$BUILD_DIR"
# Save script to user's home for easy future access (--install only)
if [ "$MODE" = "install" ]; then
USER_HOME=$(eval echo "~${SUDO_USER:-}" 2>/dev/null || echo "$HOME")
cp "$BUILD_DIR/tools/nextwks.sh" "$USER_HOME/nextwks.sh"
chmod +x "$USER_HOME/nextwks.sh"
echo "[*] Saved to $USER_HOME/nextwks.sh — use it for future updates"
cp "$BUILD_DIR/tools/nextwks.sh" "$HOME/nextwks.sh"
chmod +x "$HOME/nextwks.sh"
echo "[*] Saved to $HOME/nextwks.sh — use it for future updates"
fi
# ============================================================
@ -133,58 +127,61 @@ export PATH=$PATH:/usr/local/go/bin
CGO_ENABLED=0 go build -o nextworkspace .
# ============================================================
# 4. CREATE target directory structure
# 4. CREATE target & backup directories (as root)
# ============================================================
mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" \
"$BACKUP_DIR"
# ============================================================
# 5. TEARDOWN (destroy mode only — wipes target dir)
# ============================================================
if [ "$MODE" = "destroy" ]; then
echo "[*] Full teardown..."
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
maybe_sudo rm -rf "$TARGET_DIR"
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
fi
# ============================================================
# 6. COPY artifacts to target
# 6. COPY artifacts to target (as root)
# ============================================================
echo "[*] Copying artifacts..."
cp nextworkspace "$TARGET_DIR/nextworkspace"
cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION"
maybe_sudo cp nextworkspace "$TARGET_DIR/nextworkspace"
maybe_sudo cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION"
if [ -d "$BUILD_DIR/config/www" ]; then
cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/"
maybe_sudo cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/"
fi
if [ -d "$BUILD_DIR/lng" ]; then
rm -rf "$TARGET_DIR/lng"
cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng"
maybe_sudo rm -rf "$TARGET_DIR/lng"
maybe_sudo cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng"
fi
if [ -d "$BUILD_DIR/config/nextworkspace" ]; then
cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/"
maybe_sudo cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/"
fi
# Restore .env from backup
if [ -f "$BACKUP_DIR/.env" ]; then
cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
chmod 644 "$TARGET_DIR/.env"
maybe_sudo cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
maybe_sudo chmod 644 "$TARGET_DIR/.env"
fi
# ============================================================
# 7. GENERATE config files with placeholder substitution
# (install + destroy modes; update skips to keep secrets stable)
# Write to /tmp first, then sudo cp to target
# ============================================================
GEN_DIR=$(mktemp -d)
trap "rm -rf '$GEN_DIR'" EXIT
if [ "$MODE" != "update" ]; then
echo "[*] Generating config files..."
# Caddyfile
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$BUILD_DIR/config/caddy/Caddyfile" > "$TARGET_DIR/config/caddy/Caddyfile"
"$BUILD_DIR/config/caddy/Caddyfile" > "$GEN_DIR/Caddyfile"
# Authelia config — preserve existing secrets if present
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
@ -195,7 +192,7 @@ if [ "$MODE" != "update" ]; then
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \
-e "s|{SMTP_PASS}|$SMTP_PASS|g" \
"$BUILD_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml"
"$BUILD_DIR/config/authelia/configuration.yml" > "$GEN_DIR/configuration.yml"
# Users database
ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}"
@ -204,53 +201,56 @@ if [ "$MODE" != "update" ]; then
fi
sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \
-e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
"$BUILD_DIR/config/authelia/users_database.yml" > "$TARGET_DIR/config/authelia/users_database.yml"
"$BUILD_DIR/config/authelia/users_database.yml" > "$GEN_DIR/users_database.yml"
# Copy generated configs to target
maybe_sudo cp "$GEN_DIR/Caddyfile" "$TARGET_DIR/config/caddy/Caddyfile"
maybe_sudo cp "$GEN_DIR/configuration.yml" "$TARGET_DIR/config/authelia/configuration.yml"
maybe_sudo cp "$GEN_DIR/users_database.yml" "$TARGET_DIR/config/authelia/users_database.yml"
# Persist generated secrets so --destroy is idempotent
if [ -f "$BACKUP_DIR/.env" ]; then
# Update existing .env with any new secrets
sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true
maybe_sudo sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true
fi
echo "JWT_SECRET=$JWT_SECRET" >> "$BACKUP_DIR/.env"
echo "SESSION_SECRET=$SESSION_SECRET" >> "$BACKUP_DIR/.env"
[ -n "$ADMIN_PASSWORD_HASH" ] && echo "ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH" >> "$BACKUP_DIR/.env"
chmod 600 "$BACKUP_DIR/.env"
maybe_sudo bash -c "echo 'JWT_SECRET=$JWT_SECRET' >> '$BACKUP_DIR/.env'"
maybe_sudo bash -c "echo 'SESSION_SECRET=$SESSION_SECRET' >> '$BACKUP_DIR/.env'"
[ -n "$ADMIN_PASSWORD_HASH" ] && maybe_sudo bash -c "echo 'ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH' >> '$BACKUP_DIR/.env'"
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
fi
# ============================================================
# 8. FIX OWNERSHIP — chown to the non-root user
# 8. FIX OWNERSHIP — all files in TARGET_DIR/BACKUP_DIR to user
# ============================================================
RUN_USER="${SUDO_USER:-${USER}}"
echo "[*] Setting file ownership to $RUN_USER..."
chown -R "$RUN_UID:$RUN_GID" "$TARGET_DIR" 2>/dev/null || true
chown -R "$RUN_UID:$RUN_GID" "$BACKUP_DIR" 2>/dev/null || true
maybe_sudo chown -R "$RUN_USER:" "$TARGET_DIR" 2>/dev/null || true
maybe_sudo chown -R "$RUN_USER:" "$BACKUP_DIR" 2>/dev/null || true
# ============================================================
# 9. STOP old containers (all modes)
# 9. STOP old containers (all modes, as root)
# ============================================================
echo "[*] Stopping any previous containers..."
for c in caddy authelia launcher; do
podman stop "$c" 2>/dev/null && echo " stopped $c" || true
podman rm "$c" 2>/dev/null && echo " removed $c" || true
maybe_sudo podman stop "$c" 2>/dev/null && echo " stopped $c" || true
maybe_sudo podman rm "$c" 2>/dev/null && echo " removed $c" || true
done
sleep 1
# ============================================================
# 10. PORT CHECK — before deploy
# ============================================================
if ss -tlnp 2>/dev/null | grep -q ':80 '; then
if maybe_sudo ss -tlnp 2>/dev/null | grep -q ':80 '; then
echo "=============================================="
echo "[WARN] Port 80 is already in use."
ss -tlnp 2>/dev/null | grep ':80 '
maybe_sudo ss -tlnp 2>/dev/null | grep ':80 '
echo ""
echo " This is usually Caddy from a previous run."
echo " Run this to stop it, then re-run deploy:"
echo " podman stop caddy && podman rm caddy"
echo "=============================================="
echo ""
read -p "Stop the container on port 80 now? [y/N]: " KILL
if [ "$KILL" = "y" ] || [ "$KILL" = "Y" ]; then
podman stop caddy 2>/dev/null || true
podman rm caddy 2>/dev/null || true
maybe_sudo podman stop caddy 2>/dev/null || true
maybe_sudo podman rm caddy 2>/dev/null || true
sleep 2
echo "[OK] Port 80 freed."
else
@ -260,35 +260,34 @@ if ss -tlnp 2>/dev/null | grep -q ':80 '; then
fi
# ============================================================
# 11. DEPLOY stack
# 11. DEPLOY stack (as root — needs port 80/443)
# ============================================================
echo "[*] Deploying containers on $NETWORK_NAME..."
# Ensure network exists
podman network create "$NETWORK_NAME" 2>/dev/null || true
maybe_sudo podman network create "$NETWORK_NAME" 2>/dev/null || true
# AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret)
AUTHELIA_SECRET="${SESSION_SECRET:-}"
if [ -z "$AUTHELIA_SECRET" ]; then
# Update mode — extract from existing config
AUTHELIA_SECRET=$(sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \
AUTHELIA_SECRET=$(maybe_sudo sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \
| grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "")
fi
# Generate compose file with substituted secret
sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \
"$BUILD_DIR/compose/stack.yaml" > "$TARGET_DIR/compose/stack.yaml"
"$BUILD_DIR/compose/stack.yaml" > "$GEN_DIR/stack.yaml"
maybe_sudo cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml"
podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
maybe_sudo podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
sleep 1
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
maybe_sudo podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
# ============================================================
# 11. HEALTH CHECK
# 12. HEALTH CHECK
# ============================================================
echo "[*] Running health check..."
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
HEALTH=$(podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "")
HEALTH=$(maybe_sudo podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "")
if [ "$HEALTH" = "OK" ]; then
echo "[OK] NextWorkspace launcher is healthy"
echo "[OK] https://$DOMAIN/"
@ -300,8 +299,8 @@ done
echo "[FAIL] Health check failed — launcher did not respond"
echo ""
echo "--- Container status ---"
podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true
maybe_sudo podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true
echo ""
echo "--- Launcher logs (last 20 lines) ---"
podman logs launcher --tail 20 2>/dev/null || echo " (no logs)"
maybe_sudo podman logs launcher --tail 20 2>/dev/null || echo " (no logs)"
exit 1