#!/bin/bash ################################################################################ # Script Name: add-http-auth.sh # Version: 3.0 # Description: Add HTTP Basic Auth to Prometheus stack reverse proxies # Supports both nginx and Apache — auto-detects which is in use. # Uses non-destructive include snippets to preserve existing # HTTPS/certbot configs. # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # # Supported Services: # - Prometheus (port 9090) # - Alertmanager (port 9093) # - Mimir (port 9009) — optionally protects /api/v1/push # - Loki (port 3100) — optionally protects /loki/api/v1/push # # Supported Web Servers: # - nginx — inserts 'include' snippets into location blocks # - Apache — inserts 'Include' snippets into blocks # # Usage: # sudo ./add-http-auth.sh # sudo ./add-http-auth.sh --remove # sudo ./add-http-auth.sh --status # ################################################################################ set -euo pipefail SCRIPT_VERSION="3.0" BACKUP_DIR="/var/backups/http-auth" # Detected at runtime WEB_SERVER="" # "nginx" or "apache" CONFIG_DIR="" # where vhost configs live SNIPPET_DIR="" # where auth snippets go AUTH_DIR="" # where htpasswd files go WEB_USER="" # www-data, nginx, apache, etc. SERVICE_NAME="" # systemd service name # Service definitions: name|nginx_config|apache_config|port SERVICES=( "prometheus|prometheus.conf|prometheus.conf|9090" "alertmanager|alerts.conf|alerts.conf|9093" "mimir|mimir.conf|mimir.conf|9009" "loki|loki.conf|loki.conf|3100" ) # ============================================================================ # HELPER FUNCTIONS # ============================================================================ show_usage() { cat <&2 exit 1 } warn() { echo "WARNING: $1" >&2 } # Get the config filename for the current web server get_config_file() { local entry="$1" local name nginx_conf apache_conf port IFS='|' read -r name nginx_conf apache_conf port <<< "$entry" if [ "$WEB_SERVER" = "nginx" ]; then echo "$nginx_conf" else echo "$apache_conf" fi } get_service_name() { local entry="$1" IFS='|' read -r name _ _ _ <<< "$entry" echo "$name" } get_service_port() { local entry="$1" IFS='|' read -r _ _ _ port <<< "$entry" echo "$port" } # ============================================================================ # WEB SERVER DETECTION # ============================================================================ detect_web_server() { local has_nginx=false local has_apache=false if command -v nginx &>/dev/null && systemctl is-active --quiet nginx 2>/dev/null; then has_nginx=true fi if command -v apache2ctl &>/dev/null && systemctl is-active --quiet apache2 2>/dev/null; then has_apache=true elif command -v httpd &>/dev/null && systemctl is-active --quiet httpd 2>/dev/null; then has_apache=true fi if [ "$has_nginx" = true ] && [ "$has_apache" = true ]; then echo "" echo "Both nginx and Apache detected. Which are you using for reverse proxies?" echo " 1) nginx" echo " 2) Apache" read -r -p "Select [1]: " choice case "${choice:-1}" in 2) WEB_SERVER="apache" ;; *) WEB_SERVER="nginx" ;; esac elif [ "$has_nginx" = true ]; then WEB_SERVER="nginx" elif [ "$has_apache" = true ]; then WEB_SERVER="apache" else die "Neither nginx nor Apache detected as running" fi echo " Detected web server: ${WEB_SERVER}" } # Set paths based on detected web server configure_paths() { if [ "$WEB_SERVER" = "nginx" ]; then if [ -d "/etc/nginx/sites-available" ]; then CONFIG_DIR="/etc/nginx/sites-available" elif [ -d "/etc/nginx/conf.d" ]; then CONFIG_DIR="/etc/nginx/conf.d" else die "nginx config directory not found" fi SNIPPET_DIR="/etc/nginx/snippets" AUTH_DIR="/etc/nginx/auth" SERVICE_NAME="nginx" if id "www-data" &>/dev/null; then WEB_USER="www-data" elif id "nginx" &>/dev/null; then WEB_USER="nginx" else WEB_USER="root" fi else # Apache if [ -d "/etc/apache2/sites-available" ]; then CONFIG_DIR="/etc/apache2/sites-available" SNIPPET_DIR="/etc/apache2/conf-available" SERVICE_NAME="apache2" elif [ -d "/etc/httpd/conf.d" ]; then CONFIG_DIR="/etc/httpd/conf.d" SNIPPET_DIR="/etc/httpd/conf.d" SERVICE_NAME="httpd" else die "Apache config directory not found" fi AUTH_DIR="/etc/httpd/auth" [ -d "/etc/apache2" ] && AUTH_DIR="/etc/apache2/auth" if id "www-data" &>/dev/null; then WEB_USER="www-data" elif id "apache" &>/dev/null; then WEB_USER="apache" else WEB_USER="root" fi fi } # ============================================================================ # HTTPS DETECTION # ============================================================================ has_https() { local config_file="$1" if [ "$WEB_SERVER" = "nginx" ]; then grep -qE 'listen\s+.*443\s+ssl' "$config_file" 2>/dev/null else grep -qE 'SSLEngine\s+on|/dev/null fi } # ============================================================================ # AUTH SNIPPET CHECK # ============================================================================ has_auth_snippet() { local config_file="$1" local service="$2" if [ "$WEB_SERVER" = "nginx" ]; then grep -qF "include ${SNIPPET_DIR}/auth-${service}.conf" "$config_file" 2>/dev/null else grep -qF "Include ${SNIPPET_DIR}/auth-${service}.conf" "$config_file" 2>/dev/null fi } # ============================================================================ # SETUP FUNCTIONS # ============================================================================ install_htpasswd() { if command -v htpasswd &>/dev/null; then return 0 fi echo "Installing htpasswd..." if command -v apt-get &>/dev/null; then apt-get -y install apache2-utils elif command -v dnf &>/dev/null; then dnf -y install httpd-tools elif command -v yum &>/dev/null; then yum -y install httpd-tools else die "Cannot install htpasswd — install apache2-utils or httpd-tools manually" fi } backup_config() { local config_file="$1" local timestamp timestamp=$(date +%F_%H%M%S) local backup_path="${BACKUP_DIR}/${timestamp}" mkdir -p "$backup_path" cp "$config_file" "$backup_path/" echo " Backed up to ${backup_path}/$(basename "$config_file")" } # ============================================================================ # NGINX-SPECIFIC FUNCTIONS # ============================================================================ nginx_create_snippet() { local service="$1" local display_name="$2" cat > "${SNIPPET_DIR}/auth-${service}.conf" < "$temp_file" mv "$temp_file" "$config_file" echo " Inserted auth include into $(basename "$config_file")" } nginx_insert_push_auth() { local config_file="$1" local service="$2" if grep -q "location.*/api/v1/push" "$config_file" && \ ! grep -A2 "location.*/api/v1/push" "$config_file" | grep -qF "auth-${service}.conf"; then local temp_file temp_file=$(mktemp) local include_line=" include ${SNIPPET_DIR}/auth-${service}.conf;" awk -v inc="$include_line" ' /location.*\/api\/v1\/push/ && !push_done { print print inc push_done = 1 next } { print } ' "$config_file" > "$temp_file" mv "$temp_file" "$config_file" echo " Protected push endpoint with auth" fi } nginx_remove_auth() { local config_file="$1" local service="$2" local temp_file temp_file=$(mktemp) grep -vF "include ${SNIPPET_DIR}/auth-${service}.conf" "$config_file" > "$temp_file" mv "$temp_file" "$config_file" } nginx_test_config() { nginx -t 2>&1 } # ============================================================================ # APACHE-SPECIFIC FUNCTIONS # ============================================================================ apache_create_snippet() { local service="$1" local display_name="$2" cat > "${SNIPPET_DIR}/auth-${service}.conf" < or local temp_file temp_file=$(mktemp) if grep -qE '' "$config_file"; then # Insert after opening tag awk -v inc="$include_line" ' // && !done { print print inc done = 1 next } { print } ' "$config_file" > "$temp_file" elif grep -qE ' before the first ProxyPass awk -v inc="$include_line" -v sdir="${SNIPPET_DIR}" -v svc="$service" ' /ProxyPass\s/ && !done { # Add a Location block with auth before ProxyPass print " " print inc print " " print "" done = 1 } { print } ' "$config_file" > "$temp_file" else # No Location or Proxy block found — add a Location block before awk -v inc="$include_line" ' /<\/VirtualHost>/ && !done { print "" print " " print inc print " " print "" done = 1 } { print } ' "$config_file" > "$temp_file" fi mv "$temp_file" "$config_file" echo " Inserted auth into $(basename "$config_file")" } apache_insert_push_auth() { local config_file="$1" local service="$2" local push_path="" if [ "$service" = "mimir" ]; then push_path="/api/v1/push" elif [ "$service" = "loki" ]; then push_path="/loki/api/v1/push" else return 0 fi # Check if there's already a Location block for the push path if grep -qF "$push_path" "$config_file" && \ ! grep -A3 "$push_path" "$config_file" | grep -qF "auth-${service}.conf"; then backup_config "$config_file" local temp_file temp_file=$(mktemp) local include_line=" Include ${SNIPPET_DIR}/auth-${service}.conf" awk -v path="$push_path" -v inc="$include_line" ' $0 ~ path && /Location/ && !push_done { print print inc push_done = 1 next } { print } ' "$config_file" > "$temp_file" mv "$temp_file" "$config_file" echo " Protected push endpoint with auth" fi } apache_remove_auth() { local config_file="$1" local service="$2" local temp_file temp_file=$(mktemp) grep -vF "Include ${SNIPPET_DIR}/auth-${service}.conf" "$config_file" > "$temp_file" mv "$temp_file" "$config_file" } apache_test_config() { if command -v apache2ctl &>/dev/null; then apache2ctl configtest 2>&1 else httpd -t 2>&1 fi } # ============================================================================ # GENERIC WRAPPERS (dispatch to nginx or apache functions) # ============================================================================ create_snippet() { if [ "$WEB_SERVER" = "nginx" ]; then nginx_create_snippet "$@" else apache_create_snippet "$@" fi } insert_auth() { if [ "$WEB_SERVER" = "nginx" ]; then nginx_insert_auth "$@" else apache_insert_auth "$@" fi } insert_push_auth() { if [ "$WEB_SERVER" = "nginx" ]; then nginx_insert_push_auth "$@" else apache_insert_push_auth "$@" fi } remove_auth_from_config() { if [ "$WEB_SERVER" = "nginx" ]; then nginx_remove_auth "$@" else apache_remove_auth "$@" fi } test_config() { if [ "$WEB_SERVER" = "nginx" ]; then nginx_test_config else apache_test_config fi } # ============================================================================ # STATUS & REMOVE # ============================================================================ show_status() { detect_web_server configure_paths echo "" echo "==========================================" echo "HTTP Basic Auth Status (${WEB_SERVER})" echo "==========================================" echo "" for entry in "${SERVICES[@]}"; do local name config_file name=$(get_service_name "$entry") config_file=$(get_config_file "$entry") local display_name display_name="${name^}" local full_path="${CONFIG_DIR}/${config_file}" printf " %-14s " "${display_name}:" if [ ! -f "$full_path" ]; then echo "no config found" continue fi if has_auth_snippet "$full_path" "$name"; then if [ -f "${AUTH_DIR}/.htpasswd-${name}" ]; then echo "ENABLED (htpasswd + snippet)" else echo "BROKEN (snippet exists but htpasswd file missing)" fi else echo "not configured" fi done echo "" echo "Web server: ${WEB_SERVER}" echo "Config dir: ${CONFIG_DIR}" echo "Snippet dir: ${SNIPPET_DIR}" echo "Auth dir: ${AUTH_DIR}" echo "Backup dir: ${BACKUP_DIR}" echo "" } do_remove() { detect_web_server configure_paths echo "" echo "Removing HTTP Basic Auth from all services (${WEB_SERVER})..." echo "" for entry in "${SERVICES[@]}"; do local name config_file name=$(get_service_name "$entry") config_file=$(get_config_file "$entry") local full_path="${CONFIG_DIR}/${config_file}" if [ ! -f "$full_path" ]; then continue fi if has_auth_snippet "$full_path" "$name"; then backup_config "$full_path" remove_auth_from_config "$full_path" "$name" echo " Removed auth from ${config_file}" fi rm -f "${SNIPPET_DIR}/auth-${name}.conf" done echo "" echo "Testing ${WEB_SERVER} configuration..." if test_config; then systemctl reload "$SERVICE_NAME" echo "" echo "Auth removed and ${WEB_SERVER} reloaded." else warn "${WEB_SERVER} config test failed — check your config manually" fi } # ============================================================================ # MAIN SETUP # ============================================================================ setup_auth() { detect_web_server configure_paths echo "" echo "==========================================" echo "Add HTTP Basic Auth to Prometheus Stack" echo "Version: ${SCRIPT_VERSION} (${WEB_SERVER})" echo "==========================================" # Check for HTTPS local has_any_https=false for entry in "${SERVICES[@]}"; do local name config_file name=$(get_service_name "$entry") config_file=$(get_config_file "$entry") local full_path="${CONFIG_DIR}/${config_file}" if [ -f "$full_path" ] && has_https "$full_path"; then has_any_https=true break fi done if [ "$has_any_https" = false ]; then echo "" warn "No HTTPS configuration detected!" echo " Basic Auth over HTTP sends credentials in cleartext." echo " Strongly recommended: run certbot first to enable HTTPS." echo "" read -r -p "Continue without HTTPS? [y/N]: " confirm if [[ ! "$confirm" =~ ^[Yy]$ ]]; then echo "Aborted. Run certbot first, then re-run this script." exit 0 fi fi # Detect which services have configs echo "" echo "Detected services:" local found_any=false for entry in "${SERVICES[@]}"; do local name config_file name=$(get_service_name "$entry") config_file=$(get_config_file "$entry") local full_path="${CONFIG_DIR}/${config_file}" if [ -f "$full_path" ]; then local https_status="HTTP" has_https "$full_path" && https_status="HTTPS" echo " ✓ ${name} (${config_file}) [${https_status}]" found_any=true fi done if [ "$found_any" = false ]; then die "No service configs found in ${CONFIG_DIR}. Set up ${WEB_SERVER} reverse proxies first." fi echo "" # Ask about push endpoint protection local protect_push=false echo "Mimir and Loki have push endpoints used by remote agents." echo "Protecting them requires configuring credentials in Prometheus/Alloy." read -r -p "Protect push endpoints with auth too? [y/N]: " push_confirm if [[ "$push_confirm" =~ ^[Yy]$ ]]; then protect_push=true fi # Ask about shared vs per-service credentials local shared_creds=false local shared_htpasswd="" echo "" echo "Credential mode:" echo " 1) Same username/password for all services" echo " 2) Different credentials per service" read -r -p "Select [1]: " cred_mode if [[ "${cred_mode:-1}" != "2" ]]; then shared_creds=true read -r -p "Username for all services [admin]: " shared_user shared_user=${shared_user:-admin} # Create a temporary shared htpasswd file — will be copied per service shared_htpasswd=$(mktemp) htpasswd -c "$shared_htpasswd" "$shared_user" fi # Create directories mkdir -p "$AUTH_DIR" "$SNIPPET_DIR" "$BACKUP_DIR" echo "" # Set up auth for each detected service for entry in "${SERVICES[@]}"; do local name config_file port name=$(get_service_name "$entry") config_file=$(get_config_file "$entry") port=$(get_service_port "$entry") local full_path="${CONFIG_DIR}/${config_file}" if [ ! -f "$full_path" ]; then continue fi local display_name display_name="${name^}" echo "--- ${display_name} ---" # Create htpasswd file if [ "$shared_creds" = true ]; then if [ -f "${AUTH_DIR}/.htpasswd-${name}" ]; then read -r -p " htpasswd file exists. Overwrite with shared credentials? [Y/n]: " overwrite if [[ "$overwrite" =~ ^[Nn]$ ]]; then echo " Keeping existing htpasswd" else cp "$shared_htpasswd" "${AUTH_DIR}/.htpasswd-${name}" echo " Using shared credentials" fi else cp "$shared_htpasswd" "${AUTH_DIR}/.htpasswd-${name}" echo " Using shared credentials" fi else if [ -f "${AUTH_DIR}/.htpasswd-${name}" ]; then read -r -p " htpasswd file exists. Recreate? [y/N]: " recreate if [[ ! "$recreate" =~ ^[Yy]$ ]]; then echo " Keeping existing htpasswd" else read -r -p " Username [admin]: " username username=${username:-admin} htpasswd -c "${AUTH_DIR}/.htpasswd-${name}" "$username" fi else read -r -p " Username [admin]: " username username=${username:-admin} htpasswd -c "${AUTH_DIR}/.htpasswd-${name}" "$username" fi fi # Create auth snippet create_snippet "$name" "$display_name" # Insert into main location/proxy block insert_auth "$full_path" "$name" # Handle push endpoints for Mimir and Loki if [[ "$name" == "mimir" ]] || [[ "$name" == "loki" ]]; then if [ "$protect_push" = true ]; then insert_push_auth "$full_path" "$name" else echo " ⚠ Push endpoint left open — consider IP restrictions" fi fi echo "" done # Clean up shared temp file [ -n "$shared_htpasswd" ] && rm -f "$shared_htpasswd" # Set permissions on htpasswd files chmod 640 "${AUTH_DIR}"/.htpasswd-* 2>/dev/null || true chown "root:${WEB_USER}" "${AUTH_DIR}"/.htpasswd-* 2>/dev/null || true # Test and reload echo "Testing ${WEB_SERVER} configuration..." if test_config; then systemctl reload "$SERVICE_NAME" echo "" echo "==========================================" echo "HTTP Basic Auth Successfully Configured!" echo "==========================================" echo "" echo "Web server: ${WEB_SERVER}" echo "Backups: ${BACKUP_DIR}" echo "" echo "To remove auth later: $0 --remove" echo "To check status: $0 --status" else echo "" echo "${WEB_SERVER} configuration test FAILED!" echo "Your backups are in ${BACKUP_DIR} — restore manually if needed." exit 1 fi } # ============================================================================ # MAIN # ============================================================================ main() { if [[ $EUID -ne 0 ]]; then die "This script must be run as root" fi case "${1:-}" in -h|--help) show_usage ;; --remove) do_remove ;; --status) show_status ;; *) install_htpasswd setup_auth ;; esac } main "$@"