wireguard-ui/setup-linux-production.sh

471 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Guided production setup on Linux for WireGuard UI: optional complementary packages
# (openssl, curl, wireguard-tools, etc.; Caddy on Debian/Ubuntu), binary install, systemd,
# optional HTTPS + Caddy site, secret files (session, Android passkey SHA), optional FCM JSON.
# Run as root or with sudo. Does not overwrite existing files without prompting.
#
# Usage: sudo ./setup-linux-production.sh
set -euo pipefail
IFS=$'\n\t'
RED='\033[0;31m'
GRN='\033[0;32m'
YLW='\033[1;33m'
RST='\033[0m'
die() {
printf '%b%s%b\n' "$RED" "Error: $*" "$RST" >&2
exit 1
}
info() { printf '%b%s%b\n' "$GRN" "$*" "$RST"; }
warn() { printf '%b%s%b\n' "$YLW" "$*" "$RST"; }
require_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "Ejecutá este script con sudo o como root."
fi
}
trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
prompt() {
local msg="$1"
local default="${2:-}"
local val
if [[ -n "$default" ]]; then
read -r -p "$(printf '%s [%s]: ' "$msg" "$default")" val || true
val="$(trim "${val}")"
if [[ -z "$val" ]]; then
printf '%s' "$default"
else
printf '%s' "$val"
fi
else
read -r -p "$(printf '%s: ' "$msg")" val || true
printf '%s' "$(trim "${val}")"
fi
}
yes_no() {
local msg="$1"
local default_n="${2:-y}"
local hint="s/N"
[[ "$default_n" == "y" ]] && hint="S/n"
while true; do
read -r -p "${msg} [${hint}]: " a || true
a="$(trim "${a}")"
a="${a,,}"
if [[ -z "$a" ]]; then
[[ "$default_n" == "y" ]] && return 0
[[ "$default_n" == "n" ]] && return 1
fi
case "$a" in
s|si||y|yes) return 0 ;;
n|no) return 1 ;;
*) warn "Respondé s o n." ;;
esac
done
}
ensure_dir() {
install -d -m "$2" -o "$3" -g "$4" "$1"
}
random_hex() {
local n="${1:-32}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$n"
else
head -c "$(("$n" / 2))" /dev/urandom | xxd -p -c "$(("$n" / 2))"
fi
}
normalize_base_path() {
local p="$1"
p="$(trim "$p")"
p="${p#/}"
p="${p%/}"
if [[ -z "$p" ]]; then
printf '/'
else
printf '/%s' "$p"
fi
}
# Detect pkg manager binary on PATH (not full distro ID).
detect_pkg_mgr() {
if command -v apt-get >/dev/null 2>&1; then
printf '%s\n' apt
elif command -v dnf >/dev/null 2>&1; then
printf '%s\n' dnf
elif command -v yum >/dev/null 2>&1; then
printf '%s\n' yum
elif command -v apk >/dev/null 2>&1; then
printf '%s\n' apk
else
printf '%s\n' unknown
fi
}
# openssl: secretos rand; curl: repos; gnupg: claves Caddy/Debian.
# wireguard-tools: wg, wg-quick (útil si WGUI_ALLOW_WG_QUICK).
install_recommended_pkgs() {
local mgr="$1"
case "$mgr" in
apt)
info "Instalación con apt-get (openssl ca-certificates curl gnupg wireguard-tools)…"
apt-get update -qq
apt-get install -y openssl ca-certificates curl gnupg wireguard-tools
;;
dnf)
info "Instalación con dnf (openssl curl gnupg2 wireguard-tools + ca-certificates)…"
dnf install -y openssl curl gnupg2 wireguard-tools ca-certificates ||
die "dnf install falló."
;;
yum)
info "Instalación con yum (openssl curl gnupg2 wireguard-tools ca-certificates)…"
yum install -y openssl curl gnupg2 wireguard-tools ca-certificates ||
die "yum install falló."
;;
apk)
info "Instalación con apk (openssl ca-certificates curl wireguard-tools)…"
apk add --no-cache openssl ca-certificates curl wireguard-tools
;;
*)
warn "No hay apt-get/dnf/yum/apk: instalá manualmente openssl, ca-certificates, curl, herramientas WireGuard."
return 1
;;
esac
}
# Repo Cloudsmith estable de Caddy (Debian/Ubuntu + apt-get). Idempotente si ya está instalado.
ensure_caddy_apt_pkg() {
if command -v caddy >/dev/null 2>&1; then
return 0
fi
if ! command -v apt-get >/dev/null 2>&1; then
return 1
fi
if [[ "${DISTRO_DEBIANLIKE:-0}" -ne 1 ]]; then
return 1
fi
info "Instalando paquete Caddy (repos oficiales)…"
apt-get update -qq
apt-get install -y debian-keyring debian-archive-keyring curl gnupg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
apt-get update -qq
apt-get install -y caddy
}
# --- Main -----------------------------------------------------------------
require_root
info "=== Instalador guiado: WireGuard UI (systemd / secretos / Caddy opcional) ==="
warn "Revisá el README del proyecto sobre permisos (wg-quick, aplicar config, logs)."
echo
if yes_no "¿Este servidor es Debian/Ubuntu (apt)? Si no, se omitirá la instalación automática de Caddy pero podés usar el archivo generado más adelante." "n"; then
DISTRO_DEBIANLIKE=1
else
DISTRO_DEBIANLIKE=0
fi
echo
info "--- Paquetes complementarios ---"
warn "Útil tener openssl, TLS trust, curl, gpg y wg/wg-quick en el servidor."
if yes_no "¿Instalar paquetes recomendados para WireGuard UI (automático según el gestor detectado)?" "y"; then
PKG_MGR="$(detect_pkg_mgr)"
if [[ "$PKG_MGR" == "unknown" ]]; then
warn "No detecté apt/dnf/yum/apk."
else
info "Gestor detectado: $PKG_MGR"
install_recommended_pkgs "$PKG_MGR" || true
info "Paquetes recomendados instalados (o gestor omitido)."
fi
fi
if yes_no "¿Instalar ahora el paquete Caddy (reverse proxy HTTPS)? Solo automatizado si marcaste Debian/Ubuntu y existe apt-get." "n"; then
if ! ensure_caddy_apt_pkg; then
warn "No instalé Caddy automáticamente: podés hacerlo después o usar el bloque opcional HTTPS al final del script."
else
info "Caddy disponible como comando: $(command -v caddy)"
fi
fi
echo
WG_HOME="$(prompt 'Directorio de secretos y archivos generados (env, Caddy, etc.)' '/etc/wireguard-ui')"
DATA_DIR="$(prompt 'Directorio de datos (base de datos ./db del programa = WorkingDirectory systemd)' '/var/lib/wireguard-ui')"
BIN_DST="$(prompt 'Ruta del binario instalado (se copia o compilá vos)' '/usr/local/bin/wireguard-ui')"
APP_USER="$(prompt 'Usuario del servicio systemd (root suele hacer falta si usás wg-quick desde la UI)' 'root')"
APP_GROUP="$APP_USER"
if [[ "$APP_USER" != "root" ]] && ! getent passwd "$APP_USER" >/dev/null; then
if yes_no "El usuario '$APP_USER' no existe. ¿Crearlos como cuenta de sistema (sin login)?" "y"; then
useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" || die "No pude crear el usuario."
APP_GROUP="$APP_USER"
info "Usuario de sistema $APP_USER creado."
else
die "Creá el usuario antes de continuar o elegí otro valor."
fi
fi
ensure_dir "$WG_HOME" 0750 "$APP_USER" "$APP_GROUP"
ensure_dir "$DATA_DIR" 0750 "$APP_USER" "$APP_GROUP"
if yes_no "¿Compilar WireGuard UI desde ESTE repo (PWD actual) y guardar binario ahí?" "n"; then
REPO_ROOT="$(pwd)"
if [[ ! -f "$REPO_ROOT/go.mod" ]]; then
die "No encuentro go.mod en $(pwd); ejecutá el script desde la raíz del repo."
fi
if yes_no "¿Ejecutar prepare_assets.sh antes del build?" "y"; then
chmod +x "$REPO_ROOT/prepare_assets.sh" 2>/dev/null || true
(cd "$REPO_ROOT" && bash ./prepare_assets.sh)
fi
if ! command -v go >/dev/null 2>&1; then
die "Falta 'go' en el PATH para compilar."
fi
(cd "$REPO_ROOT" && go build -o "$BIN_DST" -trimpath .)
info "Binario compilado en $BIN_DST"
else
SRC_BIN="$(prompt "Ruta al binario ya existente (vacío si ya está en BIN_DST, dejar en blanco)" "")"
if [[ -n "$SRC_BIN" ]]; then
install -Dm755 "$SRC_BIN" "$BIN_DST"
info "Binario instalado desde $SRC_BIN$BIN_DST"
fi
fi
[[ -f "$BIN_DST" ]] || die "El binario no existe: $BIN_DST"
BIND_LOOPBACK="$(prompt 'Bind local del proceso (ej. 127.0.0.1 si lo tapa Caddy, 0.0.0.0 si es directo)' '127.0.0.1:5000')"
if ! yes_no "¿Es correcto BIND_ADDRESS='$BIND_LOOPBACK'?" "y"; then
BIND_LOOPBACK="$(prompt 'Nuevo BIND_ADDRESS (host:puerto ej. 0.0.0.0:5000)' "$BIND_LOOPBACK")"
fi
BASE_RAW="$(prompt 'BASE_PATH (sin barra inicial, ej: wg)' 'wg')"
BASE_PATH_NORM="$(normalize_base_path "$BASE_RAW")"
WGUI_ALLOW_WG_QUICK="false"
yes_no "¿Activar controles wg-quick desde la UI? (WGUI_ALLOW_WG_QUICK)" "y" && WGUI_ALLOW_WG_QUICK="true"
WGUI_SYNCCONF="false"
yes_no "¿Ejecutar wg syncconf después de aplicar wg.conf cuando sea posible? (WGUI_WG_SYNCCONF_AFTER_APPLY)" "y" && WGUI_SYNCCONF="true"
SESSION_FILE="$WG_HOME/session.secret"
if [[ ! -f "$SESSION_FILE" ]] || yes_no "Ya existe $SESSION_FILE. ¿Regenerar secreto de sesión?" "n"; then
umask 077
printf '%s' "$(random_hex 64)" >"$SESSION_FILE"
chown "$APP_USER:$APP_GROUP" "$SESSION_FILE"
chmod 600 "$SESSION_FILE"
umask 022
info "Creado $SESSION_FILE"
fi
LOG_TAIL="$(prompt 'Ruta de log opcional para cola desde la UI (WGUI_LOG_TAIL_PATH, puede quedar vacío)' '/var/log/wireguard-ui.log')"
if [[ -n "$LOG_TAIL" ]]; then
touch "$LOG_TAIL" 2>/dev/null || true
if [[ ! -f "$LOG_TAIL" ]]; then
install -Dm644 /dev/null "$LOG_TAIL" || warn "No pude crear $LOG_TAIL manualmente después."
fi
chown "$APP_USER:$APP_GROUP" "$LOG_TAIL" 2>/dev/null || true
fi
ANDROID_SHA_FILE="$WG_HOME/android-SHA.secret"
ANDROID_SHA_VAL=""
PASSKEY_SETUP="false"
WEBAUTH_RP_ID=""
WEBAUTH_ORIGINS=""
WEBAUTH_DISP="WireGuard UI"
ANDROID_PKG=""
if yes_no "¿Configurás Passkeys/Android (necesita HTTPS público típicamente)?" "n"; then
PASSKEY_SETUP="true"
WEBAUTH_RP_ID="$(prompt 'WGUI_WEBAUTHN_RP_ID (hostname verificado por el navegador, sin https)' 'vpn.example.net')"
WEBAUTH_ORIGINS="$(prompt 'WGUI_WEBAUTHN_RP_ORIGINS separados por coma (ej https://vpn.example.net,https://www.vpn.example.net)' '')"
WEBAUTH_DISP="$(prompt 'WGUI_WEBAUTHN_RP_DISPLAY_NAME' 'WireGuard UI')"
ANDROID_PKG="$(prompt 'WGUI_ANDROID_PASSKEY_PACKAGE (applicationId Android, opcional)' 'com.wireguardui.wireguard_ui_client')"
warn "Fingerprint SHA256 del APK (release): obtenelo con el signing report de Gradle o keytool."
echo " Ej.: keytool -list -v -keystore tu.keystore -alias upload | grep 'SHA256'"
if yes_no "¿Pegar el fingerprint ahora (se guarda en $ANDROID_SHA_FILE)?" "y"; then
read -r -p "Pegá el SHA-256 (con o sin dos puntos): " ANDROID_SHA_VAL || true
ANDROID_SHA_VAL="$(trim "$ANDROID_SHA_VAL")"
if [[ -n "$ANDROID_SHA_VAL" ]]; then
umask 077
printf '%s\n' "$ANDROID_SHA_VAL" >"$ANDROID_SHA_FILE"
chown "$APP_USER:$APP_GROUP" "$ANDROID_SHA_FILE"
chmod 600 "$ANDROID_SHA_FILE"
umask 022
info "Guardado $ANDROID_SHA_FILE"
fi
else
warn "Después copiá el contenido en $ANDROID_SHA_FILE y restringí permisos (600)."
fi
fi
FCM_PATH=""
if yes_no "¿Instalar credenciales Firebase (FCM) para push al cliente Android?" "n"; then
FCM_SRC="$(prompt 'Ruta al JSON de cuenta de servicio en ESTA máquina (se copia a $WG_HOME)' '')"
[[ -n "$FCM_SRC" ]] || die "Ruta FCM vacía."
[[ -f "$FCM_SRC" ]] || die "No existe el archivo: $FCM_SRC"
FCM_PATH="$WG_HOME/firebase-service-account.json"
install -Dm600 -o "$APP_USER" -g "$APP_GROUP" "$FCM_SRC" "$FCM_PATH"
info "FCM en $FCM_PATH"
fi
ENV_FILE="$WG_HOME/wireguard-ui.env"
if [[ -f "$ENV_FILE" ]] && ! yes_no "Sobrescribir $ENV_FILE ?" "n"; then
ENV_FILE_BAK="$WG_HOME/wireguard-ui.env.bak.$(date +%Y%m%d%H%M%S)"
cp -a "$ENV_FILE" "$ENV_FILE_BAK"
info "Backup: $ENV_FILE_BAK"
fi
{
echo "# Generado por setup-linux-production.sh — no commitear"
echo "BIND_ADDRESS=$BIND_LOOPBACK"
echo "BASE_PATH=$BASE_PATH_NORM"
echo "WGUI_ALLOW_WG_QUICK=$WGUI_ALLOW_WG_QUICK"
echo "WGUI_WG_SYNCCONF_AFTER_APPLY=$WGUI_SYNCCONF"
echo "SESSION_SECRET_FILE=$SESSION_FILE"
if [[ -n "$LOG_TAIL" ]]; then
echo "WGUI_LOG_TAIL_PATH=$LOG_TAIL"
fi
if [[ "$PASSKEY_SETUP" == "true" ]]; then
echo "WGUI_WEBAUTHN_RP_ID=$WEBAUTH_RP_ID"
echo "WGUI_WEBAUTHN_RP_ORIGINS=$WEBAUTH_ORIGINS"
echo "WGUI_WEBAUTHN_RP_DISPLAY_NAME=$WEBAUTH_DISP"
if [[ -f "$ANDROID_SHA_FILE" ]]; then
echo "WGUI_ANDROID_PASSKEY_SHA256=$ANDROID_SHA_FILE"
fi
if [[ -n "$ANDROID_PKG" ]]; then
echo "WGUI_ANDROID_PASSKEY_PACKAGE=$ANDROID_PKG"
fi
fi
if [[ -n "$FCM_PATH" ]]; then
echo "FCM_CREDENTIALS_FILE=$FCM_PATH"
fi
} >"$ENV_FILE.tmp"
install -Dm600 -o "$APP_USER" -g "$APP_GROUP" "$ENV_FILE.tmp" "$ENV_FILE"
rm -f "$ENV_FILE.tmp"
info "Variables de entorno: $ENV_FILE"
UNIT_PATH="/etc/systemd/system/wireguard-ui.service"
cat >"$UNIT_PATH.tmp" <<UNIT
[Unit]
Description=WireGuard UI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$APP_USER
Group=$APP_GROUP
WorkingDirectory=$DATA_DIR
EnvironmentFile=$ENV_FILE
ExecStart=$BIN_DST
Restart=on-failure
RestartSec=5
# Ajustá capabilities si corrés sin root y necesitás wg syncconf:
# AmbientCapabilities=CAP_NET_ADMIN
# CapabilityBoundingSet=CAP_NET_ADMIN
[Install]
WantedBy=multi-user.target
UNIT
install -Dm644 "$UNIT_PATH.tmp" "$UNIT_PATH"
rm -f "$UNIT_PATH.tmp"
info "Unidad systemd: $UNIT_PATH"
systemctl daemon-reload
if yes_no "¿Habilitar e iniciar wireguard-ui ahora?" "y"; then
systemctl enable --now wireguard-ui.service
systemctl --no-pager -l status wireguard-ui.service || true
else
warn "Activá manualmente: systemctl enable --now wireguard-ui"
fi
# --- Caddy opcional --------------------------------------------------------
if yes_no "¿Instalar y configurar Caddy para HTTPS (Let's Encrypt)?" "n"; then
DOMAIN="$(prompt 'Nombre DNS público que apunta a este servidor (ej. vpn.example.net)' '')"
[[ -n "$DOMAIN" ]] || die "Dominio vacío."
ACME_EMAIL="$(prompt "Email para Let's Encrypt (ACME)" 'admin@example.com')"
if [[ "$DISTRO_DEBIANLIKE" -eq 1 ]]; then
if ! ensure_caddy_apt_pkg && ! command -v caddy >/dev/null 2>&1; then
warn "Caddy sigue ausente tras intentar instalación Debian/Ubuntu; instalalo a mano o repetí los pasos Cloudsmith desde la documentación de Caddy."
fi
else
warn "Instalá Caddy manualmente; se generará solo el fragmento de sitio."
fi
BACKEND="http://$BIND_LOOPBACK"
# Si BIND es 0.0.0.0:5000, Caddy debe hablar a 127.0.0.1
BACKEND="${BACKEND/0.0.0.0/127.0.0.1}"
CADDY_FRAG="$WG_HOME/caddy-wireguard-ui.caddy"
cat >"$CADDY_FRAG" <<CADDY
# Fragmento generado — importar desde /etc/caddy/Caddyfile (ver abajo)
$DOMAIN {
encode zstd gzip
tls $ACME_EMAIL
reverse_proxy $BACKEND
}
CADDY
chmod 644 "$CADDY_FRAG"
info "Fragmento Caddy escrito: $CADDY_FRAG"
MAIN_CF="/etc/caddy/Caddyfile"
MARK_BEGIN="# --- wireguard-ui-installer BEGIN ---"
MARK_END="# --- wireguard-ui-installer END ---"
if [[ -f "$MAIN_CF" ]] && grep -qF "$MARK_BEGIN" "$MAIN_CF" 2>/dev/null; then
warn "Tu Caddyfile ya contiene el bloque del instalador; no lo dupliqué."
else
if [[ -f "$MAIN_CF" ]]; then
{
echo ""
echo "$MARK_BEGIN"
echo "import $CADDY_FRAG"
echo "$MARK_END"
} >>"$MAIN_CF"
info "Añadí 'import $CADDY_FRAG' al final de $MAIN_CF"
else
warn "No existe $MAIN_CF. Creá uno mínimo con:"
echo " import $CADDY_FRAG"
fi
fi
if command -v caddy >/dev/null 2>&1; then
if caddy validate --config "$MAIN_CF" 2>/dev/null; then
if yes_no "¿Recargar Caddy ahora (systemctl reload caddy)?" "y"; then
systemctl enable caddy 2>/dev/null || true
systemctl reload caddy || systemctl restart caddy
fi
else
warn "Revisá la sintaxis: caddy validate --config $MAIN_CF"
fi
fi
info "Abrí https://$DOMAIN${BASE_PATH_NORM}/ (BASE_PATH sin doble barra)."
warn "Passkeys: publicá assetlinks en la raíz HTTPS del host (ver README) además del panel bajo BASE_PATH."
fi
echo
info "Listo. Resumen:"
echo " Binario: $BIN_DST"
echo " Entorno: $ENV_FILE"
echo " Servicio: systemctl status wireguard-ui"
echo " Datos (db): $DATA_DIR"
echo " Panel (local): revisá BIND_ADDRESS y BASE_PATH en $ENV_FILE"
echo
warn "Seguridad: restringí permisos en $WG_HOME, rotá secretos si filtraron, y usá firewall solo donde corresponda."