Files
lxc-enroll/lxc-enroll.sh
T

439 lines
22 KiB
Bash

#!/usr/bin/env bash
# Run as root on a Proxmox host. No credentials or site-specific addresses here.
set -Eeuo pipefail
set +x
umask 077
fail() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }
ask() { printf '%s ' "$1" >&2; IFS= read -r REPLY <&3 || fail 'Eingabe abgebrochen.'; }
secret() {
printf '%s ' "$1" >&2
IFS= read -r -s REPLY <&3 || fail 'Eingabe abgebrochen.'
printf '\n' >&2
}
installed_label() { if [[ $1 == 1 ]]; then printf 'vorhanden'; else printf 'fehlt'; fi; }
LIST=0; DRY=0; MANAGER=''; VERSION=''; LOGIN_SERVER=''; SELECTION=''; VERSION_EXPLICIT=0
SSH_ENABLED=1; SSH_USER=''; SSH_SUDO=0; SSH_NOPASSWD=0; SAVE_SETTINGS=1
VARS_FILE=/etc/lxc-enroll/.vars
while [[ $# -gt 0 ]]; do
case "$1" in
--list) LIST=1; shift ;;
--dry-run) DRY=1; shift ;;
--manager|--agent-version|--login-server|--select)
[[ $# -ge 2 && -n $2 ]] || fail "Wert fehlt: $1"
case "$1" in
--manager) MANAGER=$2;; --agent-version) VERSION=$2; VERSION_EXPLICIT=1;;
--login-server) LOGIN_SERVER=$2;; --select) SELECTION=$2;;
esac
shift 2 ;;
--help|-h)
cat <<'HELP'
Auf dem Proxmox-Host als root starten:
bash lxc-enroll.sh
bash lxc-enroll.sh --list
bash lxc-enroll.sh --dry-run --select '106 107'
Optionen: --manager IP/NAME --agent-version VERSION --login-server HTTPS-URL
--select 'CTID CTID' --list --dry-run
Nur laufende Debian-/Ubuntu-LXCs mit systemd. Vorhandene Tailscale-/Wazuh-
Registrierungen bleiben unveraendert. Optional Tailscale SSH/Nutzer/Sudo fuer die Auswahl.
Lokale Einstellungen: /etc/lxc-enroll/.vars (JSON, ohne Geheimnisse).
Keine Host-Agent-Installation. Geheimnisse werden lokal abgefragt.
HELP
exit 0 ;;
*) fail "Unbekannte Option: $1" ;;
esac
done
[[ $EUID == 0 ]] || fail 'Bitte auf dem Proxmox-Host als root starten.'
for tool in pct timeout python3; do command -v "$tool" >/dev/null || fail "$tool fehlt auf dem Host."; done
declare -a IDS=() NAMES=() TS=() WZ=() TUN=() MANAGERS=() SELECTED=() REMOTE_DIRS=()
TMP=''
cleanup() {
local id dir
for id in "${SELECTED[@]}"; do
dir=${REMOTE_DIRS[$id]:-}
if [[ $dir =~ ^/run/lxc-enroll\.[A-Za-z0-9]+$ ]]; then
timeout 15 pct exec "$id" -- rm -rf -- "$dir" >/dev/null 2>&1 || true
fi
done
[[ -z $TMP ]] || rm -rf -- "$TMP"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
yes_no() {
local prompt=$1 default=$2
ask "$prompt"
case "$REPLY" in '' ) REPLY=$default;; j|J|ja|Ja|y|Y|yes|Yes) REPLY=1;; n|N|nein|Nein|no|No) REPLY=0;; *) fail 'Bitte j oder n eingeben.';; esac
}
load_settings() {
local fields key value
[[ ! -e $VARS_FILE && ! -L $VARS_FILE ]] && return 0
yes_no 'Gespeicherte Einstellungen aus /etc/lxc-enroll/.vars verwenden? [J/n]' 1
[[ $REPLY == 1 ]] || return 0
# JSON data, never source/eval. Reject non-root-owned or writable settings.
fields=$(python3 - "$VARS_FILE" <<'PY'
import json, os, stat, sys
p=sys.argv[1]; s=os.lstat(p)
if not stat.S_ISREG(s.st_mode) or s.st_uid != 0 or stat.S_IMODE(s.st_mode) & 0o077:
sys.exit('Unsichere Rechte der .vars: regulaere Datei, root, Modus 600 erforderlich.')
d=json.load(open(p))
for k in ('manager','agent_version','login_server','ssh_enabled','ssh_user','ssh_sudo','ssh_nopasswd'):
v=d.get(k,'')
if not isinstance(v,str) or any(c in v for c in '\n\r\t\0') or len(v)>16384:
sys.exit('Ungueltige .vars.')
print(k+'\t'+v)
PY
) || fail 'Gespeicherte Einstellungen konnten nicht sicher gelesen werden.'
while IFS=$'\t' read -r key value; do
case "$key" in
manager) [[ -n $MANAGER ]] || MANAGER=$value;;
agent_version) [[ -n $VERSION ]] || VERSION=$value;;
login_server) [[ -n $LOGIN_SERVER ]] || LOGIN_SERVER=$value;;
ssh_enabled) [[ $value =~ ^[01]$ ]] || fail 'Ungueltige SSH-Einstellung.'; SSH_ENABLED=$value;;
ssh_user) SSH_USER=$value;;
ssh_sudo) [[ $value =~ ^[01]$ ]] || fail 'Ungueltige Sudo-Einstellung.'; SSH_SUDO=$value;;
ssh_nopasswd) [[ $value =~ ^[01]$ ]] || fail 'Ungueltige Sudo-Einstellung.'; SSH_NOPASSWD=$value;;
esac
done <<<"$fields"
}
save_settings() {
[[ $SAVE_SETTINGS == 1 ]] || return 0
install -d -m 700 /etc/lxc-enroll
python3 - "$VARS_FILE" "$MANAGER" "$VERSION" "$LOGIN_SERVER" "$SSH_ENABLED" "$SSH_USER" "$SSH_SUDO" "$SSH_NOPASSWD" <<'PY'
import json, os, sys, tempfile
p=sys.argv[1]
keys=('manager','agent_version','login_server','ssh_enabled','ssh_user','ssh_sudo','ssh_nopasswd')
fd,tmp=tempfile.mkstemp(prefix='.vars.',dir=os.path.dirname(p))
try:
os.fchmod(fd,0o600)
with os.fdopen(fd,'w') as f: json.dump(dict(zip(keys,sys.argv[2:])),f,indent=2); f.write('\n')
os.replace(tmp,p)
finally:
if os.path.exists(tmp): os.unlink(tmp)
PY
say 'Einstellungen ohne Geheimnisse gespeichert: /etc/lxc-enroll/.vars'
}
scan() {
local rows id status rest config meta name os codename ts wz mgr tun init
rows=$(pct list) || fail 'Containerliste konnte nicht gelesen werden.'
while read -r id status rest; do
[[ $id =~ ^[1-9][0-9]*$ ]] || continue
if [[ $status != running ]]; then say "Uebersprungen: LXC $id ($status)."; continue; fi
config=$(pct config "$id") || { say "Uebersprungen: LXC $id (Konfiguration nicht lesbar)."; continue; }
if grep -q '^lock:' <<<"$config"; then say "Uebersprungen: LXC $id (gesperrt)."; continue; fi
meta=$(timeout 20 pct exec "$id" -- sh -s <<'PROBE'
set -eu
is_installed() { [ "$(dpkg-query -W -f='${Status}' "$1" 2>/dev/null || true)" = 'install ok installed' ]; }
. /etc/os-release
ts=0; wz=0; mgr=0; tun=0; init=0
if command -v tailscale >/dev/null 2>&1 || is_installed tailscale; then ts=1; fi
if [ -x /var/ossec/bin/wazuh-agentd ] || is_installed wazuh-agent; then wz=1; fi
if [ -x /var/ossec/bin/wazuh-managerd ] || is_installed wazuh-manager; then mgr=1; wz=1; fi
[ ! -c /dev/net/tun ] || tun=1
[ ! -d /run/systemd/system ] || init=1
printf '%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$(hostname)" "$ID" "${VERSION_CODENAME:-unknown}" "$ts" "$wz" "$mgr" "$tun" "$init" done
PROBE
) || { say "Uebersprungen: LXC $id (Pruefung fehlgeschlagen)."; continue; }
IFS='|' read -r name os codename ts wz mgr tun init rest <<<"$meta"
[[ $mgr == 0 ]] || MANAGERS+=("$id")
if [[ $rest != done || $init != 1 || ! $os =~ ^(debian|ubuntu)$ || ! $codename =~ ^[a-z]+$ ]]; then
say "Uebersprungen: LXC $id (unterstuetzt: Debian/Ubuntu mit systemd und Versions-Codename)."; continue
fi
if [[ $ts == 1 && $wz == 1 ]]; then
say "Ausgeblendet: LXC $id $name (beide Komponenten vorhanden; Verbindung nicht geprueft)."; continue
fi
[[ $name =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ ]] || { say "Uebersprungen: LXC $id (Hostname ungueltig)."; continue; }
IDS+=("$id"); NAMES[$id]=$name; TS[$id]=$ts; WZ[$id]=$wz; TUN[$id]=$tun
done <<<"$rows"
say ''; say 'Auswaehlbare LXCs (nur fehlende Komponenten werden installiert):'
printf '%-8s %-24s %-12s %-12s %s\n' 'CTID' 'Name' 'Tailscale' 'Wazuh' 'TUN'
for id in "${IDS[@]}"; do
printf '%-8s %-24s %-12s %-12s %s\n' "$id" "${NAMES[$id]}" "$(installed_label "${TS[$id]}")" "$(installed_label "${WZ[$id]}")" "$(installed_label "${TUN[$id]}")"
done
}
ensure_tun() {
local id=$1 cfg slot=-1 n
cfg=$(pct config "$id") || return 1
if ! grep -Eq '^dev[0-9]+:.*(/dev/net/tun)(,|$)' <<<"$cfg"; then
for n in {0..255}; do
if ! grep -q "^dev$n:" <<<"$cfg"; then slot=$n; break; fi
done
[[ $slot != -1 ]] || { say "LXC $id: Kein freier Device-Slot."; return 1; }
printf '%s\n' "$cfg" >"$TMP/lxc-$id.conf.before" || return 1
# Backups survive this run; no secrets are stored in these files.
install -d -m 700 /var/backups/lxc-enroll || return 1
install -m 600 "$TMP/lxc-$id.conf.before" "/var/backups/lxc-enroll/$id-$(date +%Y%m%d-%H%M%S).conf" || return 1
pct set "$id" "--dev$slot" /dev/net/tun || return 1
fi
# Never forcibly stop a workload. If clean shutdown fails, stop this CT's work.
pct shutdown "$id" --timeout 90 || return 1
pct start "$id" || return 1
for n in {1..30}; do
if pct exec "$id" -- test -c /dev/net/tun 2>/dev/null; then return 0; fi
sleep 2
done
say "LXC $id: TUN nach Neustart nicht verfuegbar."; return 1
}
write_worker() {
cat >"$TMP/worker.sh" <<'WORKER'
#!/usr/bin/env bash
set -Eeuo pipefail
set +x
umask 077
DIR=$1; MANAGER=$2; AGENT_VERSION=$3; LOGIN=$4; NAME=$5; WANT_TS=$6; WANT_WZ=$7
SSH_ENABLED=$8; SSH_USER=$9; SSH_SUDO=${10}; SSH_NOPASSWD=${11}
trap 'rm -f -- "$DIR/headscale.key" "$DIR/enrollment.pass" "$DIR/sudo.pass"' EXIT
export DEBIAN_FRONTEND=noninteractive
present() { [[ $(dpkg-query -W -f='${Status}' "$1" 2>/dev/null || true) == 'install ok installed' ]]; }
download() { curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 --connect-timeout 10 --max-time 60 --retry 2 "$1" -o "$2"; }
has_repo() { grep -Rqs -- "$1" /etc/apt/sources.list.d 2>/dev/null || grep -qs -- "$1" /etc/apt/sources.list 2>/dev/null; }
# Recheck before installing; never overwrite an existing component or enrollment.
if present tailscale || command -v tailscale >/dev/null; then WANT_TS=0; fi
if present wazuh-agent || present wazuh-manager || [[ -x /var/ossec/bin/wazuh-agentd ]]; then WANT_WZ=0; fi
if [[ $WANT_WZ == 1 && -e /var/ossec/etc/ossec.conf ]]; then
echo 'Wazuh-Konfiguration bereits vorhanden: bitte unvollstaendige Installation zuerst reparieren.' >&2; exit 1
fi
[[ $WANT_TS == 1 || $WANT_WZ == 1 || $SSH_ENABLED == 1 ]] || { echo 'Komponenten inzwischen vorhanden; nichts geaendert.'; exit 0; }
# Check manager reachability before installing a new agent.
if [[ $WANT_WZ == 1 ]]; then
for port in 1514 1515; do
timeout 5 bash -c 'exec 7<>/dev/tcp/$1/$2' -- "$MANAGER" "$port" || {
echo "Wazuh-Manager auf Port $port nicht erreichbar; Installation abgebrochen." >&2; exit 1;
}
done
fi
apt-get -o DPkg::Lock::Timeout=120 update
apt-get -o DPkg::Lock::Timeout=120 install -y --no-install-recommends ca-certificates curl gnupg
install -d -m 755 /usr/share/keyrings /etc/apt/sources.list.d
if [[ $WANT_TS == 1 ]]; then
[[ -c /dev/net/tun && -s $DIR/headscale.key ]] || { echo 'TUN oder Headscale-Schluessel fehlt.' >&2; exit 1; }
. /etc/os-release
if ! has_repo 'pkgs.tailscale.com'; then
[[ ! -e /etc/apt/sources.list.d/tailscale.list ]] || { echo 'Vorhandene tailscale.list bitte zuerst pruefen.' >&2; exit 1; }
download "https://pkgs.tailscale.com/stable/$ID/$VERSION_CODENAME.noarmor.gpg" "$DIR/tailscale.gpg"
download "https://pkgs.tailscale.com/stable/$ID/$VERSION_CODENAME.tailscale-keyring.list" "$DIR/tailscale.list"
install -m 644 "$DIR/tailscale.gpg" /usr/share/keyrings/tailscale-archive-keyring.gpg
install -m 644 "$DIR/tailscale.list" /etc/apt/sources.list.d/tailscale.list
fi
apt-get -o DPkg::Lock::Timeout=120 update
apt-get -o DPkg::Lock::Timeout=120 install -y --no-install-recommends tailscale
systemctl enable --now tailscaled
# File reference keeps the key out of command arguments/history. Keep local DNS.
timeout 90 tailscale up --login-server="$LOGIN" --auth-key="file:$DIR/headscale.key" --hostname="$NAME" --accept-dns=false --timeout=60s
rm -f -- "$DIR/headscale.key"
tailscale ip -4
echo 'Tailscale installiert und angemeldet.'
fi
if [[ $WANT_WZ == 1 ]]; then
if ! has_repo 'packages.wazuh.com'; then
[[ ! -e /etc/apt/sources.list.d/lxc-enroll-wazuh.list ]] || exit 1
download https://packages.wazuh.com/key/GPG-KEY-WAZUH "$DIR/wazuh.asc"
gpg --batch --yes --dearmor -o "$DIR/wazuh.gpg" "$DIR/wazuh.asc"
install -m 644 "$DIR/wazuh.gpg" /usr/share/keyrings/lxc-enroll-wazuh.gpg
printf '%s\n' 'deb [signed-by=/usr/share/keyrings/lxc-enroll-wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main' >/etc/apt/sources.list.d/lxc-enroll-wazuh.list
chmod 644 /etc/apt/sources.list.d/lxc-enroll-wazuh.list
fi
apt-get -o DPkg::Lock::Timeout=120 update
apt-cache madison wazuh-agent | awk '{print $3}' | grep -Fx "$AGENT_VERSION" >/dev/null || {
echo "Wazuh-Version $AGENT_VERSION nicht verfuegbar; kein neuerer Agent wird installiert." >&2; exit 1;
}
export WAZUH_MANAGER="$MANAGER" WAZUH_REGISTRATION_SERVER="$MANAGER" WAZUH_AGENT_NAME="$NAME"
if [[ -s $DIR/enrollment.pass ]]; then export WAZUH_REGISTRATION_PASSWORD; WAZUH_REGISTRATION_PASSWORD=$(cat "$DIR/enrollment.pass"); fi
apt-get -o DPkg::Lock::Timeout=120 install -y --no-install-recommends "wazuh-agent=$AGENT_VERSION"
unset WAZUH_REGISTRATION_PASSWORD
rm -f -- "$DIR/enrollment.pass"
apt-mark hold wazuh-agent
systemctl daemon-reload
systemctl enable --now wazuh-agent
connected=0
for attempt in {1..30}; do
if grep -q "^status='connected'" /var/ossec/var/run/wazuh-agentd.state 2>/dev/null; then
connected=1; break
fi
sleep 2
done
if [[ $connected == 0 ]]; then
echo 'Wazuh installiert, Verbindung nicht bestaetigt. Agent-Konfiguration/Logs pruefen; keine automatische Neuregistrierung.' >&2
exit 1
fi
echo 'Wazuh installiert und mit dem Manager verbunden.'
fi
if [[ $SSH_ENABLED == 1 ]]; then
if [[ $SSH_SUDO == 1 ]]; then apt-get -o DPkg::Lock::Timeout=120 install -y --no-install-recommends sudo; fi
new_user=0
if id "$SSH_USER" >/dev/null 2>&1; then
[[ $(id -u "$SSH_USER") != 0 ]] || { echo 'SSH-Ziel darf kein root-Konto sein.' >&2; exit 1; }
else
useradd -m -U -s /bin/bash "$SSH_USER"
new_user=1
fi
if [[ $SSH_SUDO == 1 ]]; then
usermod -aG sudo "$SSH_USER"
if [[ $SSH_NOPASSWD == 1 ]]; then
sudo_rule="/etc/sudoers.d/lxc-enroll-$SSH_USER"
printf '%s ALL=(ALL:ALL) NOPASSWD: ALL\n' "$SSH_USER" >"$DIR/sudoers"
visudo -cf "$DIR/sudoers"
if [[ -e $sudo_rule ]] && ! cmp -s "$DIR/sudoers" "$sudo_rule"; then
echo 'Abweichende vorhandene Sudo-Regel: bitte manuell pruefen.' >&2; exit 1
fi
install -m 440 "$DIR/sudoers" "$sudo_rule"
elif [[ $new_user == 1 ]]; then
[[ -s $DIR/sudo.pass ]] || { echo 'Passwort fuer neuen Sudo-Nutzer fehlt.' >&2; exit 1; }
{ printf '%s:' "$SSH_USER"; cat "$DIR/sudo.pass"; printf '\n'; } | chpasswd
echo 'Passwort fuer neuen Nutzer gesetzt; bestehende Nutzerpasswoerter bleiben unveraendert.'
fi
fi
rm -f -- "$DIR/sudo.pass"
tailscale set --ssh=true
echo "Tailscale SSH fuer $SSH_USER aktiviert. Headscale-Netzwerk- UND SSH-Policy muessen diesen Zugriff erlauben."
echo 'Kein OpenSSH-Server installiert. Vorhandene Nutzer-/Sudo-Rechte werden nicht entzogen.'
fi
WORKER
}
deploy_one() {
local id=$1 dir ts wz
[[ $(pct status "$id") == 'status: running' ]] || return 1
if pct config "$id" | grep -q '^lock:'; then say "LXC $id inzwischen gesperrt."; return 1; fi
ts=$((1-${TS[$id]})); wz=$((1-${WZ[$id]}))
if [[ $ts == 1 && ${TUN[$id]} == 0 ]]; then ensure_tun "$id" || return 1; fi
dir=$(pct exec "$id" -- mktemp -d /run/lxc-enroll.XXXXXXXX) || return 1
[[ $dir =~ ^/run/lxc-enroll\.[A-Za-z0-9]+$ ]] || return 1
REMOTE_DIRS[$id]=$dir
pct push "$id" "$TMP/worker.sh" "$dir/worker.sh" --user 0 --group 0 --perms 600 || return 1
if [[ $ts == 1 ]]; then pct push "$id" "$TMP/headscale.key" "$dir/headscale.key" --user 0 --group 0 --perms 600 || return 1; fi
if [[ $wz == 1 && -s $TMP/enrollment.pass ]]; then pct push "$id" "$TMP/enrollment.pass" "$dir/enrollment.pass" --user 0 --group 0 --perms 600 || return 1; fi
if [[ $SSH_ENABLED == 1 ]]; then
if [[ -s $TMP/sudo.pass ]]; then pct push "$id" "$TMP/sudo.pass" "$dir/sudo.pass" --user 0 --group 0 --perms 600 || return 1; fi
fi
# Credentials are files, never pct arguments. bash receives only non-secret settings.
pct exec "$id" -- bash "$dir/worker.sh" "$dir" "$MANAGER" "$VERSION" "$LOGIN_SERVER" "${NAMES[$id]}" "$ts" "$wz" "$SSH_ENABLED" "$SSH_USER" "$SSH_SUDO" "$SSH_NOPASSWD" || return 1
if [[ $ts == 1 ]]; then
pct exec "$id" -- tailscale status --json | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("BackendState")=="Running" and d.get("Self",{}).get("Online") is True else 1)' || {
say "LXC $id: Headscale-Verbindung nicht als online bestaetigt."; return 1;
}
fi
pct exec "$id" -- rm -rf -- "$dir" || return 1
REMOTE_DIRS[$id]=''
}
main() {
local id input wanted found need_ts=0 need_wz=0 restart_ids='' ok='' failed='' auto_version password
scan
[[ $LIST == 0 ]] || return 0
[[ ${#IDS[@]} -gt 0 ]] || { say 'Keine passenden LXCs gefunden.'; return 0; }
exec 3<>/dev/tty || fail 'Interaktives Terminal fehlt. --list kann ohne Terminal laufen.'
load_settings
yes_no "Tailscale SSH fuer alle ausgewaehlten LXCs aktivieren? (Standard: $SSH_ENABLED; 1=Ja, 0=Nein) [j/n]" "$SSH_ENABLED"
SSH_ENABLED=$REPLY
if [[ $SSH_ENABLED == 1 ]]; then
ask "SSH-Nutzername${SSH_USER:+ [$SSH_USER]}:"
SSH_USER=${REPLY:-$SSH_USER}
[[ $SSH_USER =~ ^[a-z_][a-z0-9_-]{0,30}$ && $SSH_USER != root ]] || fail 'Bitte einen normalen Linux-Nutzernamen verwenden.'
yes_no "Nutzer zu sudo hinzufuegen? (Standard: $SSH_SUDO) [j/n]" "$SSH_SUDO"; SSH_SUDO=$REPLY
if [[ $SSH_SUDO == 1 ]]; then
yes_no "Sudo OHNE Passwort erlauben? (Standard: $SSH_NOPASSWD; 0=Passwort erforderlich) [j/n]" "$SSH_NOPASSWD"; SSH_NOPASSWD=$REPLY
fi
fi
yes_no 'Einstellungen ohne Geheimnisse lokal in .vars speichern? [J/n]' 1; SAVE_SETTINGS=$REPLY
if [[ -z $SELECTION ]]; then ask 'CTIDs auswaehlen (Leerzeichen, oder all; leer = abbrechen):'; SELECTION=$REPLY; fi
[[ -n $SELECTION ]] || return 0
if [[ $SELECTION == all ]]; then SELECTED=("${IDS[@]}"); else
SELECTION=${SELECTION//,/ }
for wanted in $SELECTION; do
[[ $wanted =~ ^[1-9][0-9]*$ ]] || fail 'Bitte nur CTIDs eingeben.'
found=0
for id in "${IDS[@]}"; do [[ $id != "$wanted" ]] || found=1; done
[[ $found == 1 ]] || fail "LXC $wanted ist nicht auswaehlbar."
for id in "${SELECTED[@]}"; do [[ $id != "$wanted" ]] || fail "LXC $wanted doppelt ausgewaehlt."; done
SELECTED+=("$wanted")
done
fi
say ''; say 'Geplante Aenderungen:'
for id in "${SELECTED[@]}"; do
say "LXC $id ${NAMES[$id]}: Tailscale $(installed_label "${TS[$id]}"), Wazuh $(installed_label "${WZ[$id]}")."
if [[ ${TS[$id]} == 0 ]]; then
need_ts=1
if [[ ${TUN[$id]} == 0 ]]; then restart_ids+=" $id"; fi
fi
[[ ${WZ[$id]} != 0 ]] || need_wz=1
done
if [[ -n $restart_ids ]]; then say "TUN-Zugriff ergaenzen und sauber neu starten:$restart_ids"; fi
if [[ $SSH_ENABLED == 1 ]]; then
say "Tailscale SSH fuer die Auswahl: Nutzer $SSH_USER, Sudo=$SSH_SUDO, Sudo-ohne-Passwort=$SSH_NOPASSWD."
say 'Headscale muss Zugriff auf Port 22 und den gewaehlten Linux-Nutzer in seiner SSH-Policy erlauben.'
say 'Bestehende SSH-Sitzungen ueber Tailscale koennen beim Aktivieren unterbrochen werden.'
fi
[[ $DRY == 0 ]] || { say 'Vorschau abgeschlossen. Keine Aenderungen vorgenommen.'; return 0; }
command -v flock >/dev/null || fail 'flock fehlt auf dem Host.'
exec 8>/run/lxc-enroll.lock
flock -n 8 || fail 'Ein anderer Enrollment-Lauf ist bereits aktiv.'
if [[ $need_wz == 1 ]]; then
if [[ -z $MANAGER ]]; then ask 'Erreichbare Wazuh-Manager-Adresse (LAN-IP oder Headscale-IP):'; MANAGER=$REPLY; fi
[[ $MANAGER =~ ^[A-Za-z0-9][A-Za-z0-9.:-]*$ ]] || fail 'Manager-Adresse ungueltig.'
if [[ ${#MANAGERS[@]} == 1 ]]; then
auto_version=$(pct exec "${MANAGERS[0]}" -- dpkg-query -W '-f=${Version}' wazuh-manager) || fail 'Manager-Version nicht lesbar.'
if [[ $VERSION_EXPLICIT == 1 && $VERSION != "$auto_version" ]]; then fail 'Agent-Version muss der lokalen Manager-Version entsprechen.'; fi
VERSION=$auto_version
elif [[ -z $VERSION ]]; then ask 'Exakte Manager-Paketversion (z.B. 4.14.8-1):'; VERSION=$REPLY; fi
[[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$ ]] || fail 'Paketversion ungueltig.'
say "Neue Wazuh-Agents: $VERSION (anschliessend auf hold; gemeinsam mit Manager aktualisieren)."
fi
if [[ $need_ts == 1 ]]; then
if [[ -z $LOGIN_SERVER ]]; then ask 'Headscale-Login-Server (vollstaendige HTTPS-URL):'; LOGIN_SERVER=$REPLY; fi
[[ $LOGIN_SERVER =~ ^https://[A-Za-z0-9][A-Za-z0-9.:-]*/?$ ]] || fail 'Bitte eine HTTPS-Server-URL ohne Zugangsdaten/Query eingeben.'
say 'Neue Tailscale-Clients verwenden Headscale; vorhandene lokale DNS-Einstellungen bleiben aktiv.'
fi
ask 'Nur diese fehlenden Komponenten installieren? Bitte INSTALL eingeben:'
[[ $REPLY == INSTALL ]] || { say 'Abgebrochen.'; return 0; }
if [[ -n $restart_ids ]]; then
ask "Diese Container werden kurz unterbrochen:$restart_ids. Bitte NEUSTART eingeben:"
[[ $REPLY == NEUSTART ]] || { say 'Abgebrochen; nichts installiert.'; return 0; }
fi
TMP=$(mktemp -d /run/lxc-enroll.XXXXXXXX)
if [[ $SSH_ENABLED == 1 ]]; then
if [[ $SSH_SUDO == 1 && $SSH_NOPASSWD == 0 ]]; then
secret 'Passwort fuer NEU angelegte Sudo-Nutzer (bestehende Passwoerter bleiben unveraendert):'; password=$REPLY
[[ -n $password && $password != *:* && $password != *$'\r'* ]] || fail 'Passwort leer oder enthaelt ungueltige Zeichen.'
secret 'Passwort wiederholen:'; [[ $REPLY == "$password" ]] || fail 'Passwoerter stimmen nicht ueberein.'
printf '%s' "$password" >"$TMP/sudo.pass"; password=''; REPLY=''
fi
fi
save_settings
if [[ $need_ts == 1 ]]; then
secret 'Wiederverwendbaren, kurz gueltigen Headscale-Preauth-Key eingeben (wird nicht angezeigt):'
[[ -n $REPLY && $REPLY != *$'\r'* ]] || fail 'Schluessel fehlt/ungueltig.'
printf '%s' "$REPLY" >"$TMP/headscale.key"; REPLY=''
fi
if [[ $need_wz == 1 ]]; then
secret 'Optionales Wazuh-Enrollment-Passwort (leer, falls keines verlangt wird):'
printf '%s' "$REPLY" >"$TMP/enrollment.pass"; REPLY=''
fi
write_worker
for id in "${SELECTED[@]}"; do
say ''; say "Bearbeite LXC $id ${NAMES[$id]} ..."
# All deployment operations explicitly propagate errors. Parent owns cleanup.
if deploy_one "$id"; then ok+=" $id"; else failed+=" $id"; fi
done
say ''; say "Erfolgreich abgeschlossen:${ok:- keine}"
say "Nicht abgeschlossen:${failed:- keine}"
say 'Bereits vorhandene Komponenten wurden nicht umkonfiguriert; deren Verbindung bitte separat pruefen.'
say 'Enrollment richtet noch keine Discord-Regeln oder vollstaendige Schwachstellenabdeckung ein.'
[[ -z $failed ]]
}
main