e7265ff1d1
1) Debian/Ubuntu u jail.d/defaults-debian.conf postavlja globalni backend = systemd. Nasi nginx jailovi ga nisu nadjacali, pa su naslijedili systemd, ignorirali logpath i citali journal u kojem nginx access loga nema. "fail2ban-client get nginx-basicauth logpath" je vracao "No file is currently monitored" — jailovi nikad nisu banali nista, iako fail2ban-regex nad istim logom nalazi 48 poklapanja. Dodan eksplicitan backend = auto na oba nginx jaila; sshd ostaje na systemd backendu gdje mu je mjesto. 2) Novo pitanje pri instalaciji: je li server iza reverse proxyja. Ako da, generira se conf.d/guacamole-realip.conf sa set_real_ip_from / real_ip_header X-Forwarded-For. Bez toga je $remote_addr uvijek IP proxyja, pa bi fail2ban banirao proxy i zakljucao sve korisnike odjednom, rate limit bi bio jedan zajednicki budzet, a GeoIP bi gledao krivu adresu. Provjereno iza Nginx Proxy Managera: direktan pristup i dalje daje LAN IP, spoofani X-Forwarded-For s nepovjerljive adrese se ignorira, a kroz proxy fail2ban sad biljezi stvarni javni IP klijenta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
819 lines
31 KiB
Bash
Executable File
819 lines
31 KiB
Bash
Executable File
#!/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.5.5"
|
|
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 se aktivira preko TOTP_ENABLED (ugrađen 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_USER: guacamole_user
|
|
POSTGRESQL_PASSWORD: \${POSTGRES_PASS}
|
|
TOTP_ENABLED: "true"
|
|
# 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 = ^<HOST> .* "(GET|POST|HEAD) /guacamole.*" 401
|
|
ignoreregex =
|
|
EOF
|
|
|
|
cat > /etc/fail2ban/filter.d/nginx-guacamole.conf << 'EOF'
|
|
[Definition]
|
|
failregex = ^<HOST> .* "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)"
|