Add netbird_zitadel.sh

This commit is contained in:
2026-03-21 10:16:11 +00:00
commit 9ff859324a

633
netbird_zitadel.sh Normal file
View File

@@ -0,0 +1,633 @@
#!/bin/bash
# =============================================================================
# Skrypt instalacji samodzielnie utrzymywanego NetBird z Zitadel
# Utworzony dla OS: Debian 13 (Trixie)
# =============================================================================
# Skrypt wykonuje pełną instalację serwera NetBird od zera:
# 1. Konfiguracja systemu (hostname, pakiety)
# 2. Konfiguracja firewalla (UFW)
# 3. Instalacja Docker
# 4. Instalacja NetBird (oficjalny skrypt getting-started-with-zitadel.sh)
# 5. Instalacja CrowdSec (opcjonalnie)
# 6. Konfiguracja CrowdSec
# 7. Weryfikacja końcowa
# =============================================================================
set -euo pipefail
# !!!!!! Konfiguracja !!!!!! #
# Skonfiguruj parametry poniżej przed uruchomieniem skryptu:
HOSTNAME="netbird-zitadel" # Wpisz nazwę hosta
NETBIRD_DOMAIN="vpn.ariscard.eu" # Domena DNS, pod którą ma działać NetBird
NETBIRD_DIR="/netbird" # Katalog roboczy — tu trafią docker-compose.yml i pliki konfiguracyjne
INSTALL_CROWDSEC=false # true = zainstaluj CrowdSec, false = pomiń
CROWDSEC_PORT=8081 # Port CrowdSec API (domyślny 8080 koliduje z NetBird)
CROWDSEC_ENROLL_KEY="1234567890" # Klucz rejestracji z panelu web CrowdSec
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/netbird-install_${TIMESTAMP}.log"
# Porty do odblokowania w zaporze ogniowej UFW
UFW_TCP_PORTS="22 80 443 33073 10000 33080"
UFW_UDP_PORTS="3478 49152:65535"
# Pakiety systemowe
BASE_PACKAGES=(mc sudo jq curl htop cron ca-certificates gnupg)
# --- Kolory ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# --- Funkcje pomocnicze ---
log() { echo -e "${GREEN}[$(date '+%H:%M:%S')] [INFO]${NC} $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] [WARN]${NC} $*" | tee -a "$LOG_FILE"; }
err() { echo -e "${RED}[$(date '+%H:%M:%S')] [ERROR]${NC} $*" | tee -a "$LOG_FILE" >&2; }
header() { echo -e "\n${CYAN}${BOLD}=== $* ===${NC}" | tee -a "$LOG_FILE"; }
abort() {
err "$1"
err "Skrypt przerwany. Log: ${LOG_FILE}"
exit 1
}
confirm() {
local msg="${1:-Kontynuować?}"
read -r -p "$(echo -e "${YELLOW}${msg} [t/N]: ${NC}")" answer </dev/tty || answer="N"
[[ "${answer,,}" == "t" ]]
}
retry() {
local retries="${1}"
local delay="${2}"
shift 2
local attempt=1
while [[ $attempt -le $retries ]]; do
if "$@"; then
return 0
fi
warn "Komenda nie powiodła się (próba ${attempt}/${retries}). Ponawiam za ${delay}s..."
sleep "$delay"
attempt=$((attempt + 1))
done
return 1
}
cleanup_on_error() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
err "Skrypt zakończył się błędem (kod: ${exit_code})."
err "Sprawdź log: ${LOG_FILE}"
err "Ostatni ukończony etap: ${CURRENT_STAGE:-nieznany}"
fi
}
trap cleanup_on_error EXIT
CURRENT_STAGE="inicjalizacja"
# Dane logowania (uzupełniane automatycznie po instalacji NetBird)
NETBIRD_DASHBOARD_URL=""
NETBIRD_ADMIN_USER=""
NETBIRD_ADMIN_PASS=""
# =============================================================================
# ETAP 0: Weryfikacja wstępna
# =============================================================================
preflight_checks() {
header "ETAP 0: Walidacja wstępna"
CURRENT_STAGE="walidacja wstępna"
# Sprawdzenie czy uruchamiane jako Root
if [[ $EUID -ne 0 ]]; then
abort "Skrypt musi być uruchomiony jako root."
fi
# Sprawdzenie czy dystrybucja to Debian
if [[ ! -f /etc/os-release ]]; then
abort "Nie można określić systemu operacyjnego."
fi
source /etc/os-release
if [[ "$ID" != "debian" ]]; then
warn "Skrypt przeznaczony dla Debiana. Wykryto: ${ID}"
warn "Dalsze działanie na własne ryzyko — skrypt może nie działać poprawnie."
if ! confirm "Czy kontynuować mimo nieobsługiwanej dystrybucji?"; then
abort "Przerwano — nieobsługiwana dystrybucja."
fi
fi
log "System: ${PRETTY_NAME}"
# Sprawdzenie domeny
if [[ -z "$NETBIRD_DOMAIN" ]]; then
abort "Zmienna NETBIRD_DOMAIN nie może być pusta. Ustaw domenę DNS w sekcji konfiguracji."
fi
log "Domena NetBird: ${NETBIRD_DOMAIN}"
# Sprawdzenie DNS — czy domena wskazuje na ten serwer
local resolved_ip
resolved_ip=$(dig +short "$NETBIRD_DOMAIN" 2>/dev/null | tail -1) || true
if [[ -n "$resolved_ip" ]]; then
log "DNS ${NETBIRD_DOMAIN} -> ${resolved_ip}"
else
warn "Nie udało się rozwiązać domeny ${NETBIRD_DOMAIN}."
warn "Upewnij się, że rekordy DNS (A/AAAA) wskazują na adres IP tego serwera."
if ! confirm "Kontynuować mimo braku rozwiązania DNS?"; then
abort "Przerwano — domena nie jest skonfigurowana."
fi
fi
# Sprawdzenie połączenia z internetem
if ! wget -q --spider --timeout=10 https://deb.debian.org 2>/dev/null && \
! bash -c 'echo >/dev/tcp/deb.debian.org/443' 2>/dev/null; then
abort "Brak połączenia z internetem. Wymagane do instalacji pakietów."
fi
log "Połączenie z internetem: OK"
# Sprawdzenie wolnego miejsca (min. 5GB)
local available_kb
available_kb=$(df / --output=avail | tail -1 | tr -d ' ')
if [[ "$available_kb" -lt 5242880 ]]; then
warn "Mało wolnego miejsca na dysku (< 5GB)."
if ! confirm "Kontynuować mimo mało miejsca?"; then
abort "Przerwano — za mało miejsca na dysku."
fi
fi
log "Wolne miejsce na dysku: $(df -h / --output=avail | tail -1 | tr -d ' ')"
# Sprawdzenie czy Docker/NetBird nie są już zainstalowane
if command -v docker &>/dev/null && docker compose ps 2>/dev/null | grep -qi netbird; then
warn "Wykryto działające kontenery NetBird. Możliwe, że NetBird jest już zainstalowany."
if ! confirm "Kontynuować mimo to?"; then
abort "Przerwano — NetBird może być już zainstalowany."
fi
fi
log "Walidacja wstępna zakończona pomyślnie."
}
# =============================================================================
# ETAP 1: Konfiguracja systemu
# =============================================================================
configure_system() {
header "ETAP 1: Konfiguracja systemu"
CURRENT_STAGE="konfiguracja systemu"
# Hostname
local current_hostname
current_hostname=$(hostname)
if [[ "$current_hostname" != "$HOSTNAME" ]]; then
log "Ustawianie hostname: ${HOSTNAME}"
hostnamectl set-hostname "$HOSTNAME"
log "Hostname zmieniony: ${current_hostname} -> ${HOSTNAME}"
else
log "Hostname już ustawiony: ${HOSTNAME}"
fi
# Aktualizacja systemu
log "Aktualizacja systemu..."
export DEBIAN_FRONTEND=noninteractive
if ! retry 3 10 apt-get update -qq; then
abort "Nie udało się zaktualizować listy pakietów."
fi
if ! apt-get upgrade -y -qq 2>&1 | tee -a "$LOG_FILE"; then
warn "Aktualizacja pakietów zakończyła się z ostrzeżeniami."
fi
log "System zaktualizowany."
# Instalacja pakietów bazowych
log "Instalacja pakietów bazowych: ${BASE_PACKAGES[*]}"
if ! apt-get install -y -qq "${BASE_PACKAGES[@]}" 2>&1 | tee -a "$LOG_FILE"; then
abort "Nie udało się zainstalować pakietów bazowych."
fi
log "Pakiety bazowe zainstalowane."
}
# =============================================================================
# ETAP 2: Firewall (UFW)
# =============================================================================
configure_ufw() {
header "ETAP 2: Konfiguracja firewalla (UFW)"
CURRENT_STAGE="konfiguracja UFW"
if ! command -v ufw &>/dev/null; then
log "Instalacja UFW..."
apt-get install -y -qq ufw 2>&1 | tee -a "$LOG_FILE"
fi
# Reset reguł (czysta konfiguracja)
log "Konfiguracja reguł UFW..."
ufw --force reset >> "$LOG_FILE" 2>&1
# Domyślna polityka
ufw default deny incoming >> "$LOG_FILE" 2>&1
ufw default allow outgoing >> "$LOG_FILE" 2>&1
# Reguły TCP
for port in $UFW_TCP_PORTS; do
log " Otwieranie TCP: ${port}"
ufw allow "$port"/tcp >> "$LOG_FILE" 2>&1 || ufw allow "$port" >> "$LOG_FILE" 2>&1
done
# Reguły UDP
for port in $UFW_UDP_PORTS; do
log " Otwieranie UDP: ${port}"
ufw allow "$port"/udp >> "$LOG_FILE" 2>&1
done
# Włączenie UFW
ufw --force enable >> "$LOG_FILE" 2>&1
log "UFW włączony."
# Status
log "Status UFW:"
ufw status verbose 2>&1 | tee -a "$LOG_FILE"
}
# =============================================================================
# ETAP 3: Instalacja Docker
# =============================================================================
install_docker() {
header "ETAP 3: Instalacja Docker"
CURRENT_STAGE="instalacja Docker"
# Sprawdzenie czy Docker już zainstalowany
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
local docker_version
docker_version=$(docker --version)
log "Docker już zainstalowany: ${docker_version}"
if docker compose version &>/dev/null 2>&1; then
log "Docker Compose plugin: $(docker compose version)"
return 0
else
warn "Docker Compose plugin nie jest zainstalowany. Kontynuuję instalację..."
fi
fi
# Dodanie klucza GPG Docker
log "Dodawanie klucza GPG Docker..."
install -m 0755 -d /etc/apt/keyrings
if ! retry 3 5 curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc; then
abort "Nie udało się pobrać klucza GPG Docker."
fi
chmod a+r /etc/apt/keyrings/docker.asc
# Dodanie repozytorium Docker
log "Dodawanie repozytorium Docker..."
source /etc/os-release
tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: ${VERSION_CODENAME}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
# Instalacja Docker
log "Instalacja pakietów Docker..."
if ! retry 3 10 apt-get update -qq; then
abort "Nie udało się zaktualizować listy pakietów po dodaniu repo Docker."
fi
local docker_packages=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
if ! apt-get install -y -qq "${docker_packages[@]}" 2>&1 | tee -a "$LOG_FILE"; then
abort "Nie udało się zainstalować Docker."
fi
# Weryfikacja
if ! docker info &>/dev/null 2>&1; then
log "Uruchamianie Docker daemon..."
systemctl enable --now docker
sleep 3
fi
if ! docker info &>/dev/null 2>&1; then
abort "Docker daemon nie działa po instalacji."
fi
if ! docker compose version &>/dev/null 2>&1; then
abort "Docker Compose plugin nie jest dostępny po instalacji."
fi
log "Docker zainstalowany: $(docker --version)"
log "Docker Compose: $(docker compose version)"
}
# =============================================================================
# ETAP 4: Instalacja NetBird
# =============================================================================
install_netbird() {
header "ETAP 4: Instalacja NetBird"
CURRENT_STAGE="instalacja NetBird"
# Utworzenie katalogu roboczego
if [[ ! -d "$NETBIRD_DIR" ]]; then
log "Tworzenie katalogu roboczego: ${NETBIRD_DIR}"
mkdir -p "$NETBIRD_DIR"
fi
log "Katalog roboczy: ${NETBIRD_DIR}"
log "Domena: ${NETBIRD_DOMAIN}"
log "Pobieranie i uruchamianie oficjalnego skryptu instalacyjnego NetBird..."
log "Źródło: https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh"
export NETBIRD_DOMAIN="$NETBIRD_DOMAIN"
if ! curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh -o /tmp/netbird-install.sh; then
abort "Nie udało się pobrać skryptu instalacyjnego NetBird."
fi
chmod +x /tmp/netbird-install.sh
log "Skrypt instalacyjny pobrany. Uruchamianie z katalogu ${NETBIRD_DIR}..."
# Uruchomienie z katalogu NETBIRD_DIR — oficjalny skrypt generuje pliki
# (docker-compose.yml, Caddyfile, *.env, management.json, itp.)
# w bieżącym katalogu roboczym
cd "$NETBIRD_DIR"
if ! bash /tmp/netbird-install.sh 2>&1 | tee -a "$LOG_FILE"; then
abort "Skrypt instalacyjny NetBird zakończył się błędem."
fi
rm -f /tmp/netbird-install.sh
# Odczytanie danych logowania z wyniku instalacji
NETBIRD_DASHBOARD_URL=$(grep 'dashboard at' "$LOG_FILE" | tail -1 | sed 's/.*dashboard at //' | tr -d ' ') || true
NETBIRD_ADMIN_USER=$(grep 'Username:' "$LOG_FILE" | tail -1 | awk '{print $NF}') || true
NETBIRD_ADMIN_PASS=$(grep 'Password:' "$LOG_FILE" | tail -1 | awk '{print $NF}') || true
log "Instalacja NetBird zakończona. Pliki w: ${NETBIRD_DIR}"
}
# =============================================================================
# ETAP 5: Instalacja CrowdSec
# =============================================================================
install_crowdsec() {
header "ETAP 5: Instalacja CrowdSec"
CURRENT_STAGE="instalacja CrowdSec"
# Instalacja repozytorium CrowdSec
log "Dodawanie repozytorium CrowdSec..."
if ! retry 3 5 bash -c 'curl -s https://install.crowdsec.net | sh' 2>&1 | tee -a "$LOG_FILE"; then
warn "Nie udało się dodać repozytorium CrowdSec."
if ! confirm "Pominąć instalację CrowdSec?"; then
abort "Przerwano — nie udało się zainstalować CrowdSec."
fi
return 0
fi
# Instalacja CrowdSec
log "Instalacja CrowdSec..."
if ! apt-get install -y -qq crowdsec 2>&1 | tee -a "$LOG_FILE"; then
warn "Nie udało się zainstalować CrowdSec."
return 1
fi
# Instalacja firewall bouncer
log "Instalacja CrowdSec Firewall Bouncer..."
if ! apt-get install -y -qq crowdsec-firewall-bouncer-iptables 2>&1 | tee -a "$LOG_FILE"; then
warn "Nie udało się zainstalować firewall bouncer."
fi
log "CrowdSec zainstalowany."
}
# =============================================================================
# ETAP 6: Konfiguracja CrowdSec (zmiana portu z 8080 na CROWDSEC_PORT)
# =============================================================================
configure_crowdsec() {
header "ETAP 6: Konfiguracja CrowdSec (port ${CROWDSEC_PORT})"
CURRENT_STAGE="konfiguracja CrowdSec"
local config_file="/etc/crowdsec/config.yaml"
local creds_file="/etc/crowdsec/local_api_credentials.yaml"
local bouncer_file="/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml"
# Sprawdzenie czy port 8080 jest zajęty (przez NetBird)
if ss -tlnp | grep -q ':8080 '; then
log "Port 8080 zajęty (NetBird). Zmiana portu CrowdSec na ${CROWDSEC_PORT}."
else
log "Port 8080 wolny, ale zmieniam na ${CROWDSEC_PORT} prewencyjnie."
fi
# Zmiana portu w config.yaml
if [[ -f "$config_file" ]]; then
log "Modyfikacja ${config_file}..."
if grep -q 'listen_uri' "$config_file"; then
sed -i "s|listen_uri:.*|listen_uri: 127.0.0.1:${CROWDSEC_PORT}|" "$config_file"
else
warn "Nie znaleziono 'listen_uri' w ${config_file}. Sprawdź ręcznie."
fi
else
warn "Brak pliku ${config_file}"
fi
# Zmiana URL w local_api_credentials.yaml
if [[ -f "$creds_file" ]]; then
log "Modyfikacja ${creds_file}..."
sed -i "s|url:.*|url: http://127.0.0.1:${CROWDSEC_PORT}/|" "$creds_file"
else
warn "Brak pliku ${creds_file}"
fi
# Zmiana URL w bouncer config
if [[ -f "$bouncer_file" ]]; then
log "Modyfikacja ${bouncer_file}..."
sed -i "s|api_url:.*|api_url: http://127.0.0.1:${CROWDSEC_PORT}/|" "$bouncer_file"
else
warn "Brak pliku ${bouncer_file}"
fi
# Restart serwisów CrowdSec
log "Restartowanie CrowdSec..."
systemctl restart crowdsec 2>&1 | tee -a "$LOG_FILE" || warn "Nie udało się zrestartować crowdsec."
sleep 3
if systemctl is-active --quiet crowdsec; then
log "CrowdSec działa na porcie ${CROWDSEC_PORT}."
else
warn "CrowdSec nie uruchomił się poprawnie. Sprawdź: journalctl -u crowdsec"
fi
# Restart firewall bouncer
log "Restartowanie firewall bouncer..."
systemctl enable --now crowdsec-firewall-bouncer 2>&1 | tee -a "$LOG_FILE" || \
warn "Nie udało się uruchomić firewall bouncer."
if systemctl is-active --quiet crowdsec-firewall-bouncer; then
log "CrowdSec Firewall Bouncer działa."
else
warn "Firewall Bouncer nie uruchomił się. Sprawdź: journalctl -u crowdsec-firewall-bouncer"
fi
# Enroll do konsoli CrowdSec
if [[ -n "$CROWDSEC_ENROLL_KEY" ]]; then
log "Rejestracja w konsoli CrowdSec..."
if cscli console enroll -e context "$CROWDSEC_ENROLL_KEY" 2>&1 | tee -a "$LOG_FILE"; then
log "Rejestracja w konsoli CrowdSec zakończona."
else
warn "Nie udało się zarejestrować w konsoli CrowdSec."
fi
fi
log "Konfiguracja CrowdSec zakończona."
}
# =============================================================================
# ETAP 7: Weryfikacja końcowa
# =============================================================================
final_verification() {
header "ETAP 7: Weryfikacja końcowa"
CURRENT_STAGE="weryfikacja końcowa"
local errors=0
# Docker
log "Sprawdzanie Docker..."
if docker info &>/dev/null 2>&1; then
log " Docker: OK"
else
err " Docker: NIE DZIAŁA"
errors=$((errors + 1))
fi
# Kontenery NetBird
log "Sprawdzanie kontenerów NetBird..."
if [[ -f "${NETBIRD_DIR}/docker-compose.yml" ]]; then
cd "$NETBIRD_DIR"
local services
mapfile -t services < <(docker compose config --services 2>/dev/null)
local running=0
local total=${#services[@]}
for svc in "${services[@]}"; do
local state
state=$(docker compose ps --format '{{.State}}' "$svc" 2>/dev/null || echo "missing")
if [[ "$state" == "running" ]]; then
log " ${svc}: OK"
running=$((running + 1))
else
err " ${svc}: ${state}"
errors=$((errors + 1))
fi
done
log " Kontenery: ${running}/${total} działają."
else
warn " Nie znaleziono ${NETBIRD_DIR}/docker-compose.yml — pomijam sprawdzanie kontenerów."
fi
# UFW
log "Sprawdzanie UFW..."
if ufw status | grep -q "Status: active"; then
log " UFW: aktywny"
else
warn " UFW: nieaktywny"
errors=$((errors + 1))
fi
# CrowdSec
if [[ "$INSTALL_CROWDSEC" == true ]]; then
log "Sprawdzanie CrowdSec..."
if systemctl is-active --quiet crowdsec 2>/dev/null; then
log " CrowdSec: OK (port ${CROWDSEC_PORT})"
else
warn " CrowdSec: nie działa"
fi
if systemctl is-active --quiet crowdsec-firewall-bouncer 2>/dev/null; then
log " Firewall Bouncer: OK"
else
warn " Firewall Bouncer: nie działa"
fi
else
log " CrowdSec: pominięty (INSTALL_CROWDSEC=false)"
fi
# Sprawdzenie portów
log "Sprawdzanie nasłuchujących portów..."
ss -tlnp | grep -E ':(80|443|8080|33073|10000|33080|3478|8081) ' 2>&1 | tee -a "$LOG_FILE" || true
# Podsumowanie
header "PODSUMOWANIE INSTALACJI"
log "Hostname: $(hostname)"
log "System: $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')"
log "Docker: $(docker --version 2>/dev/null || echo 'N/A')"
log "Compose: $(docker compose version 2>/dev/null || echo 'N/A')"
log "Domena: ${NETBIRD_DOMAIN}"
log "UFW: $(ufw status | head -1)"
if [[ "$INSTALL_CROWDSEC" == true ]]; then
log "CrowdSec: $(systemctl is-active crowdsec 2>/dev/null || echo 'N/A')"
else
log "CrowdSec: pominięty"
fi
if [[ -f "${NETBIRD_DIR}/docker-compose.yml" ]]; then
log "Kontenery: ${running:-0}/${total:-0} działają"
log "Katalog: ${NETBIRD_DIR}"
fi
log "Log: ${LOG_FILE}"
if [[ $errors -gt 0 ]]; then
warn "Instalacja zakończona z ${errors} problemami. Sprawdź log."
else
log "Instalacja zakończona pomyślnie!"
fi
# Dane logowania do panelu NetBird
if [[ -n "$NETBIRD_DASHBOARD_URL" || -n "$NETBIRD_ADMIN_USER" || -n "$NETBIRD_ADMIN_PASS" ]]; then
header "DANE LOGOWANIA DO PANELU NETBIRD"
log "Panel dostępny pod adresem: ${NETBIRD_DASHBOARD_URL:-N/A}"
log "Nazwa użytkownika: ${NETBIRD_ADMIN_USER:-N/A}"
log "Hasło: ${NETBIRD_ADMIN_PASS:-N/A}"
else
warn "Nie udało się odczytać danych logowania z wyniku instalacji."
warn "Sprawdź log: ${LOG_FILE}"
fi
}
# =============================================================================
# MAIN
# =============================================================================
main() {
header "NetBird Installation — Start"
log "Data: $(date)"
log "Log: ${LOG_FILE}"
echo ""
echo -e "${BOLD}Ten skrypt wykona następujące operacje:${NC}"
echo " 1. Walidacja wstępna (system, sieć, DNS)"
echo " 2. Konfiguracja systemu (hostname: ${HOSTNAME}, pakiety)"
echo " 3. Konfiguracja firewalla (UFW)"
echo " 4. Instalacja Docker"
echo " 5. Instalacja NetBird (domena: ${NETBIRD_DOMAIN}, katalog: ${NETBIRD_DIR})"
if [[ "$INSTALL_CROWDSEC" == true ]]; then
echo " 6. Instalacja i konfiguracja CrowdSec"
else
echo " 6. Instalacja i konfiguracja CrowdSec — POMINIĘTA (INSTALL_CROWDSEC=false)"
fi
echo " 7. Weryfikacja końcowa"
echo ""
if ! confirm "Rozpocząć instalację?"; then
log "Przerwano przez użytkownika."
exit 0
fi
preflight_checks
configure_system
configure_ufw
install_docker
install_netbird
if [[ "$INSTALL_CROWDSEC" == true ]]; then
install_crowdsec
configure_crowdsec
else
log "Instalacja CrowdSec pominięta (INSTALL_CROWDSEC=false)."
fi