ee4c7875ad
- Test-Seite (<h1>… OK</h1>) wird nur noch geschrieben wenn der Webroot leer ist, sonst klobbert ein zweiter Lauf existierende Hugo-Inhalte. - chown -R statt chown auf den Webroot, damit der SFTP-User auch Files die der Script-Lauf erzeugt hat (z.B. die Test-Seite selbst) überschreiben kann. Sonst SSH_FX_PERMISSION_DENIED beim Re-Upload derselben Datei via SFTP.
286 lines
9.3 KiB
Bash
Executable File
286 lines
9.3 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# setup-hugo-host.sh
|
|
#
|
|
# Richtet einen frischen Debian-12-LXC-Container als Hugo-Hosting-Server ein:
|
|
# - nginx auf Port 80 (TLS via vorgelagertem Reverse-Proxy, z.B. NPM)
|
|
# - SFTP-only-User für Deployment (chroot auf /var/www)
|
|
# - ufw, fail2ban, unattended-upgrades, sysctl-hardening
|
|
#
|
|
# Verwendung:
|
|
# ./setup-hugo-host.sh <domain>
|
|
#
|
|
# Beispiel:
|
|
# ./setup-hugo-host.sh example.com
|
|
#
|
|
# Wichtig:
|
|
# Nach dem Lauf ist KEIN interaktiver SSH-Login mehr möglich (nur SFTP).
|
|
# Administration erfolgt über die Proxmox-Konsole: pct enter <vmid>
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# ── KONFIGURATION ─────────────────────────────────────────────────────────────
|
|
# Public Keys, die SFTP-Zugriff erhalten. Mehrere Keys: eine Zeile pro Key.
|
|
|
|
SSH_PUBKEYS="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDMeSZTfX3tLpgujHXzP+VrbbxO1/cificO8beYvaTyb karim@mac"
|
|
|
|
# ── Argumente ─────────────────────────────────────────────────────────────────
|
|
|
|
if [ $# -ne 1 ]; then
|
|
echo "Usage: $0 <domain>"
|
|
echo ""
|
|
echo "Example:"
|
|
echo " $0 example.com"
|
|
exit 1
|
|
fi
|
|
|
|
DOMAIN="$1"
|
|
SFTP_USER="webedit-$(echo "$DOMAIN" | tr '.' '-')"
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "Bitte als root ausführen."
|
|
exit 1
|
|
fi
|
|
|
|
echo "==============================================="
|
|
echo "Hugo-Host Setup"
|
|
echo " Domain: $DOMAIN"
|
|
echo " SFTP-User: $SFTP_USER"
|
|
echo "==============================================="
|
|
echo ""
|
|
|
|
# ── 1. Pakete ─────────────────────────────────────────────────────────────────
|
|
|
|
echo "[1/9] Pakete installieren..."
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get upgrade -y -qq
|
|
apt-get install -y -qq nginx ufw fail2ban unattended-upgrades curl ca-certificates
|
|
|
|
# ── 2. SSH-Daemon als reiner SFTP-Server ──────────────────────────────────────
|
|
|
|
echo "[2/9] SSH-Daemon auf SFTP-only konfigurieren..."
|
|
|
|
getent group sftp-users >/dev/null || groupadd sftp-users
|
|
|
|
cat > /etc/ssh/sshd_config <<'EOF'
|
|
PermitRootLogin no
|
|
PasswordAuthentication no
|
|
PubkeyAuthentication yes
|
|
KbdInteractiveAuthentication no
|
|
|
|
X11Forwarding no
|
|
AllowAgentForwarding no
|
|
AllowTcpForwarding no
|
|
PermitTunnel no
|
|
MaxAuthTries 3
|
|
ClientAliveInterval 300
|
|
ClientAliveCountMax 2
|
|
|
|
AllowGroups sftp-users
|
|
|
|
Subsystem sftp internal-sftp
|
|
|
|
Match Group sftp-users
|
|
ChrootDirectory /var/www
|
|
ForceCommand internal-sftp -u 0002
|
|
AuthorizedKeysFile /etc/ssh/sftp-keys/%u/authorized_keys
|
|
EOF
|
|
|
|
sshd -t
|
|
systemctl restart ssh
|
|
|
|
# ── 3. Firewall ───────────────────────────────────────────────────────────────
|
|
|
|
echo "[3/9] UFW konfigurieren..."
|
|
|
|
ufw --force reset >/dev/null
|
|
ufw default deny incoming
|
|
ufw default allow outgoing
|
|
ufw allow 22/tcp
|
|
ufw allow 80/tcp
|
|
ufw allow 443/tcp
|
|
ufw --force enable
|
|
|
|
# ── 4. fail2ban ───────────────────────────────────────────────────────────────
|
|
|
|
echo "[4/9] fail2ban konfigurieren..."
|
|
|
|
cat > /etc/fail2ban/jail.local <<'EOF'
|
|
[DEFAULT]
|
|
bantime = 1h
|
|
findtime = 10m
|
|
maxretry = 4
|
|
|
|
[sshd]
|
|
enabled = true
|
|
|
|
[nginx-http-auth]
|
|
enabled = true
|
|
|
|
[nginx-botsearch]
|
|
enabled = true
|
|
EOF
|
|
|
|
systemctl enable --now fail2ban
|
|
|
|
# ── 5. Automatische Security-Updates ──────────────────────────────────────────
|
|
|
|
echo "[5/9] Automatische Updates aktivieren..."
|
|
|
|
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
|
APT::Periodic::Update-Package-Lists "1";
|
|
APT::Periodic::Unattended-Upgrade "1";
|
|
APT::Periodic::AutocleanInterval "7";
|
|
EOF
|
|
|
|
sed -i 's|^//Unattended-Upgrade::Automatic-Reboot "false";|Unattended-Upgrade::Automatic-Reboot "true";|' \
|
|
/etc/apt/apt.conf.d/50unattended-upgrades
|
|
sed -i 's|^//Unattended-Upgrade::Automatic-Reboot-Time "02:00";|Unattended-Upgrade::Automatic-Reboot-Time "03:00";|' \
|
|
/etc/apt/apt.conf.d/50unattended-upgrades
|
|
|
|
# ── 6. Sysctl-Hardening ───────────────────────────────────────────────────────
|
|
|
|
echo "[6/9] Sysctl-Hardening..."
|
|
|
|
cat > /etc/sysctl.d/99-hardening.conf <<'EOF'
|
|
net.ipv4.conf.all.rp_filter = 1
|
|
net.ipv4.conf.all.accept_redirects = 0
|
|
net.ipv4.conf.all.send_redirects = 0
|
|
net.ipv4.tcp_syncookies = 1
|
|
net.ipv6.conf.all.accept_redirects = 0
|
|
kernel.dmesg_restrict = 1
|
|
EOF
|
|
|
|
# Nur unsere eigene Datei anwenden, nicht --system (Debian-Defaults setzen Keys,
|
|
# die in einem unprivileged LXC read-only sind und set -e auslösen würden).
|
|
# || true: in unpriv LXC ist z.B. kernel.dmesg_restrict ebenfalls read-only —
|
|
# nicht kritisch, einfach ignorieren.
|
|
sysctl -p /etc/sysctl.d/99-hardening.conf >/dev/null 2>&1 || true
|
|
|
|
# ── 7. Nginx-Site einrichten ──────────────────────────────────────────────────
|
|
|
|
echo "[7/9] Nginx-Site für $DOMAIN einrichten..."
|
|
|
|
rm -f /etc/nginx/sites-enabled/default
|
|
|
|
chown root:root /var/www
|
|
chmod 755 /var/www
|
|
mkdir -p "/var/www/$DOMAIN"
|
|
|
|
cat > "/etc/nginx/sites-available/$DOMAIN" <<EOF
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name $DOMAIN www.$DOMAIN;
|
|
|
|
root /var/www/$DOMAIN;
|
|
index index.html;
|
|
|
|
location / {
|
|
try_files \$uri \$uri/ \$uri.html =404;
|
|
}
|
|
|
|
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2?)\$ {
|
|
expires 30d;
|
|
add_header Cache-Control "public, immutable";
|
|
access_log off;
|
|
}
|
|
|
|
location ~ /\. {
|
|
deny all;
|
|
return 404;
|
|
}
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
|
}
|
|
EOF
|
|
|
|
ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN"
|
|
|
|
# Test-Seite nur schreiben wenn Webroot noch leer ist — sonst würde ein erneuter
|
|
# Script-Lauf bestehende Hugo-Inhalte überschreiben.
|
|
if [ -z "$(ls -A "/var/www/$DOMAIN" 2>/dev/null)" ]; then
|
|
echo "<h1>$DOMAIN — OK</h1>" > "/var/www/$DOMAIN/index.html"
|
|
fi
|
|
|
|
nginx -t
|
|
systemctl reload nginx
|
|
|
|
# ── 8. SFTP-User anlegen ──────────────────────────────────────────────────────
|
|
|
|
echo "[8/9] SFTP-User $SFTP_USER anlegen..."
|
|
|
|
mkdir -p /etc/ssh/sftp-keys
|
|
chown root:root /etc/ssh/sftp-keys
|
|
chmod 755 /etc/ssh/sftp-keys
|
|
|
|
if id "$SFTP_USER" >/dev/null 2>&1; then
|
|
echo " User $SFTP_USER existiert bereits"
|
|
else
|
|
adduser --disabled-password --gecos "" --shell /usr/sbin/nologin "$SFTP_USER"
|
|
# adduser --disabled-password setzt das Shadow-Feld auf "!" → sshd sieht
|
|
# "Account locked" auch für Key-Auth. passwd -d entfernt das Passwort
|
|
# komplett → Status "NP" → Key-Auth funktioniert.
|
|
passwd -d "$SFTP_USER"
|
|
fi
|
|
|
|
usermod -aG sftp-users "$SFTP_USER"
|
|
|
|
chown -R "$SFTP_USER:sftp-users" "/var/www/$DOMAIN"
|
|
chmod 775 "/var/www/$DOMAIN"
|
|
|
|
mkdir -p "/etc/ssh/sftp-keys/$SFTP_USER"
|
|
chown -R root:root "/etc/ssh/sftp-keys/$SFTP_USER"
|
|
chmod 755 "/etc/ssh/sftp-keys/$SFTP_USER"
|
|
|
|
# ── 9. Public Keys eintragen ──────────────────────────────────────────────────
|
|
|
|
echo "[9/9] Public Keys eintragen..."
|
|
|
|
AUTH_KEYS="/etc/ssh/sftp-keys/$SFTP_USER/authorized_keys"
|
|
|
|
if ! grep -qE '^(ssh-(rsa|ed25519|dss)|ecdsa-sha2-)' <<<"$SSH_PUBKEYS"; then
|
|
echo " ⚠ SSH_PUBKEYS enthält keinen gültigen Public Key — bitte oben im Script setzen."
|
|
exit 1
|
|
fi
|
|
|
|
printf '%s\n' "$SSH_PUBKEYS" > "$AUTH_KEYS"
|
|
chown root:root "$AUTH_KEYS"
|
|
chmod 644 "$AUTH_KEYS"
|
|
|
|
KEY_COUNT=$(grep -cE '^(ssh-(rsa|ed25519|dss)|ecdsa-sha2-)' "$AUTH_KEYS")
|
|
echo " ✓ $KEY_COUNT Public Key(s) eingetragen"
|
|
|
|
# ── Fertig ────────────────────────────────────────────────────────────────────
|
|
|
|
IP=$(hostname -I | awk '{print $1}')
|
|
|
|
echo ""
|
|
echo "==============================================="
|
|
echo " FERTIG"
|
|
echo "==============================================="
|
|
echo ""
|
|
echo "Domain: $DOMAIN"
|
|
echo "Webroot: /var/www/$DOMAIN"
|
|
echo "Container-IP: $IP"
|
|
echo ""
|
|
echo "Deployment (vom lokalen Hugo-Projekt aus):"
|
|
echo ""
|
|
echo " hugo --minify"
|
|
echo " lftp -u $SFTP_USER, sftp://$IP -e \"mirror -R --delete public/ /$DOMAIN/; quit\""
|
|
echo ""
|
|
echo "Alternative mit sftp (ohne --delete):"
|
|
echo " sftp $SFTP_USER@$IP <<< \"put -r public/* /$DOMAIN/\""
|
|
echo ""
|
|
echo "Tipp: für TLS einen Reverse-Proxy (z.B. Nginx Proxy Manager)"
|
|
echo " auf $DOMAIN → http://$IP:80 zeigen lassen."
|
|
echo ""
|
|
echo "Hinweis: SSH-Shell-Login ist deaktiviert (SFTP-only)."
|
|
echo " Administration via Proxmox-Konsole: pct enter <vmid>"
|
|
echo ""
|