Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
@@ -0,0 +1,891 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# Script Name: setup-web-server.sh
|
||||
# Version: 1.1
|
||||
# Description: Production setup for a dedicated nginx web server hosting a
|
||||
# Hugo static site with GeoIP2 enriched logging. Subcommand-
|
||||
# based: base hardening, nginx install, Hugo,
|
||||
# TLS, security toolkit, and status dashboard.
|
||||
#
|
||||
# Author: Phil Connor
|
||||
# Contact: contact@mylinux.work
|
||||
# Website: https://mylinux.work
|
||||
# License: MIT
|
||||
#
|
||||
# Subcommands:
|
||||
# base — Server hardening (firewall, fail2ban, sysctl, unattended-upgrades)
|
||||
# nginx — Install nginx from official repo + production config
|
||||
# hugo — Install Hugo extended + site directory + deploy script
|
||||
# tls — Let's Encrypt certificates
|
||||
# security — Download and run nginx-security.sh toolkit
|
||||
# status — Dashboard showing all component status
|
||||
# all — Run base → nginx → hugo → tls → security
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./setup-web-server.sh base [OPTIONS]
|
||||
# sudo ./setup-web-server.sh nginx --domain mylinux.work
|
||||
# sudo ./setup-web-server.sh hugo --domain mylinux.work
|
||||
# sudo ./setup-web-server.sh tls --domain mylinux.work --email admin@example.com
|
||||
# sudo ./setup-web-server.sh security
|
||||
# sudo ./setup-web-server.sh status
|
||||
# sudo ./setup-web-server.sh all --domain mylinux.work
|
||||
#
|
||||
################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# SHARED HELPERS
|
||||
# =============================================================================
|
||||
|
||||
# --- Colors (TTY-aware) ---
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
else
|
||||
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" NC=""
|
||||
fi
|
||||
|
||||
# --- Logging (prefixed with # for Prometheus comment compatibility) ---
|
||||
info() { echo -e "# ${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "# ${YELLOW}[WARN]${NC} $*"; }
|
||||
die() { echo -e "# ${RED}[FATAL]${NC} $*" >&2; exit 1; }
|
||||
step() { echo -e "# ${CYAN}[STEP]${NC} $*"; }
|
||||
banner() {
|
||||
echo
|
||||
echo -e "# ${BOLD}════════════════════════════════════════${NC}"
|
||||
echo -e "# ${BOLD} $*${NC}"
|
||||
echo -e "# ${BOLD}════════════════════════════════════════${NC}"
|
||||
echo
|
||||
}
|
||||
|
||||
# --- Root check ---
|
||||
require_root() {
|
||||
[[ $EUID -eq 0 ]] || die "This script must be run as root (or via sudo)"
|
||||
}
|
||||
|
||||
# --- OS detection ---
|
||||
OS="" OSVER="" PKG_MGR=""
|
||||
detect_os() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
OS=$(grep -oP '(?<=^ID=).+' /etc/os-release | tr -d '"' | tr '[:upper:]' '[:lower:]')
|
||||
OSVER=$(grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"' | cut -d. -f1)
|
||||
fi
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
PKG_MGR="apt -y"
|
||||
;;
|
||||
rocky|almalinux|centos|rhel|ol)
|
||||
PKG_MGR=$(command -v dnf >/dev/null 2>&1 && echo "dnf -y" || echo "yum -y")
|
||||
;;
|
||||
*)
|
||||
die "Unsupported OS: ${OS:-unknown}. Requires Ubuntu/Debian or RHEL/Rocky."
|
||||
;;
|
||||
esac
|
||||
info "Detected ${OS} ${OSVER}, using ${PKG_MGR}"
|
||||
}
|
||||
|
||||
# --- Architecture ---
|
||||
detect_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64) echo "amd64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
*) die "Unsupported architecture: $arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Backup file with timestamp ---
|
||||
backup_file() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
local ts
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
cp "$file" "${file}.bak-${ts}"
|
||||
info "Backed up ${file}"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- nginx test and reload ---
|
||||
nginx_test_and_reload() {
|
||||
if nginx -t 2>&1; then
|
||||
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||
systemctl reload nginx
|
||||
info "nginx config valid — reloaded"
|
||||
else
|
||||
systemctl start nginx
|
||||
info "nginx config valid — started"
|
||||
fi
|
||||
else
|
||||
die "nginx config test failed — not reloading"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Generate password ---
|
||||
generate_password() {
|
||||
openssl rand -base64 24 | tr -d '/+=' | head -c 24
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DEFAULTS
|
||||
# =============================================================================
|
||||
|
||||
DOMAIN="mylinux.work"
|
||||
HOSTNAME_SET="web01"
|
||||
TIMEZONE="America/Chicago"
|
||||
ADMIN_EMAIL="contact@mylinux.work"
|
||||
ADMIN_NAME="chiefgeek"
|
||||
ADMIN_PASS=""
|
||||
DB_PASS=""
|
||||
CERTBOT_EMAIL="contact@mylinux.work"
|
||||
HUGO_VERSION="latest"
|
||||
DRY_RUN=false
|
||||
SKIP_HUGO=false
|
||||
SKIP_TLS=false
|
||||
|
||||
# =============================================================================
|
||||
# ARGUMENT PARSING
|
||||
# =============================================================================
|
||||
|
||||
SUBCOMMAND=""
|
||||
|
||||
parse_args() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle help before subcommand assignment
|
||||
case "$1" in
|
||||
--help|-h|help) show_help; exit 0 ;;
|
||||
--*) die "Missing subcommand. Usage: $0 <subcommand> [OPTIONS]\n# Run '$0 --help' for available subcommands." ;;
|
||||
esac
|
||||
|
||||
SUBCOMMAND="$1"
|
||||
shift
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) DOMAIN="$2"; shift 2 ;;
|
||||
--hostname) HOSTNAME_SET="$2"; shift 2 ;;
|
||||
--timezone) TIMEZONE="$2"; shift 2 ;;
|
||||
--admin-email) ADMIN_EMAIL="$2"; shift 2 ;;
|
||||
--admin-name) ADMIN_NAME="$2"; shift 2 ;;
|
||||
--admin-pass) ADMIN_PASS="$2"; shift 2 ;;
|
||||
--db-pass) DB_PASS="$2"; shift 2 ;;
|
||||
--email) CERTBOT_EMAIL="$2"; shift 2 ;;
|
||||
--hugo-version) HUGO_VERSION="$2"; shift 2 ;;
|
||||
--skip-hugo) SKIP_HUGO=true; shift ;;
|
||||
--skip-tls) SKIP_TLS=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--help|-h) show_help; exit 0 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat <<'EOF'
|
||||
setup-web-server.sh — Production web server setup
|
||||
|
||||
USAGE:
|
||||
sudo ./setup-web-server.sh SUBCOMMAND [OPTIONS]
|
||||
|
||||
SUBCOMMANDS:
|
||||
base Server hardening (firewall, fail2ban, sysctl, updates)
|
||||
nginx Install nginx from official repo + production config
|
||||
hugo Install Hugo extended + site directory + deploy script
|
||||
tls Let's Encrypt certificates
|
||||
security Download and run nginx-security.sh toolkit
|
||||
status Show all component status
|
||||
all Run everything: base → nginx → hugo → tls → security
|
||||
|
||||
OPTIONS:
|
||||
--domain DOMAIN Primary domain (default: mylinux.work)
|
||||
--hostname NAME Server hostname (default: web01)
|
||||
--timezone ZONE Timezone (default: America/Chicago)
|
||||
--admin-email EMAIL Admin email (default: contact@mylinux.work)
|
||||
--admin-name NAME Admin username (default: chiefgeek)
|
||||
--admin-pass PASS Admin password (generated if not set)
|
||||
--db-pass PASS MySQL password (generated if not set)
|
||||
--email EMAIL Certbot email (default: contact@mylinux.work)
|
||||
--hugo-version VERSION Hugo version (default: latest)
|
||||
--skip-hugo Skip Hugo install in 'all' (for rsync-based deploys)
|
||||
--skip-tls Skip TLS setup in 'all' (DNS not ready yet)
|
||||
--dry-run Show what would be done
|
||||
--help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
sudo ./setup-web-server.sh base --hostname web01 --timezone America/Chicago
|
||||
sudo ./setup-web-server.sh nginx --domain mylinux.work
|
||||
sudo ./setup-web-server.sh all --domain mylinux.work
|
||||
sudo ./setup-web-server.sh status
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: base
|
||||
# =============================================================================
|
||||
|
||||
cmd_base() {
|
||||
banner "Base Server Setup"
|
||||
|
||||
# --- Hostname ---
|
||||
step "Setting hostname to ${HOSTNAME_SET}"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would set hostname to ${HOSTNAME_SET}"
|
||||
else
|
||||
hostnamectl set-hostname "$HOSTNAME_SET"
|
||||
info "Hostname set to ${HOSTNAME_SET}"
|
||||
fi
|
||||
|
||||
# --- Timezone ---
|
||||
step "Setting timezone to ${TIMEZONE}"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would set timezone to ${TIMEZONE}"
|
||||
else
|
||||
timedatectl set-timezone "$TIMEZONE"
|
||||
info "Timezone set to ${TIMEZONE}"
|
||||
fi
|
||||
|
||||
# --- Update packages ---
|
||||
step "Updating packages"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would update packages"
|
||||
else
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
apt update && apt upgrade -y
|
||||
;;
|
||||
*)
|
||||
$PKG_MGR update
|
||||
;;
|
||||
esac
|
||||
info "Packages updated"
|
||||
fi
|
||||
|
||||
# --- Base tools ---
|
||||
step "Installing base tools"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would install base packages"
|
||||
else
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
apt install -y curl wget git htop tmux unzip jq tree ncdu \
|
||||
fail2ban ufw net-tools dnsutils software-properties-common \
|
||||
gnupg2 ca-certificates lsb-release openssl
|
||||
;;
|
||||
*)
|
||||
$PKG_MGR install curl wget git htop tmux unzip jq tree ncdu \
|
||||
fail2ban firewalld net-tools bind-utils openssl
|
||||
;;
|
||||
esac
|
||||
info "Base tools installed"
|
||||
fi
|
||||
|
||||
# --- Firewall ---
|
||||
step "Configuring firewall"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would configure firewall (allow 22, 80, 443)"
|
||||
else
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
ufw default deny incoming 2>/dev/null || true
|
||||
ufw default allow outgoing 2>/dev/null || true
|
||||
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
|
||||
;;
|
||||
*)
|
||||
systemctl enable --now firewalld
|
||||
firewall-cmd --permanent --add-service=ssh
|
||||
firewall-cmd --permanent --add-service=http
|
||||
firewall-cmd --permanent --add-service=https
|
||||
firewall-cmd --reload
|
||||
;;
|
||||
esac
|
||||
info "Firewall configured"
|
||||
fi
|
||||
|
||||
# --- fail2ban ---
|
||||
step "Configuring fail2ban"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would configure fail2ban for SSH"
|
||||
else
|
||||
local logpath="/var/log/auth.log"
|
||||
[[ "$OS" =~ ^(rocky|almalinux|centos|rhel|ol)$ ]] && logpath="/var/log/secure"
|
||||
|
||||
cat > /etc/fail2ban/jail.local <<JAILEOF
|
||||
[DEFAULT]
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
maxretry = 3
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
logpath = ${logpath}
|
||||
JAILEOF
|
||||
|
||||
systemctl enable --now fail2ban
|
||||
info "fail2ban configured for SSH"
|
||||
fi
|
||||
|
||||
# --- Unattended upgrades ---
|
||||
step "Configuring unattended upgrades"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would enable unattended upgrades"
|
||||
else
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
apt install -y unattended-upgrades
|
||||
dpkg-reconfigure -plow unattended-upgrades 2>/dev/null || true
|
||||
;;
|
||||
*)
|
||||
$PKG_MGR install dnf-automatic
|
||||
sed -i 's/apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.conf 2>/dev/null || true
|
||||
systemctl enable --now dnf-automatic.timer
|
||||
;;
|
||||
esac
|
||||
info "Unattended upgrades enabled"
|
||||
fi
|
||||
|
||||
# --- Sysctl tuning ---
|
||||
step "Applying sysctl tuning for web serving"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would write /etc/sysctl.d/99-web-tuning.conf"
|
||||
else
|
||||
cat > /etc/sysctl.d/99-web-tuning.conf <<'SYSEOF'
|
||||
net.core.somaxconn = 65535
|
||||
net.ipv4.tcp_fastopen = 3
|
||||
fs.file-max = 2097152
|
||||
net.ipv4.tcp_keepalive_time = 600
|
||||
net.ipv4.tcp_keepalive_intvl = 30
|
||||
net.ipv4.tcp_keepalive_probes = 5
|
||||
net.core.rmem_max = 16777216
|
||||
net.core.wmem_max = 16777216
|
||||
net.ipv4.tcp_rmem = 4096 87380 16777216
|
||||
net.ipv4.tcp_wmem = 4096 65536 16777216
|
||||
net.ipv4.tcp_tw_reuse = 1
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
net.ipv4.tcp_max_syn_backlog = 65535
|
||||
SYSEOF
|
||||
sysctl --system >/dev/null 2>&1
|
||||
info "Sysctl tuning applied"
|
||||
fi
|
||||
|
||||
# --- File limits ---
|
||||
step "Setting file descriptor limits"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would set file limits"
|
||||
else
|
||||
if ! grep -q "# web-server-setup" /etc/security/limits.conf 2>/dev/null; then
|
||||
cat >> /etc/security/limits.conf <<'LIMEOF'
|
||||
|
||||
# web-server-setup
|
||||
* soft nofile 65536
|
||||
* hard nofile 65536
|
||||
root soft nofile 65536
|
||||
root hard nofile 65536
|
||||
LIMEOF
|
||||
fi
|
||||
info "File limits configured"
|
||||
fi
|
||||
|
||||
info "Base server setup complete"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: nginx
|
||||
# =============================================================================
|
||||
|
||||
cmd_nginx() {
|
||||
banner "Nginx Installation"
|
||||
|
||||
# --- Install nginx from OS package manager ---
|
||||
step "Installing nginx"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would install nginx"
|
||||
else
|
||||
$PKG_MGR install nginx
|
||||
info "nginx installed: $(nginx -v 2>&1)"
|
||||
fi
|
||||
|
||||
# --- Production nginx.conf ---
|
||||
step "Writing production nginx.conf"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would write /etc/nginx/nginx.conf"
|
||||
else
|
||||
backup_file /etc/nginx/nginx.conf
|
||||
|
||||
local cores
|
||||
cores=$(nproc)
|
||||
local connections=$((cores * 4096))
|
||||
[[ $connections -gt 65536 ]] && connections=65536
|
||||
|
||||
cat > /etc/nginx/nginx.conf <<NGXEOF
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65536;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
events {
|
||||
worker_connections ${connections};
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '\$remote_addr - \$remote_user [\$time_local] '
|
||||
'"\$request" \$status \$body_bytes_sent '
|
||||
'"\$http_referer" "\$http_user_agent" '
|
||||
'ssl=\$ssl_protocol rt=\$request_time '
|
||||
'\$geoip2_country_code \"\$geoip2_asn_org\"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
keepalive_requests 1000;
|
||||
|
||||
client_body_timeout 30;
|
||||
client_header_timeout 30;
|
||||
send_timeout 30;
|
||||
client_body_buffer_size 16k;
|
||||
client_max_body_size 50m;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/plain text/css text/javascript
|
||||
application/javascript application/json
|
||||
application/xml application/rss+xml
|
||||
image/svg+xml;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# Drop requests with no matching server_name
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
NGXEOF
|
||||
|
||||
# Ensure www-data user exists
|
||||
id www-data &>/dev/null || useradd -r -s /usr/sbin/nologin www-data
|
||||
|
||||
info "nginx.conf written (${cores} cores, ${connections} connections)"
|
||||
fi
|
||||
|
||||
# --- Site directories ---
|
||||
step "Creating nginx config structure"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would create conf.d directory and site configs"
|
||||
else
|
||||
mkdir -p /etc/nginx/conf.d
|
||||
# Remove default configs that conflict
|
||||
rm -f /etc/nginx/conf.d/default.conf
|
||||
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- Hugo site server block ---
|
||||
step "Creating nginx config for ${DOMAIN}"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would create /etc/nginx/conf.d/${DOMAIN}.conf"
|
||||
else
|
||||
mkdir -p "/var/www/${DOMAIN}/public"
|
||||
|
||||
cat > "/etc/nginx/conf.d/${DOMAIN}.conf" <<SITEEOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${DOMAIN} www.${DOMAIN};
|
||||
|
||||
root /var/www/${DOMAIN}/public;
|
||||
index index.html;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ =404;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
}
|
||||
SITEEOF
|
||||
info "Created ${DOMAIN} server block"
|
||||
fi
|
||||
|
||||
# --- Start nginx ---
|
||||
if [[ "$DRY_RUN" == "false" ]]; then
|
||||
systemctl enable nginx
|
||||
nginx_test_and_reload
|
||||
info "nginx installed and running"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: hugo
|
||||
# =============================================================================
|
||||
|
||||
cmd_hugo() {
|
||||
banner "Hugo Installation"
|
||||
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
|
||||
# --- Resolve version ---
|
||||
local version="$HUGO_VERSION"
|
||||
if [[ "$version" == "latest" ]]; then
|
||||
step "Resolving latest Hugo version"
|
||||
version=$(curl -fsSL https://api.github.com/repos/gohugoio/hugo/releases/latest | \
|
||||
grep -oP '"tag_name":\s*"v\K[^"]+' | head -1)
|
||||
[[ -z "$version" ]] && die "Could not resolve latest Hugo version"
|
||||
info "Latest Hugo version: ${version}"
|
||||
fi
|
||||
|
||||
# --- Install Hugo ---
|
||||
step "Installing Hugo extended ${version}"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would install Hugo extended ${version}"
|
||||
else
|
||||
if command -v hugo &>/dev/null; then
|
||||
local current
|
||||
current=$(hugo version 2>/dev/null | grep -oP 'v\K[0-9.]+' | head -1)
|
||||
if [[ "$current" == "$version" ]]; then
|
||||
info "Hugo ${version} already installed"
|
||||
else
|
||||
info "Upgrading Hugo from ${current} to ${version}"
|
||||
fi
|
||||
fi
|
||||
|
||||
local url="https://github.com/gohugoio/hugo/releases/download/v${version}/hugo_extended_${version}_linux-${arch}.tar.gz"
|
||||
local tmp="/tmp/hugo-install-$$"
|
||||
mkdir -p "$tmp"
|
||||
wget -qO "${tmp}/hugo.tar.gz" "$url" || die "Failed to download Hugo ${version}"
|
||||
tar -xzf "${tmp}/hugo.tar.gz" -C "$tmp"
|
||||
mv "${tmp}/hugo" /usr/local/bin/hugo
|
||||
chmod +x /usr/local/bin/hugo
|
||||
rm -rf "$tmp"
|
||||
info "Hugo installed: $(hugo version 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# --- Site directory ---
|
||||
step "Creating site directory /var/www/${DOMAIN}"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would create /var/www/${DOMAIN} and deploy user"
|
||||
else
|
||||
mkdir -p "/var/www/${DOMAIN}/public"
|
||||
mkdir -p "/var/www/${DOMAIN}/source"
|
||||
|
||||
# Create deploy user
|
||||
if ! id www-deploy &>/dev/null; then
|
||||
useradd -r -s /bin/bash -m -d /home/www-deploy www-deploy
|
||||
info "Created www-deploy user"
|
||||
fi
|
||||
|
||||
# Ensure www-data group exists
|
||||
getent group www-data &>/dev/null || groupadd -r www-data
|
||||
|
||||
usermod -aG www-data www-deploy 2>/dev/null || true
|
||||
chown -R www-deploy:www-data "/var/www/${DOMAIN}"
|
||||
chmod -R 775 "/var/www/${DOMAIN}"
|
||||
info "Site directory ready"
|
||||
fi
|
||||
|
||||
# --- Deploy script ---
|
||||
step "Creating deploy script"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would create /usr/local/bin/deploy-site.sh"
|
||||
else
|
||||
cat > /usr/local/bin/deploy-site.sh <<DEPLOYEOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SITE_DIR="/var/www/${DOMAIN}"
|
||||
BRANCH="\${1:-main}"
|
||||
|
||||
if [[ ! -d "\$SITE_DIR/source/.git" ]]; then
|
||||
echo "# No git repo in \$SITE_DIR/source — clone your repo first:"
|
||||
echo "# git clone --depth 1 YOUR_REPO_URL \$SITE_DIR/source"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "\$SITE_DIR/source"
|
||||
git fetch origin "\$BRANCH"
|
||||
git reset --hard "origin/\$BRANCH"
|
||||
hugo --minify --destination "\$SITE_DIR/public"
|
||||
echo "# Deploy complete: \$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
DEPLOYEOF
|
||||
chmod +x /usr/local/bin/deploy-site.sh
|
||||
info "Deploy script created at /usr/local/bin/deploy-site.sh"
|
||||
fi
|
||||
|
||||
info "Hugo setup complete"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: tls
|
||||
# =============================================================================
|
||||
|
||||
cmd_tls() {
|
||||
banner "TLS Certificate Setup"
|
||||
|
||||
step "Installing certbot"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would install certbot and obtain certificates"
|
||||
else
|
||||
case "$OS" in
|
||||
ubuntu|debian)
|
||||
apt install -y certbot python3-certbot-nginx
|
||||
;;
|
||||
*)
|
||||
$PKG_MGR install certbot python3-certbot-nginx
|
||||
;;
|
||||
esac
|
||||
|
||||
step "Obtaining certificate for ${DOMAIN}"
|
||||
certbot --nginx -d "${DOMAIN}" -d "www.${DOMAIN}" \
|
||||
--non-interactive --agree-tos --email "${CERTBOT_EMAIL}" \
|
||||
--redirect 2>&1 || warn "certbot for ${DOMAIN} returned non-zero (may already exist)"
|
||||
|
||||
step "Verifying auto-renewal"
|
||||
certbot renew --dry-run 2>&1 || warn "Renewal dry-run had issues"
|
||||
|
||||
info "TLS certificates configured"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: security
|
||||
# =============================================================================
|
||||
|
||||
cmd_security() {
|
||||
banner "Security Toolkit"
|
||||
|
||||
local script_path="/usr/local/bin/nginx-security.sh"
|
||||
|
||||
step "Checking for nginx-security.sh"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
info "[DRY RUN] Would download and run nginx-security.sh"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -f "$script_path" ]]; then
|
||||
step "Downloading nginx-security.sh"
|
||||
wget -qO "$script_path" "https://mylinux.work/downloads/nginx-security.sh" || \
|
||||
die "Failed to download nginx-security.sh"
|
||||
chmod +x "$script_path"
|
||||
info "Downloaded nginx-security.sh"
|
||||
else
|
||||
info "nginx-security.sh already present"
|
||||
fi
|
||||
|
||||
step "Running bot-block"
|
||||
"$script_path" bot-block || warn "bot-block returned non-zero"
|
||||
|
||||
step "Running js-challenge"
|
||||
"$script_path" js-challenge --db-ip || warn "js-challenge returned non-zero"
|
||||
|
||||
step "Running crowdsec"
|
||||
"$script_path" crowdsec || warn "crowdsec returned non-zero"
|
||||
|
||||
echo
|
||||
info "Security toolkit applied"
|
||||
info "Optional: ${script_path} block-head"
|
||||
info "Status: ${script_path} status"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: status
|
||||
# =============================================================================
|
||||
|
||||
cmd_status() {
|
||||
banner "Server Status Dashboard"
|
||||
|
||||
echo -e "# ${BOLD}System${NC}"
|
||||
echo -e "# Hostname: $(hostname)"
|
||||
echo -e "# OS: $(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2)"
|
||||
echo -e "# Kernel: $(uname -r)"
|
||||
echo -e "# Uptime: $(uptime -p 2>/dev/null || uptime)"
|
||||
echo -e "# CPU: $(nproc) cores"
|
||||
echo -e "# RAM: $(free -h | awk '/Mem:/{print $2}') total, $(free -h | awk '/Mem:/{print $3}') used"
|
||||
echo -e "# Disk: $(df -h / | awk 'NR==2{print $3"/"$2" ("$5" used)"}')"
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}Nginx${NC}"
|
||||
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||
echo -e "# Status: ${GREEN}running${NC}"
|
||||
echo -e "# Version: $(nginx -v 2>&1 | cut -d/ -f2)"
|
||||
if nginx -t 2>/dev/null; then
|
||||
echo -e "# Config: ${GREEN}valid${NC}"
|
||||
else
|
||||
echo -e "# Config: ${RED}invalid${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "# Status: ${RED}stopped${NC}"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}Hugo Site${NC}"
|
||||
if [[ -d "/var/www/${DOMAIN}/public" ]]; then
|
||||
local file_count
|
||||
file_count=$(find "/var/www/${DOMAIN}/public" -type f 2>/dev/null | wc -l)
|
||||
echo -e "# Directory: /var/www/${DOMAIN}/public"
|
||||
echo -e "# Files: ${file_count}"
|
||||
if [[ -d "/var/www/${DOMAIN}/source/.git" ]]; then
|
||||
local last_commit
|
||||
last_commit=$(git -C "/var/www/${DOMAIN}/source" log -1 --format='%ci' 2>/dev/null || echo "unknown")
|
||||
echo -e "# Last git: ${last_commit}"
|
||||
fi
|
||||
else
|
||||
echo -e "# Status: ${YELLOW}not deployed${NC}"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}TLS Certificates${NC}"
|
||||
for d in "$DOMAIN"; do
|
||||
local cert="/etc/letsencrypt/live/${d}/fullchain.pem"
|
||||
if [[ -f "$cert" ]]; then
|
||||
local expiry
|
||||
expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
|
||||
echo -e "# ${d}: expires ${expiry}"
|
||||
else
|
||||
echo -e "# ${d}: ${YELLOW}no certificate${NC}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}Firewall${NC}"
|
||||
if command -v ufw &>/dev/null; then
|
||||
local ufw_status
|
||||
ufw_status=$(ufw status 2>/dev/null | head -1)
|
||||
echo -e "# UFW: ${ufw_status}"
|
||||
elif command -v firewall-cmd &>/dev/null; then
|
||||
echo -e "# firewalld: $(firewall-cmd --state 2>/dev/null)"
|
||||
else
|
||||
echo -e "# Status: ${YELLOW}no firewall detected${NC}"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}Fail2ban${NC}"
|
||||
if systemctl is-active --quiet fail2ban 2>/dev/null; then
|
||||
echo -e "# Status: ${GREEN}running${NC}"
|
||||
local jails
|
||||
jails=$(fail2ban-client status 2>/dev/null | grep "Jail list" | cut -d: -f2 | xargs)
|
||||
echo -e "# Jails: ${jails:-none}"
|
||||
else
|
||||
echo -e "# Status: ${RED}stopped${NC}"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo -e "# ${BOLD}CrowdSec${NC}"
|
||||
if systemctl is-active --quiet crowdsec 2>/dev/null; then
|
||||
echo -e "# Engine: ${GREEN}running${NC}"
|
||||
local decisions
|
||||
decisions=$(cscli decisions list -o raw 2>/dev/null | tail -n +2 | wc -l)
|
||||
echo -e "# Decisions: ${decisions}"
|
||||
else
|
||||
echo -e "# Engine: ${YELLOW}not installed${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SUBCOMMAND: all
|
||||
# =============================================================================
|
||||
|
||||
cmd_all() {
|
||||
banner "Full Server Setup"
|
||||
|
||||
if [[ -z "$DOMAIN" ]]; then
|
||||
die "--domain is required for the 'all' subcommand"
|
||||
fi
|
||||
|
||||
cmd_base
|
||||
cmd_nginx
|
||||
if [[ "$SKIP_HUGO" == "true" ]]; then
|
||||
info "Skipping Hugo install (--skip-hugo) — site directory still created by nginx subcommand"
|
||||
else
|
||||
cmd_hugo
|
||||
fi
|
||||
if [[ "$SKIP_TLS" == "true" ]]; then
|
||||
info "Skipping TLS setup (--skip-tls) — run 'tls' subcommand after DNS is pointed"
|
||||
else
|
||||
cmd_tls
|
||||
fi
|
||||
cmd_security
|
||||
|
||||
banner "Setup Complete"
|
||||
echo
|
||||
echo -e "# ${BOLD}Summary${NC}"
|
||||
echo -e "# Hugo site: https://${DOMAIN}"
|
||||
echo
|
||||
echo -e "# ${BOLD}Next Steps${NC}"
|
||||
echo -e "# 1. Clone your Hugo site repo to /var/www/${DOMAIN}/source"
|
||||
echo -e "# 2. Run deploy-site.sh to build"
|
||||
echo -e "# 3. Lock down SSH (key-only, disable root, change port)"
|
||||
echo
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
case "$SUBCOMMAND" in
|
||||
status)
|
||||
detect_os 2>/dev/null || true
|
||||
cmd_status
|
||||
;;
|
||||
base|nginx|hugo|tls|security|all)
|
||||
require_root
|
||||
detect_os
|
||||
case "$SUBCOMMAND" in
|
||||
base) cmd_base ;;
|
||||
nginx) cmd_nginx ;;
|
||||
hugo) cmd_hugo ;;
|
||||
tls) cmd_tls ;;
|
||||
security) cmd_security ;;
|
||||
all) cmd_all ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
die "Unknown subcommand: ${SUBCOMMAND}. Run with --help for usage."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user