#!/bin/bash ################################################################################ # Script Name: nginx-geoip-setup.sh # Version: 1.0 # Description: Automate nginx GeoIP2 enrichment for IP intelligence. # Installs GeoIP2 module, downloads GeoIP databases (MaxMind or # DB-IP free), creates nginx geoip2 variable mappings and an # enriched log format. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Prerequisites: # - nginx installed and running # - Root access # - MaxMind account OR use --db-ip for no-signup alternative # # Usage: # sudo ./nginx-geoip-setup.sh --db-ip # sudo ./nginx-geoip-setup.sh --account-id 123456 --license-key ABCDEF # sudo ./nginx-geoip-setup.sh --dry-run # sudo ./nginx-geoip-setup.sh --remove # sudo ./nginx-geoip-setup.sh --skip-packages # ################################################################################ set -euo pipefail # --- Configuration --- GEOIP_CONF="/etc/GeoIP.conf" GEOIP_DB_DIR="/usr/share/GeoIP" NGINX_GEOIP_CONF="/etc/nginx/conf.d/geoip2.conf" NGINX_LOG_CONF="/etc/nginx/enriched-log-format.conf" CRON_WEEKLY="/etc/cron.weekly/geoip-db-update" CRON_MONTHLY="/etc/cron.monthly/geoip-db-update" MARKER_START="# nginx-geoip-managed-start" MARKER_END="# nginx-geoip-managed-end" TIMESTAMP=$(date +%s) DRY_RUN=false REMOVE=false SKIP_PACKAGES=false USE_DBIP=false ACCOUNT_ID="${MAXMIND_ACCOUNT_ID:-}" LICENSE_KEY="${MAXMIND_LICENSE_KEY:-}" # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' info() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } step() { echo -e "${CYAN}[STEP]${NC} $*"; } usage() { cat <&2 exit 1 fi if ! command -v nginx &>/dev/null; then echo -e "${RED}Error: nginx not found${NC}" >&2 exit 1 fi # --- Distro detection --- detect_distro() { if [[ -f /etc/os-release ]]; then # shellcheck source=/dev/null . /etc/os-release case "$ID" in debian|ubuntu|linuxmint) echo "debian" ;; rhel|centos|rocky|alma|fedora|ol) echo "rhel" ;; *) echo "unknown" ;; esac else echo "unknown" fi } DISTRO=$(detect_distro) backup_file() { local file="$1" if [[ -f "$file" ]]; then cp "$file" "${file}.bak.${TIMESTAMP}" warn "Backed up: ${file} → ${file}.bak.${TIMESTAMP}" fi } # ===================================================== # REMOVE MODE # ===================================================== if [[ "$REMOVE" == "true" ]]; then step "Removing GeoIP2 configuration" for file in "$NGINX_GEOIP_CONF" "$NGINX_LOG_CONF" "$CRON_WEEKLY" "$CRON_MONTHLY"; do if [[ -f "$file" ]]; then if [[ "$DRY_RUN" == "true" ]]; then echo " Would remove: ${file}" else rm -f "$file" info "Removed: ${file}" fi else warn "Not found: ${file} (already removed?)" fi done if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: nginx -t" echo " Would run: systemctl reload nginx" else step "Testing nginx configuration" if nginx -t 2>&1; then info "nginx config valid" else echo -e "${RED}[ERROR] nginx config test failed${NC}" >&2 exit 1 fi step "Reloading nginx" systemctl reload nginx info "nginx reloaded" fi echo "" echo -e "${BOLD}GeoIP2 configuration removed.${NC}" echo " Packages and databases left in place." echo " To remove databases: rm -rf ${GEOIP_DB_DIR}/*.mmdb" exit 0 fi # ===================================================== # INSTALL MODE # ===================================================== # --- Prompt for credentials if not using DB-IP --- if [[ "$USE_DBIP" != "true" ]]; then if [[ -z "$ACCOUNT_ID" ]]; then read -rp "MaxMind Account ID (or Ctrl+C and rerun with --db-ip): " ACCOUNT_ID fi if [[ -z "$LICENSE_KEY" ]]; then read -rp "MaxMind License Key: " LICENSE_KEY fi if [[ -z "$ACCOUNT_ID" || -z "$LICENSE_KEY" ]]; then echo -e "${RED}Error: MaxMind credentials required (or use --db-ip for no-signup alternative)${NC}" >&2 exit 1 fi fi # ===================================================== # Step 1: Install packages # ===================================================== if [[ "$SKIP_PACKAGES" == "true" ]]; then warn "Skipping package installation (--skip-packages)" else step "Installing GeoIP2 packages" if [[ "$DISTRO" == "debian" ]]; then if [[ "$USE_DBIP" == "true" ]]; then PACKAGES="libnginx-mod-http-geoip2 mmdb-bin curl" else PACKAGES="libnginx-mod-http-geoip2 geoipupdate mmdb-bin" fi if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: apt-get update && apt-get install -y ${PACKAGES}" else apt-get update -qq # shellcheck disable=SC2086 apt-get install -y $PACKAGES info "Packages installed: ${PACKAGES}" fi elif [[ "$DISTRO" == "rhel" ]]; then if [[ "$USE_DBIP" == "true" ]]; then PACKAGES="nginx-mod-http-geoip2 mmdb-bin curl" else PACKAGES="nginx-mod-http-geoip2 geoipupdate mmdb-bin" fi if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: dnf install -y ${PACKAGES}" else # shellcheck disable=SC2086 dnf install -y $PACKAGES info "Packages installed: ${PACKAGES}" fi else echo -e "${RED}Error: Unsupported distro. Install packages manually and rerun with --skip-packages${NC}" >&2 exit 1 fi fi # ===================================================== # Step 2: Write GeoIP.conf (MaxMind only) # ===================================================== if [[ "$USE_DBIP" != "true" ]]; then step "Writing MaxMind configuration to ${GEOIP_CONF}" GEOIP_CONF_CONTENT="${MARKER_START} AccountID ${ACCOUNT_ID} LicenseKey ${LICENSE_KEY} EditionIDs GeoLite2-City GeoLite2-ASN GeoLite2-Country DatabaseDirectory ${GEOIP_DB_DIR} ${MARKER_END}" if [[ "$DRY_RUN" == "true" ]]; then echo " Would create: ${GEOIP_CONF}" else backup_file "$GEOIP_CONF" echo "$GEOIP_CONF_CONTENT" > "$GEOIP_CONF" chmod 600 "$GEOIP_CONF" info "Created: ${GEOIP_CONF}" fi fi # ===================================================== # Step 3: Download GeoIP databases # ===================================================== if [[ "$USE_DBIP" == "true" ]]; then step "Downloading DB-IP free databases to ${GEOIP_DB_DIR}" DBIP_MONTH=$(date +%Y-%m) if [[ "$DRY_RUN" == "true" ]]; then echo " Would download: dbip-country-lite-${DBIP_MONTH}.mmdb.gz" echo " Would download: dbip-asn-lite-${DBIP_MONTH}.mmdb.gz" else mkdir -p "$GEOIP_DB_DIR" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT curl -fsSL "https://download.db-ip.com/free/dbip-country-lite-${DBIP_MONTH}.mmdb.gz" -o "${TMPDIR}/country.mmdb.gz" curl -fsSL "https://download.db-ip.com/free/dbip-asn-lite-${DBIP_MONTH}.mmdb.gz" -o "${TMPDIR}/asn.mmdb.gz" gunzip -f "${TMPDIR}/country.mmdb.gz" gunzip -f "${TMPDIR}/asn.mmdb.gz" mv "${TMPDIR}/country.mmdb" "${GEOIP_DB_DIR}/GeoLite2-City.mmdb" mv "${TMPDIR}/asn.mmdb" "${GEOIP_DB_DIR}/GeoLite2-ASN.mmdb" info "Downloaded: DB-IP Country → GeoLite2-City.mmdb" info "Downloaded: DB-IP ASN → GeoLite2-ASN.mmdb" fi else step "Downloading MaxMind GeoIP2 databases to ${GEOIP_DB_DIR}" if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: mkdir -p ${GEOIP_DB_DIR}" echo " Would run: geoipupdate" else mkdir -p "$GEOIP_DB_DIR" geoipupdate -v info "GeoIP2 databases downloaded" fi fi # ===================================================== # Step 4: Create nginx geoip2 config # ===================================================== step "Creating nginx GeoIP2 config at ${NGINX_GEOIP_CONF}" NGINX_GEOIP_CONTENT="${MARKER_START} # GeoIP2 variable mappings — generated by nginx-geoip-setup.sh geoip2 ${GEOIP_DB_DIR}/GeoLite2-City.mmdb { auto_reload 60m; \$geoip2_country_code country iso_code; \$geoip2_country_name country names en; \$geoip2_city_name city names en; } geoip2 ${GEOIP_DB_DIR}/GeoLite2-ASN.mmdb { auto_reload 60m; \$geoip2_asn autonomous_system_number; \$geoip2_asn_org autonomous_system_organization; } ${MARKER_END}" if [[ "$DRY_RUN" == "true" ]]; then echo " Would create: ${NGINX_GEOIP_CONF}" else backup_file "$NGINX_GEOIP_CONF" echo "$NGINX_GEOIP_CONTENT" > "$NGINX_GEOIP_CONF" info "Created: ${NGINX_GEOIP_CONF}" fi # ===================================================== # Step 5: Create enriched log format # ===================================================== step "Creating enriched log format at ${NGINX_LOG_CONF}" NGINX_LOG_CONTENT="${MARKER_START} # Enriched log format with GeoIP2 data — generated by nginx-geoip-setup.sh log_format enriched '\$remote_addr - \$remote_user [\$time_local] ' '\"\$request\" \$status \$body_bytes_sent ' '\"\$http_referer\" \"\$http_user_agent\" ' '\$geoip2_country_code \"\$geoip2_asn_org\"'; ${MARKER_END}" if [[ "$DRY_RUN" == "true" ]]; then echo " Would create: ${NGINX_LOG_CONF}" else backup_file "$NGINX_LOG_CONF" echo "$NGINX_LOG_CONTENT" > "$NGINX_LOG_CONF" info "Created: ${NGINX_LOG_CONF}" fi # ===================================================== # Step 6: Patch nginx.conf to use enriched log format # ===================================================== NGINX_CONF="/etc/nginx/nginx.conf" if grep -q "enriched-log-format.conf" "$NGINX_CONF" 2>/dev/null; then warn "nginx.conf already includes enriched log format — skipping" elif grep -q "access_log.*enriched" "$NGINX_CONF" 2>/dev/null; then warn "nginx.conf already uses enriched format — skipping" else step "Patching ${NGINX_CONF} to use enriched log format" if [[ "$DRY_RUN" == "true" ]]; then echo " Would add: include ${NGINX_LOG_CONF}; before access_log" echo " Would change: access_log line to use enriched format" else backup_file "$NGINX_CONF" # Insert include before the access_log line, and append 'enriched' to it sed -i "/^[[:space:]]*access_log[[:space:]]/ { i\\\\tinclude ${NGINX_LOG_CONF}; s|access_log \(.*\);|access_log \1 enriched;| }" "$NGINX_CONF" # Only patch if access_log doesn't already have a format name (avoid double-appending) # Check if the sed worked if grep -q "enriched-log-format.conf" "$NGINX_CONF"; then info "Patched: ${NGINX_CONF}" else warn "Could not find access_log line in ${NGINX_CONF} — add manually:" warn " include ${NGINX_LOG_CONF};" warn " access_log /var/log/nginx/access.log enriched;" fi fi fi # ===================================================== # Step 7: Create cron job for database updates # ===================================================== if [[ "$USE_DBIP" == "true" ]]; then CRON_FILE="$CRON_MONTHLY" step "Creating monthly DB-IP update script at ${CRON_FILE}" CRON_CONTENT="#!/bin/sh # Monthly DB-IP database update — generated by nginx-geoip-setup.sh MONTH=\$(date +%Y-%m) TMPDIR=\$(mktemp -d) trap 'rm -rf \"\$TMPDIR\"' EXIT if curl -fsSL \"https://download.db-ip.com/free/dbip-country-lite-\${MONTH}.mmdb.gz\" -o \"\${TMPDIR}/country.mmdb.gz\" && \\ curl -fsSL \"https://download.db-ip.com/free/dbip-asn-lite-\${MONTH}.mmdb.gz\" -o \"\${TMPDIR}/asn.mmdb.gz\" && \\ gunzip \"\${TMPDIR}/country.mmdb.gz\" && \\ gunzip \"\${TMPDIR}/asn.mmdb.gz\"; then mv \"\${TMPDIR}/country.mmdb\" \"${GEOIP_DB_DIR}/GeoLite2-City.mmdb\" mv \"\${TMPDIR}/asn.mmdb\" \"${GEOIP_DB_DIR}/GeoLite2-ASN.mmdb\" logger -t dbip-update \"DB-IP databases updated\" else logger -t dbip-update -p user.err \"DB-IP download failed — kept existing databases\" fi" else CRON_FILE="$CRON_WEEKLY" step "Creating weekly geoipupdate script at ${CRON_FILE}" CRON_CONTENT="#!/bin/sh # Weekly GeoIP2 database update — generated by nginx-geoip-setup.sh /usr/bin/geoipupdate -s 2>&1 | logger -t geoipupdate" fi if [[ "$DRY_RUN" == "true" ]]; then echo " Would create: ${CRON_FILE}" else backup_file "$CRON_FILE" echo "$CRON_CONTENT" > "$CRON_FILE" chmod 755 "$CRON_FILE" info "Created: ${CRON_FILE}" fi # ===================================================== # Step 8: Validate nginx config # ===================================================== step "Testing nginx configuration" if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: nginx -t" else if nginx -t 2>&1; then info "nginx config valid" else echo -e "${RED}[ERROR] nginx config test failed${NC}" >&2 echo " Restore backups (.bak.${TIMESTAMP}) and check configuration" >&2 exit 1 fi fi # ===================================================== # Step 9: Reload nginx # ===================================================== step "Reloading nginx" if [[ "$DRY_RUN" == "true" ]]; then echo " Would run: systemctl reload nginx" else systemctl reload nginx info "nginx reloaded" fi # ===================================================== # Summary # ===================================================== echo "" echo -e "${BOLD}Done.${NC}" echo "" if [[ "$USE_DBIP" == "true" ]]; then echo " Provider: DB-IP Lite (no signup)" else echo " Provider: MaxMind GeoLite2" echo " GeoIP config: ${GEOIP_CONF}" fi echo " Database dir: ${GEOIP_DB_DIR}" echo " nginx geoip2: ${NGINX_GEOIP_CONF}" echo " Log format: ${NGINX_LOG_CONF}" echo " Cron job: ${CRON_FILE}" echo "" echo -e "${BOLD}Verify:${NC}" echo " tail -5 /var/log/nginx/access.log" echo " (should show country code and ASN org at end of each line)" echo "" echo " To remove: sudo $(basename "$0") --remove"