#!/usr/bin/env bash set -euo pipefail # ============================================================================= # NGINX CIS + STIG Hardening Script (Linux) # - Idempotent # - Applies systemd, nginx, TLS, logging, firewall, SELinux/AppArmor # ============================================================================= DOMAIN="" WEBROOT="" OWNER="deploy" EMAIL="" ENABLE_HSTS=1 DRY_RUN=0 # --------- helpers ------------------------------------------------------------ msg() { printf "%s\n" "$*" ; } die() { printf "ERROR: %s\n" "$*" >&2 ; exit 1 ; } bak() { # backup file if exists, once local f="$1" [ -f "$f" ] || return 0 if [ ! -f "${f}.bak" ]; then $DRY_RUN || cp -a "$f" "${f}.bak" msg "Backup: $f -> ${f}.bak" fi } run() { if [ "$DRY_RUN" -eq 1 ]; then printf "[DRY-RUN] %s\n" "$*" else eval "$@" fi } has_cmd() { command -v "$1" >/dev/null 2>&1 ; } # --------- args --------------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in --domain) shift; DOMAIN="${1:-}";; --webroot) shift; WEBROOT="${1:-}";; --owner) shift; OWNER="${1:-}";; --email) shift; EMAIL="${1:-}";; --no-hsts) ENABLE_HSTS=0;; --dry-run) DRY_RUN=1;; -h|--help) cat </dev/null 2>&1; then run "useradd --system --no-create-home --shell /usr/sbin/nologin nginx" fi # create owner user if missing (non-fatal) if ! id "$OWNER" >/dev/null 2>&1; then msg "Owner user $OWNER does not exist; creating system user" run "useradd --system --create-home --shell /usr/sbin/nologin $OWNER" fi # create webroot run "mkdir -p $WEBROOT" run "chown -R $OWNER:nginx $WEBROOT" run "chmod -R 0750 $WEBROOT" # --------- filesystem perms --------------------------------------------------- run "chown -R root:root /etc/nginx" run "chmod 0750 /etc/nginx" for f in /etc/nginx/nginx.conf /etc/nginx/conf.d; do [ -e "$f" ] && run "chmod 0640 $f" || true done run "mkdir -p /var/log/nginx /var/cache/nginx" run "chown -R root:nginx /var/log/nginx /var/cache/nginx" run "chmod 0750 /var/log/nginx /var/cache/nginx" # --------- systemd hardening -------------------------------------------------- run "mkdir -p /etc/systemd/system/nginx.service.d" HARDEN_FILE="/etc/systemd/system/nginx.service.d/hardening.conf" bak "$HARDEN_FILE" cat >"$HARDEN_FILE".new <<'EOF' [Service] User=nginx Group=nginx AmbientCapabilities= CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true ProtectClock=true MemoryDenyWriteExecute=true LockPersonality=true RestrictSUIDSGID=true RestrictNamespaces=true RestrictRealtime=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX SystemCallFilter=@system-service @file-system @network-io ReadWritePaths=/var/log/nginx /var/cache/nginx ReadOnlyPaths=/etc/nginx /var/www RuntimeDirectory=nginx RuntimeDirectoryMode=0750 UMask=0027 EOF run "mv -f $HARDEN_FILE.new $HARDEN_FILE" run "systemctl daemon-reload" # --------- nginx main config -------------------------------------------------- bak "/etc/nginx/nginx.conf" cat >"/etc/nginx/nginx.conf.new" <<'EOF' user nginx; worker_processes auto; events { worker_connections 1024; } http { server_tokens off; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent" ' 'rt=$request_time ua="$upstream_addr" urt="$upstream_response_time" ' 'rid="$request_id"'; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; send_timeout 10s; client_body_timeout 10s; client_header_timeout 10s; keepalive_timeout 30s; keepalive_requests 100; client_max_body_size 10m; large_client_header_buffers 2 8k; types_hash_max_size 4096; include /etc/nginx/mime.types; default_type application/octet-stream; gzip off; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m; include /etc/nginx/conf.d/*.conf; } EOF run "mv -f /etc/nginx/nginx.conf.new /etc/nginx/nginx.conf" # --------- site config -------------------------------------------------------- SITE_CONF="/etc/nginx/conf.d/site.conf" bak "$SITE_CONF" HSTS_LINE="" if [ "$ENABLE_HSTS" -eq 1 ]; then HSTS_LINE=' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;' fi # join domains into server_name SERVER_NAME="$DOMAIN" cat >"$SITE_CONF.new" <"$LOGROTATE.new" <<'EOF' /var/log/nginx/*.log { weekly rotate 8 missingok compress delaycompress notifempty create 0640 root adm sharedscripts postrotate /bin/systemctl kill -s USR1 nginx || true endscript } EOF run "mv -f $LOGROTATE.new $LOGROTATE" # --------- firewall open 80/443 ---------------------------------------------- if has_cmd ufw; then run "ufw allow 80/tcp || true" run "ufw allow 443/tcp || true" elif has_cmd firewall-cmd; then run "firewall-cmd --add-service=http --permanent || true" run "firewall-cmd --add-service=https --permanent || true" run "firewall-cmd --reload || true" else msg "No ufw or firewalld detected; ensure ports 80/443 are allowed." fi # --------- SELinux / AppArmor ------------------------------------------------- if command -v getenforce >/dev/null 2>&1; then # SELinux present ENF=$(getenforce || true) msg "SELinux detected (state: $ENF)" # Allow network connect if reverse proxying (safe default) if has_cmd setsebool; then run "setsebool -P httpd_can_network_connect on || true" fi # Ensure contexts for webroot and logs if has_cmd semanage; then run "semanage fcontext -a -t httpd_sys_content_t '${WEBROOT}(/.*)?' || true" run "restorecon -R ${WEBROOT} || true" run "semanage fcontext -a -t httpd_log_t '/var/log/nginx(/.*)?' || true" run "restorecon -R /var/log/nginx || true" fi elif [ -d /etc/apparmor.d ]; then msg "AppArmor present; ensure profile usr.sbin.nginx is enforcing." fi # --------- validate and reload ----------------------------------------------- if nginx -t; then run "systemctl enable nginx" run "systemctl restart nginx" else die "nginx -t failed; refusing to restart." fi # --------- verify report ------------------------------------------------------ set +e echo "=== VERIFY REPORT ===" echo "- Listening sockets:" ss -ltn | grep -E '(:80|:443)' || true echo "- TLS probe:" echo | openssl s_client -connect "$(echo "$DOMAIN" | awk '{print $1}'):443" -servername "$(echo "$DOMAIN" | awk '{print $1}')" 2>/dev/null | openssl x509 -noout -text | sed -n '1,15p' || echo "Certificate check skipped (self-signed or missing cert)." echo "- Headers check (one domain):" curl -ksI "https://$(echo "$DOMAIN" | awk '{print $1}')" | sed -n '1,20p' || true echo "=====================" set -e msg "Hardening completed."