#!/bin/bash set -euo pipefail # ================================================================ # Apache Guacamole + Nginx + Web GUI — All-in-One Installer # Podržano: Ubuntu/Debian, RHEL/CentOS/Rocky/Alma, Fedora, # Arch/Manjaro, openSUSE # ================================================================ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${GREEN}[✓]${NC} $*"; } warn() { echo -e "${YELLOW}[⚠]${NC} $*"; } error() { echo -e "${RED}[✗]${NC} $*" >&2; exit 1; } section() { echo -e "\n${BOLD}${BLUE}══════════ $* ══════════${NC}"; } [[ $EUID -ne 0 ]] && error "Pokrenite kao root: sudo bash $0" # ── Konfigurabilne varijable ────────────────────────────────────────────────── GUAC_VERSION="1.6.0" GUAC_PORT="8080" INSTALL_DIR="/opt/guacamole" # Direktorij repoa — Web GUI se kopira iz webgui/ pokraj ove skripte. SRC_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # ── OS detekcija ────────────────────────────────────────────────────────────── section "Detekcija operativnog sustava" [[ -f /etc/os-release ]] || error "/etc/os-release nije pronađen — nepodržan OS" source /etc/os-release OS_ID="${ID:-unknown}" OS_VERSION_ID="${VERSION_ID:-0}" OS_LIKE="${ID_LIKE:-}" determine_pkg_manager() { local id="$1" like="$2" ver="$3" if [[ "$id" =~ ^(ubuntu|debian|linuxmint|pop|kali|raspbian)$ ]] \ || echo "$like" | grep -qi debian; then echo "apt" elif [[ "$id" == "fedora" ]]; then echo "dnf" elif [[ "$id" =~ ^(centos|rhel|rocky|almalinux|ol)$ ]] \ || echo "$like" | grep -qiE "rhel|fedora|centos"; then if (( ${ver%%.*} >= 8 )); then echo "dnf"; else echo "yum"; fi elif [[ "$id" =~ ^(arch|manjaro|endeavouros|garuda)$ ]]; then echo "pacman" elif [[ "$id" =~ ^(opensuse|sles|opensuse-leap|opensuse-tumbleweed)$ ]] \ || echo "$like" | grep -qi suse; then echo "zypper" else echo "unknown" fi } PKG=$(determine_pkg_manager "$OS_ID" "$OS_LIKE" "$OS_VERSION_ID") [[ "$PKG" == "unknown" ]] && error "Nepodržani OS: $OS_ID" info "OS: ${PRETTY_NAME:-$OS_ID} | Package manager: $PKG" pkg_update() { case "$PKG" in apt) apt-get update -y ;; dnf) dnf check-update -y || true ;; yum) yum check-update -y || true ;; pacman) pacman -Sy --noconfirm ;; zypper) zypper refresh ;; esac } pkg_install() { case "$PKG" in apt) apt-get install -y "$@" ;; dnf) dnf install -y "$@" ;; yum) yum install -y "$@" ;; pacman) pacman -S --noconfirm "$@" ;; zypper) zypper install -yn "$@" ;; esac } # ── Korisnički unos ─────────────────────────────────────────────────────────── section "Konfiguracija instalacije" echo "" read -rp "$(echo -e "${CYAN}Domena${NC} (npr. rdp.firma.com) ili Enter za IP: ")" DOMAIN DOMAIN="${DOMAIN:-$(hostname -I | awk '{print $1}')}" read -rp "$(echo -e "${CYAN}Nginx Basic Auth${NC} korisničko ime [admin]: ")" BASIC_USER BASIC_USER="${BASIC_USER:-admin}" while true; do read -rsp "$(echo -e "${CYAN}Nginx Basic Auth${NC} lozinka: ")" BASIC_PASS; echo [[ -n "$BASIC_PASS" ]] && break warn "Lozinka ne može biti prazna" done echo "" echo -e "${CYAN}SSL certifikat?${NC}" echo " L = Let's Encrypt (besplatno, treba valjana domena)" echo " S = Self-signed (radi s IP adresom, browser pokazuje upozorenje)" echo " N = Bez SSL (HTTP only)" read -rp "Odabir [L/S/N]: " SSL_CHOICE SSL_CHOICE=$(echo "${SSL_CHOICE:-n}" | tr '[:upper:]' '[:lower:]') DO_SSL="n"; DO_SELFSIGNED="n"; LE_EMAIL="" case "$SSL_CHOICE" in l) DO_SSL="y" read -rp "$(echo -e "${CYAN}Email${NC} za Let's Encrypt: ")" LE_EMAIL [[ -z "$LE_EMAIL" ]] && error "Email je obavezan za Let's Encrypt" ;; s) DO_SELFSIGNED="y" ;; esac echo "" read -rp "$(echo -e "${CYAN}GeoIP blokiranje${NC}? [y/N]: ")" DO_GEOIP DO_GEOIP="${DO_GEOIP:-n}" MM_ACCOUNT_ID=""; MM_LICENSE_KEY=""; GEO_COUNTRIES="" if [[ "$DO_GEOIP" =~ ^[Yy]$ ]]; then warn "Potreban besplatni MaxMind račun: https://www.maxmind.com/en/geolite2/signup" read -rp "MaxMind Account ID: " MM_ACCOUNT_ID read -rp "MaxMind License Key: " MM_LICENSE_KEY read -rp "Dozvoljene zemlje (ISO kodovi, razmak, npr: HR BA SI): " GEO_COUNTRIES fi echo "" echo -e "${CYAN}Reverse proxy${NC} ispred ovog servera (npr. Nginx Proxy Manager)?" echo " Bez ovoga nginx vidi IP proxyja umjesto klijentovog, pa fail2ban" echo " banira proxy i zakljuca sve korisnike odjednom." read -rp "Je li server iza reverse proxyja? [y/N]: " DO_PROXY DO_PROXY="${DO_PROXY:-n}" TRUSTED_PROXY="" if [[ "$DO_PROXY" =~ ^[Yy]$ ]]; then while true; do read -rp "IP adresa proxyja (npr. 192.168.1.5 ili 10.0.0.0/24): " TRUSTED_PROXY [[ -n "$TRUSTED_PROXY" ]] && break warn "Adresa ne moze biti prazna" done fi echo "" read -rp "$(echo -e "${CYAN}Web GUI${NC} — instalirati web sučelje za upravljanje? [y/N]: ")" DO_WEBGUI DO_WEBGUI="${DO_WEBGUI:-n}" GUI_USER="admin"; GUI_PASS=""; GUI_PORT=5555; GUI_PATH="/guacadmin/" if [[ "$DO_WEBGUI" =~ ^[Yy]$ ]]; then read -rp "GUI korisničko ime [admin]: " GUI_USER GUI_USER="${GUI_USER:-admin}" while true; do read -rsp "GUI lozinka (min 8 znakova): " GUI_PASS; echo [[ ${#GUI_PASS} -ge 8 ]] && break warn "Lozinka mora imati najmanje 8 znakova" done read -rp "GUI URL path [/guacadmin/]: " inp_path GUI_PATH="${inp_path:-/guacadmin/}" [[ "$GUI_PATH" != /* ]] && GUI_PATH="/${GUI_PATH}" [[ "$GUI_PATH" != */ ]] && GUI_PATH="${GUI_PATH}/" fi # Generirane lozinke POSTGRES_PASS=$(head -c 512 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | cut -c1-32) SECRET_KEY=$(openssl rand -hex 32) GUI_SECRET=$(openssl rand -hex 32) echo "" echo -e "${BOLD}${YELLOW}════ Sačuvajte ove podatke ════${NC}" echo -e " PostgreSQL lozinka : ${CYAN}${POSTGRES_PASS}${NC}" echo -e " Guacamole admin : ${CYAN}guacadmin / guacadmin${NC} (promijenite odmah!)" [[ "$DO_WEBGUI" =~ ^[Yy]$ ]] && echo -e " Web GUI : ${CYAN}${GUI_USER} / ${GUI_PASS}${NC}" [[ "$DO_SELFSIGNED" =~ ^[Yy]$ ]] && echo -e " SSL tip : ${CYAN}Self-signed (browser upozorenje je normalno)${NC}" echo -e "${BOLD}${YELLOW}════════════════════════════════${NC}" echo "" read -rp "Pritisnite Enter za nastavak..." # ── Ažuriranje i preduvjeti ─────────────────────────────────────────────────── section "Ažuriranje i instalacija preduvjeta" pkg_update case "$PKG" in apt) pkg_install curl wget gnupg2 ca-certificates lsb-release \ openssl apache2-utils nginx fail2ban ;; dnf|yum) if [[ "$OS_ID" =~ ^(centos|rhel|rocky|almalinux|ol)$ ]]; then pkg_install epel-release 2>/dev/null || true pkg_update fi pkg_install curl wget gnupg2 ca-certificates openssl \ httpd-tools nginx fail2ban ;; pacman) pkg_install curl wget openssl apache nginx fail2ban ;; zypper) pkg_install curl wget openssl apache2-utils nginx fail2ban ;; esac info "Preduvjeti instalirani" # ── Docker ──────────────────────────────────────────────────────────────────── section "Docker" install_docker_apt() { local distro_id="$1" install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/${distro_id}/gpg" \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg local codename codename=$(. /etc/os-release && echo "${VERSION_CODENAME:-$(lsb_release -cs 2>/dev/null)}") echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/${distro_id} ${codename} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update -y pkg_install docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin } if command -v docker &>/dev/null; then info "Docker već instaliran: $(docker --version)" else info "Instaliranje Docker-a..." case "$PKG" in apt) local_id="$OS_ID" [[ "$OS_ID" =~ ^(linuxmint|pop)$ ]] && local_id="ubuntu" [[ "$OS_ID" =~ ^(kali|raspbian)$ ]] && local_id="debian" install_docker_apt "$local_id" ;; dnf|yum) pkg_install yum-utils yum-config-manager --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo pkg_install docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin ;; pacman) pkg_install docker docker-compose ;; zypper) zypper addrepo https://download.docker.com/linux/opensuse/docker-ce.repo || true zypper refresh pkg_install docker-ce docker-ce-cli containerd.io docker-compose-plugin ;; esac systemctl enable --now docker info "Docker instaliran: $(docker --version)" fi if docker compose version &>/dev/null 2>&1; then COMPOSE_CMD="docker compose" info "Docker Compose plugin: $(docker compose version)" elif command -v docker-compose &>/dev/null; then COMPOSE_CMD="docker-compose" info "Docker Compose standalone: $(docker-compose --version)" else error "Docker Compose nije pronađen. Reinstalirajte Docker." fi # ── Guacamole ───────────────────────────────────────────────────────────────── section "Priprema Guacamole" mkdir -p "${INSTALL_DIR}"/{extensions,db-init} info "Radni direktorij: ${INSTALL_DIR}" info "Preuzimanje SQL init skripte..." docker run --rm "guacamole/guacamole:${GUAC_VERSION}" \ /opt/guacamole/bin/initdb.sh --postgresql \ > "${INSTALL_DIR}/db-init/initdb.sql" info "SQL init skripta preuzeta" section "TOTP 2FA ekstenzija" # Image već sadrži TOTP jar u /opt/guacamole/totp — aktivira se s # TOTP_ENABLED=true u compose datoteci (vidi niže). Ručno preuzimanje # nije potrebno i stvorilo bi duplu ekstenziju. info "TOTP i brute-force ban aktiviraju se preko env varijabli (ugrađeni u image)" cat > "${INSTALL_DIR}/.env" << EOF POSTGRES_PASS=${POSTGRES_PASS} GUAC_VERSION=${GUAC_VERSION} SECRET_KEY=${SECRET_KEY} EOF chmod 600 "${INSTALL_DIR}/.env" section "docker-compose.yml" cat > "${INSTALL_DIR}/docker-compose.yml" << EOF version: '3.8' services: guacd: image: guacamole/guacd:${GUAC_VERSION} container_name: guacd restart: unless-stopped networks: [guac_net] postgres: image: postgres:15-alpine container_name: guac_postgres restart: unless-stopped environment: POSTGRES_DB: guacamole_db POSTGRES_USER: guacamole_user POSTGRES_PASSWORD: \${POSTGRES_PASS} volumes: - postgres_data:/var/lib/postgresql/data - ./db-init/initdb.sql:/docker-entrypoint-initdb.d/initdb.sql:ro networks: [guac_net] healthcheck: test: ["CMD-SHELL", "pg_isready -U guacamole_user -d guacamole_db"] interval: 10s timeout: 5s retries: 10 guacamole: image: guacamole/guacamole:${GUAC_VERSION} container_name: guacamole restart: unless-stopped depends_on: postgres: {condition: service_healthy} guacd: {condition: service_started} environment: GUACD_HOSTNAME: guacd POSTGRESQL_HOSTNAME: postgres POSTGRESQL_DATABASE: guacamole_db POSTGRESQL_USERNAME: guacamole_user POSTGRESQL_PASSWORD: \${POSTGRES_PASS} TOTP_ENABLED: "true" BAN_ENABLED: "true" # Bez RemoteIpValve kontejner vidi samo docker bridge kao izvor, pa bi # ban ekstenzija banirala cijeli proxy put i zakljucala sve korisnike. # internalProxies pokriva samo docker mreze, da LAN klijenti ostanu # prepoznati kao stvarni klijenti. REMOTE_IP_VALVE_ENABLED: "true" REMOTE_IP_VALVE_INTERNAL_PROXIES: "172\\\\.(1[6-9]|2[0-9]|3[01])\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}" # Stvarni GUACAMOLE_HOME je /home/guacamole/.guacamole; ova varijabla # označava predložak koji start.sh kopira u njega, pa je montirani # extensions/ direktorij time stvarno u upotrebi. GUACAMOLE_HOME: /etc/guacamole volumes: - ./extensions:/etc/guacamole/extensions:ro ports: - "127.0.0.1:${GUAC_PORT}:8080" networks: [guac_net] networks: guac_net: driver: bridge volumes: postgres_data: EOF info "docker-compose.yml kreiran" section "Pokretanje kontejnera" cd "${INSTALL_DIR}" $COMPOSE_CMD up -d info "Čekanje PostgreSQL (max 90s)..." for i in $(seq 1 18); do docker exec guac_postgres pg_isready -U guacamole_user -d guacamole_db &>/dev/null \ && { info "PostgreSQL spreman"; break; } [[ $i -eq 18 ]] && error "PostgreSQL timeout. Provjeri: docker logs guac_postgres" sleep 5 done info "Čekanje Guacamole aplikacije (30s)..." sleep 30 docker ps --filter "name=guacamole" --filter "status=running" | grep -q guacamole \ || error "Guacamole se nije pokrenuo. Provjeri: docker logs guacamole" info "Guacamole kontejner aktivan" # ── Nginx ───────────────────────────────────────────────────────────────────── section "Nginx konfiguracija" systemctl enable nginx cat > /etc/nginx/conf.d/guacamole-maps.conf << 'EOF' map $http_upgrade $connection_upgrade { default upgrade; '' close; } # Guacamole je Angular SPA i na jedno otvaranje ispali desetke # zahtjeva; 10r/m je rusilo ucitavanje. Brute-force pokriva fail2ban. limit_req_zone $binary_remote_addr zone=guac_req:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=guac_ws:10m rate=30r/m; EOF # Iza reverse proxyja je $remote_addr uvijek IP proxyja. Bez ovoga # fail2ban banira proxy (pa i sve korisnike odjednom), rate limit # postaje jedan zajednicki budzet, a GeoIP gleda krivi IP. if [[ -n "$TRUSTED_PROXY" ]]; then cat > /etc/nginx/conf.d/guacamole-realip.conf << EOF set_real_ip_from ${TRUSTED_PROXY}; real_ip_header X-Forwarded-For; real_ip_recursive on; EOF info "Real IP: vjerujem X-Forwarded-For zaglavlju od ${TRUSTED_PROXY}" else rm -f /etc/nginx/conf.d/guacamole-realip.conf fi if [[ "$PKG" == "apt" ]]; then NGINX_CONF="/etc/nginx/sites-available/guacamole" NGINX_ENABLED="/etc/nginx/sites-enabled/guacamole" mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled rm -f /etc/nginx/sites-enabled/default else NGINX_CONF="/etc/nginx/conf.d/guacamole.conf" NGINX_ENABLED="" fi cat > "$NGINX_CONF" << EOF server { listen 80; server_name ${DOMAIN}; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; location = / { return 301 /guacamole/; } location /guacamole/ { auth_basic "Restricted Access"; auth_basic_user_file /etc/nginx/.guac_htpasswd; limit_req zone=guac_req burst=50 nodelay; proxy_pass http://127.0.0.1:${GUAC_PORT}/guacamole/; proxy_buffering off; proxy_http_version 1.1; proxy_request_buffering off; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 10s; } } EOF [[ -n "$NGINX_ENABLED" ]] && ln -sf "$NGINX_CONF" "$NGINX_ENABLED" nginx -t || error "Nginx konfiguracija sadrži grešku" systemctl reload nginx info "Nginx konfiguriran" # ── Basic Auth ──────────────────────────────────────────────────────────────── section "HTTP Basic Auth" htpasswd -bc /etc/nginx/.guac_htpasswd "$BASIC_USER" "$BASIC_PASS" chmod 640 /etc/nginx/.guac_htpasswd chown root:www-data /etc/nginx/.guac_htpasswd 2>/dev/null || \ chown root:nginx /etc/nginx/.guac_htpasswd 2>/dev/null || true info "Basic Auth postavljen za: ${BASIC_USER}" # ── fail2ban ────────────────────────────────────────────────────────────────── section "fail2ban" cat > /etc/fail2ban/filter.d/nginx-basicauth.conf << 'EOF' [Definition] failregex = ^ .* "(GET|POST|HEAD) /guacamole.*" 401 ignoreregex = EOF cat > /etc/fail2ban/filter.d/nginx-guacamole.conf << 'EOF' [Definition] failregex = ^ .* "POST /guacamole/api/tokens HTTP.*" 403 ignoreregex = EOF cat > /etc/fail2ban/jail.d/guacamole.conf << EOF # Debian/Ubuntu u jail.d/defaults-debian.conf postavlja globalni # backend = systemd. Bez eksplicitnog backenda ovi jailovi naslijede # systemd, ignoriraju logpath i ne procitaju nijednu liniju # nginx access loga — nikad nista ne banaju. [nginx-basicauth] enabled = true backend = auto port = http,https filter = nginx-basicauth logpath = /var/log/nginx/access.log maxretry = 5 findtime = 300 bantime = 3600 [nginx-guacamole] enabled = true backend = auto port = http,https filter = nginx-guacamole logpath = /var/log/nginx/access.log maxretry = 5 findtime = 300 bantime = 3600 [sshd] enabled = true maxretry = 3 findtime = 300 bantime = 86400 EOF systemctl enable --now fail2ban info "fail2ban konfiguriran" # ── SSL ─────────────────────────────────────────────────────────────────────── if [[ "$DO_SSL" =~ ^[Yy]$ ]]; then section "SSL (Let's Encrypt)" case "$PKG" in apt) pkg_install certbot python3-certbot-nginx ;; dnf|yum) pkg_install certbot python3-certbot-nginx ;; pacman) pkg_install certbot certbot-nginx ;; zypper) pkg_install certbot python3-certbot-nginx || warn "certbot-nginx instaliraj ručno" ;; esac certbot --nginx -d "$DOMAIN" --email "$LE_EMAIL" \ --agree-tos --non-interactive --redirect if systemctl list-unit-files certbot.timer &>/dev/null; then systemctl enable --now certbot.timer else echo "0 3 * * * root certbot renew --quiet --post-hook 'systemctl reload nginx'" >> /etc/crontab fi nginx -t && systemctl reload nginx info "SSL aktivan, auto-obnova postavljena" fi # ── Self-signed SSL ─────────────────────────────────────────────────────────── if [[ "$DO_SELFSIGNED" =~ ^[Yy]$ ]]; then section "Self-signed SSL certifikat" CERT_DIR="/etc/ssl/guacamole" mkdir -p "$CERT_DIR" # SAN: IP adresa ili domena if [[ "${DOMAIN}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then SAN="IP:${DOMAIN}" else SAN="DNS:${DOMAIN}" fi openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout "${CERT_DIR}/selfsigned.key" \ -out "${CERT_DIR}/selfsigned.crt" \ -subj "/CN=${DOMAIN}/O=Guacamole/C=HR" \ -addext "subjectAltName=${SAN}" 2>/dev/null chmod 600 "${CERT_DIR}/selfsigned.key" info "Self-signed certifikat kreiran (vrijedi 10 godina)" # Prepiši nginx config za HTTPS cat > "$NGINX_CONF" << EOF # HTTP → HTTPS redirect server { listen 80; server_name ${DOMAIN}; return 301 https://\$host\$request_uri; } server { listen 443 ssl; server_name ${DOMAIN}; ssl_certificate ${CERT_DIR}/selfsigned.crt; ssl_certificate_key ${CERT_DIR}/selfsigned.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; location = / { return 301 /guacamole/; } location /guacamole/ { auth_basic "Restricted Access"; auth_basic_user_file /etc/nginx/.guac_htpasswd; limit_req zone=guac_req burst=50 nodelay; proxy_pass http://127.0.0.1:${GUAC_PORT}/guacamole/; proxy_buffering off; proxy_http_version 1.1; proxy_request_buffering off; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \$connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_connect_timeout 10s; } } EOF [[ -n "$NGINX_ENABLED" ]] && ln -sf "$NGINX_CONF" "$NGINX_ENABLED" nginx -t && systemctl reload nginx info "Nginx konfiguriran za HTTPS (self-signed)" warn "Browser će pokazati upozorenje — to je normalno za self-signed certifikat" warn "Chrome: klikni 'Napredno' → 'Nastavi na ${DOMAIN}'" fi # ── GeoIP ───────────────────────────────────────────────────────────────────── if [[ "$DO_GEOIP" =~ ^[Yy]$ ]]; then section "GeoIP blokiranje" case "$PKG" in apt) pkg_install libnginx-mod-http-geoip2 geoipupdate || warn "Provjeri apt repozitorij" ;; dnf|yum) pkg_install nginx-mod-http-geoip2 || warn "Modul možda nije dostupan" ;; pacman) warn "GeoIP na Arch-u: instaliraj ručno (AUR: nginx-mod-http-geoip2)" ;; esac if command -v geoipupdate &>/dev/null; then mkdir -p /etc/GeoIP /usr/share/GeoIP cat > /etc/GeoIP.conf << EOF AccountID ${MM_ACCOUNT_ID} LicenseKey ${MM_LICENSE_KEY} EditionIDs GeoLite2-Country DatabaseDirectory /usr/share/GeoIP EOF geoipupdate && info "GeoIP baza preuzeta" || warn "geoipupdate nije uspio" fi COUNTRY_MAP_ENTRIES="" for cc in $GEO_COUNTRIES; do COUNTRY_MAP_ENTRIES+=" ${cc} 1;\n"; done cat > /etc/nginx/conf.d/guacamole-geoip.conf << EOF geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb { \$geoip2_data_country_code country iso_code; } map \$geoip2_data_country_code \$allowed_country { default 0; $(echo -e "$COUNTRY_MAP_ENTRIES")} EOF sed -i '/auth_basic.*Restricted/i\ if ($allowed_country = 0) { return 403; }' "$NGINX_CONF" nginx -t && systemctl reload nginx info "GeoIP aktivan — dozvoljene: ${GEO_COUNTRIES}" fi # ── Firewall ────────────────────────────────────────────────────────────────── section "Firewall" if command -v ufw &>/dev/null; then ufw allow 22/tcp comment "SSH" 2>/dev/null || true ufw allow 80/tcp comment "HTTP" 2>/dev/null || true ufw allow 443/tcp comment "HTTPS" 2>/dev/null || true ufw --force enable info "UFW pravila postavljena" elif command -v firewall-cmd &>/dev/null; then firewall-cmd --permanent --add-service={ssh,http,https} 2>/dev/null || true firewall-cmd --reload info "firewalld pravila postavljena" else warn "Firewall nije pronađen — konfigurirajte ručno" fi # ── Web GUI ─────────────────────────────────────────────────────────────────── if [[ "$DO_WEBGUI" =~ ^[Yy]$ ]]; then section "Web GUI instalacija" WEBGUI_DIR="${INSTALL_DIR}/webgui" mkdir -p "${WEBGUI_DIR}/templates" # Python case "$PKG" in apt) pkg_install python3 python3-pip python3-venv ;; dnf|yum) pkg_install python3 python3-pip ;; pacman) pkg_install python python-pip ;; zypper) pkg_install python3 python3-pip ;; esac info "Python: $(python3 --version)" # ── Aplikacija ────────────────────────────────────────────── # Izvor je webgui/ pokraj ove skripte. Ranije je install.sh nosio # vlastitu inline kopiju koja se razisla s repoom — nedostajale su # stranice Backup i Azuriranja koje README dokumentira. if [[ ! -f "${SRC_DIR}/webgui/app.py" ]]; then error "Nedostaje ${SRC_DIR}/webgui/ — kopirajte cijeli repo na server, ne samo install.sh" fi cp "${SRC_DIR}/webgui/app.py" "${WEBGUI_DIR}/app.py" cp "${SRC_DIR}/webgui/templates/"*.html "${WEBGUI_DIR}/templates/" info "Web GUI kopiran ($(find "${SRC_DIR}/webgui/templates" -name '*.html' | wc -l) predlozaka)" # ── config.json ───────────────────────────────────────────── cat > "${WEBGUI_DIR}/config.json" << EOF {"username": "${GUI_USER}", "password": "${GUI_PASS}"} EOF chmod 600 "${WEBGUI_DIR}/config.json" # ── Flask virtualenv ───────────────────────────────────────── python3 -m venv "${WEBGUI_DIR}/venv" "${WEBGUI_DIR}/venv/bin/pip" install --quiet --upgrade pip "${WEBGUI_DIR}/venv/bin/pip" install --quiet flask info "Flask instaliran" # ── Systemd servis ─────────────────────────────────────────── cat > /etc/systemd/system/guacamole-webgui.service << EOF [Unit] Description=Guacamole Web Management GUI After=network.target docker.service Requires=docker.service [Service] Type=simple User=root WorkingDirectory=${WEBGUI_DIR} Environment=PORT=${GUI_PORT} Environment=SECRET_KEY=${GUI_SECRET} Environment=GUI_PATH=${GUI_PATH} ExecStart=${WEBGUI_DIR}/venv/bin/python3 ${WEBGUI_DIR}/app.py Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now guacamole-webgui info "Servis guacamole-webgui pokrenut" # ── Nginx location za GUI ──────────────────────────────────── GUI_LOCATION=" # Guacamole Web GUI location ${GUI_PATH} { auth_basic \"Restricted Access\"; auth_basic_user_file /etc/nginx/.guac_htpasswd; proxy_pass http://127.0.0.1:${GUI_PORT}/; proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 3600s; proxy_set_header X-Accel-Buffering no; }" python3 - << PYEOF content = open('${NGINX_CONF}').read() block = """${GUI_LOCATION}""" idx = content.rfind('}') open('${NGINX_CONF}', 'w').write(content[:idx] + block + '\n}') PYEOF nginx -t && systemctl reload nginx info "Nginx location ${GUI_PATH} dodan" fi # ── Status provjera ─────────────────────────────────────────────────────────── section "Status servisa" check_service() { systemctl is-active --quiet "$1" 2>/dev/null \ && info "$1: aktivan" || warn "$1: NIJE aktivan" } check_container() { docker ps --filter "name=$1" --filter "status=running" | grep -q "$1" \ && info "Docker [$1]: aktivan" || warn "Docker [$1]: NIJE aktivan" } check_service nginx check_service fail2ban check_service docker check_container guacamole check_container guacd check_container guac_postgres [[ "$DO_WEBGUI" =~ ^[Yy]$ ]] && check_service guacamole-webgui # ── Finalni sažetak ─────────────────────────────────────────────────────────── section "Instalacija završena" PROTO="http" [[ "$DO_SSL" =~ ^[Yy]$ ]] && PROTO="https" [[ "$DO_SELFSIGNED" =~ ^[Yy]$ ]] && PROTO="https" echo "" echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════════${NC}" echo -e "${BOLD} Guacamole:${NC}" echo -e " ${CYAN}${PROTO}://${DOMAIN}/guacamole/${NC}" echo "" echo -e "${BOLD} Nginx Basic Auth:${NC} ${BASIC_USER} / [vaša lozinka]" echo -e "${BOLD} Guacamole admin:${NC} guacadmin / guacadmin ${RED}← PROMIJENI!${NC}" if [[ "$DO_WEBGUI" =~ ^[Yy]$ ]]; then echo "" echo -e "${BOLD} Web GUI:${NC}" echo -e " ${CYAN}${PROTO}://${DOMAIN}${GUI_PATH}${NC}" echo -e " Korisnik: ${GUI_USER} | Lozinka: ${GUI_PASS}" fi [[ "$DO_GEOIP" =~ ^[Yy]$ ]] && echo -e "\n${BOLD} GeoIP:${NC} dozvoljene države: ${GEO_COUNTRIES}" echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════════${NC}" echo "" # Spremi sve u credentials datoteku CRED_FILE="${INSTALL_DIR}/credentials.txt" cat > "$CRED_FILE" << EOF Guacamole instalacija ===================== Datum: $(date) Guacamole URL : ${PROTO}://${DOMAIN}/guacamole/ Nginx Basic Auth: ${BASIC_USER} / ${BASIC_PASS} Admin login : guacadmin / guacadmin (PROMIJENI!) PostgreSQL pass : ${POSTGRES_PASS} EOF if [[ "$DO_SELFSIGNED" =~ ^[Yy]$ ]]; then cat >> "$CRED_FILE" << EOF SSL : Self-signed, vrijedi 10 god Certifikat : /etc/ssl/guacamole/selfsigned.crt Privatni ključ : /etc/ssl/guacamole/selfsigned.key EOF fi if [[ "$DO_WEBGUI" =~ ^[Yy]$ ]]; then cat >> "$CRED_FILE" << EOF Web GUI URL : ${PROTO}://${DOMAIN}${GUI_PATH} GUI korisnik : ${GUI_USER} / ${GUI_PASS} EOF fi chmod 600 "$CRED_FILE" info "Sve lozinke sačuvane u: ${CRED_FILE} (samo root može čitati)"