#!/usr/bin/env bash ######################################################################################### #### add-nginx-block-head.sh — Block HEAD requests in Nginx (HestiaCP compatible) #### #### Adds a 444 drop rule for HEAD method crawlers/scrapers. #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.01 #### #### #### #### Usage: #### #### sudo ./add-nginx-block-head.sh #### #### sudo ./add-nginx-block-head.sh --dry-run #### #### sudo ./add-nginx-block-head.sh --remove #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── DRY_RUN=false REMOVE=false SNIPPET_NAME="nginx.conf_block_head" # ── Colors ──────────────────────────────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BOLD='\033[1m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" BOLD="" RESET="" fi log() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } info() { echo -e "${BOLD}[INFO]${RESET} $*"; } # ── Usage ───────────────────────────────────────────────────────────── usage() { cat </dev/null; then err "Nginx not found" exit 1 fi if ! command -v v-list-users &>/dev/null; then err "HestiaCP not found (v-list-users missing)" exit 1 fi # ── Snippet content ────────────────────────────────────────────────── SNIPPET_CONTENT='# Block HEAD request crawlers/scrapers # Added by add-nginx-block-head.sh # Returns 444 (drop connection) — no response sent to bot if ($request_method = HEAD) { return 444; }' # ── Find all HestiaCP domains ──────────────────────────────────────── get_all_domain_dirs() { local users users=$(v-list-users plain 2>/dev/null | cut -f1) for user in $users; do local user_conf="/home/${user}/conf/web" [[ -d "$user_conf" ]] || continue # Find domain directories by looking for nginx.conf files for nginx_conf in "${user_conf}"/*/nginx.conf; do [[ -f "$nginx_conf" ]] || continue dirname "$nginx_conf" done done } # ── Remove mode ─────────────────────────────────────────────────────── if [[ "$REMOVE" == "true" ]]; then removed=0 while IFS= read -r domain_dir; do snippet="${domain_dir}/${SNIPPET_NAME}" ssl_snippet="${domain_dir}/nginx.ssl.conf_block_head" for f in "$snippet" "$ssl_snippet"; do if [[ -f "$f" ]]; then if [[ "$DRY_RUN" == "true" ]]; then info "Would remove: ${f}" else rm -f "$f" log "Removed ${f}" fi ((removed++)) || true fi done done < <(get_all_domain_dirs) if [[ $removed -eq 0 ]]; then info "No block-head snippets found — nothing to remove" exit 0 fi if [[ "$DRY_RUN" == "true" ]]; then info "Would test and reload Nginx" exit 0 fi if nginx -t 2>/dev/null; then systemctl reload nginx log "Nginx reloaded — HEAD requests are now allowed" else err "Nginx config test failed after removal — check your config" exit 1 fi exit 0 fi # ── Install mode ────────────────────────────────────────────────────── domain_dirs=() while IFS= read -r dir; do domain_dirs+=("$dir") done < <(get_all_domain_dirs) if [[ ${#domain_dirs[@]} -eq 0 ]]; then err "No HestiaCP web domains found" exit 1 fi info "Found ${#domain_dirs[@]} domain config(s)" echo "" created=0 skipped=0 created_files=() for domain_dir in "${domain_dirs[@]}"; do domain_name=$(basename "$domain_dir") # Add snippet for both HTTP and HTTPS server blocks for conf_type in "" ".ssl"; do if [[ -n "$conf_type" ]]; then snippet="${domain_dir}/nginx${conf_type}.conf_block_head" else snippet="${domain_dir}/${SNIPPET_NAME}" fi # Check the main config exists for this type main_conf="${domain_dir}/nginx${conf_type}.conf" [[ -f "$main_conf" ]] || continue if [[ -f "$snippet" ]]; then info "Already exists: ${snippet}" ((skipped++)) || true continue fi if [[ "$DRY_RUN" == "true" ]]; then info "Would create: ${snippet}" ((created++)) || true else echo "$SNIPPET_CONTENT" > "$snippet" created_files+=("$snippet") log "Created ${snippet}" ((created++)) || true fi done done echo "" if [[ $created -eq 0 && $skipped -gt 0 ]]; then info "HEAD requests are already blocked on all domains" exit 0 fi if [[ "$DRY_RUN" == "true" ]]; then echo "" echo "$SNIPPET_CONTENT" echo "" info "Would create ${created} snippet(s) (${skipped} already exist)" info "Would test Nginx config and reload" exit 0 fi # Test Nginx config info "Testing Nginx configuration..." if nginx -t 2>&1; then echo "" log "Config test passed" systemctl reload nginx log "Nginx reloaded — HEAD requests blocked on ${#domain_dirs[@]} domain(s) (444 drop)" else echo "" err "Config test FAILED — rolling back all changes" for f in "${created_files[@]}"; do rm -f "$f" err "Removed ${f}" done err "Nginx was NOT reloaded — your site is unaffected" exit 1 fi echo "" info "Verify with: curl -I https://your-site.com" info "Expected: curl returns empty reply (connection dropped)" info "To undo: $(basename "$0") --remove"