generated from nhcarrigan/template
refactor: restructure nginx config into per-app files (#1)
## Summary - Added `push.sh` script to deploy configs to prod via `sudo rsync` (with `--delete` for exact mirroring) - Split the monolithic `conf.d/server.conf` (1,682 lines, 96 server blocks) into 28 per-app files under `sites-available/`, with corresponding symlinks in `sites-enabled/` - Extracted custom `nginx.conf` settings (`log_format` directives, `server_names_hash_bucket_size`) into dedicated `conf.d/logging.conf` and `conf.d/tuning.conf` files, leaving `nginx.conf` as close to stock as possible ## Test plan - [x] `sudo nginx -t` passes on prod after the sites-available restructure ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #1 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,24 +1,260 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONF="configs/prod.conf"
|
||||
# Nginx configuration static analysis test suite.
|
||||
# Usage: bash test.sh [nginx-dir]
|
||||
# Defaults to nginx/nginx relative to the repo root.
|
||||
|
||||
# Extract server_name values in order, ignoring commented lines
|
||||
mapfile -t domains < <(grep -oP '^\s*server_name\s+\K[^;]+' "$CONF")
|
||||
NGINX_DIR="${1:-nginx/nginx}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
sorted=($(printf "%s\n" "${domains[@]}" | sort))
|
||||
pass() {
|
||||
printf " PASS: %s\n" "$1"
|
||||
((PASS++))
|
||||
}
|
||||
|
||||
# Print the sorted list for debugging
|
||||
echo "Auditing servers:"
|
||||
printf "%s " "${sorted[@]}"
|
||||
fail() {
|
||||
printf " FAIL: %s\n" "$1"
|
||||
((FAIL++))
|
||||
}
|
||||
|
||||
echo "=== nginx config static analysis ==="
|
||||
echo "Directory: $NGINX_DIR"
|
||||
echo ""
|
||||
|
||||
for i in "${!domains[@]}"; do
|
||||
if [[ "${domains[$i]}" != "${sorted[$i]}" ]]; then
|
||||
echo "Domain list is not sorted alphabetically."
|
||||
echo "First out-of-order entry: '${domains[$i]}' (should be '${sorted[$i]}')"
|
||||
exit 1
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 1. No deprecated TLS versions
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- TLS version check ---"
|
||||
deprecated=$(grep -rnP 'TLSv1(?!\.[23])' "$NGINX_DIR" --include="*.conf" 2>/dev/null || true)
|
||||
if [ -n "$deprecated" ]; then
|
||||
fail "Deprecated TLS versions (TLSv1 or TLSv1.1) found:"
|
||||
printf '%s\n' "$deprecated" | sed 's/^/ /'
|
||||
else
|
||||
pass "No deprecated TLS versions"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 2. No duplicate literal server_name values across all site configs
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- Duplicate server_name check ---"
|
||||
duplicates=$(grep -rh --include="*.conf" 'server_name' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '^\s*#' \
|
||||
| sed 's/.*server_name\s*//' \
|
||||
| sed 's/\s*;//' \
|
||||
| tr ' ' '\n' \
|
||||
| grep -vP '^\s*$|^_$|^~|^\*\.' \
|
||||
| sort | uniq -d)
|
||||
if [ -n "$duplicates" ]; then
|
||||
fail "Duplicate server_name values:"
|
||||
printf '%s\n' "$duplicates" | sed 's/^/ /'
|
||||
else
|
||||
pass "No duplicate server_name values"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 3. Every sites-available/*.conf has a sites-enabled symlink
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- sites-enabled coverage check ---"
|
||||
missing_links=0
|
||||
for conf in "$NGINX_DIR/sites-available/"*.conf; do
|
||||
name=$(basename "$conf")
|
||||
if [ ! -L "$NGINX_DIR/sites-enabled/$name" ]; then
|
||||
fail "No sites-enabled symlink for: $name"
|
||||
missing_links=1
|
||||
fi
|
||||
done
|
||||
[ "$missing_links" -eq 0 ] && pass "All sites-available configs have sites-enabled symlinks"
|
||||
echo ""
|
||||
|
||||
echo "All server_name entries are sorted alphabetically."
|
||||
exit 0
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 4. No broken symlinks in sites-enabled
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- Broken symlink check ---"
|
||||
broken=0
|
||||
for link in "$NGINX_DIR/sites-enabled/"*; do
|
||||
[ -L "$link" ] || continue
|
||||
if [ ! -e "$link" ]; then
|
||||
fail "Broken symlink: $(basename "$link")"
|
||||
broken=1
|
||||
fi
|
||||
done
|
||||
[ "$broken" -eq 0 ] && pass "No broken symlinks in sites-enabled"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 5. No orphaned sites-enabled symlinks (no matching sites-available file)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- Orphaned symlink check ---"
|
||||
orphaned=0
|
||||
for link in "$NGINX_DIR/sites-enabled/"*.conf; do
|
||||
[ -L "$link" ] || continue
|
||||
name=$(basename "$link")
|
||||
if [ ! -f "$NGINX_DIR/sites-available/$name" ]; then
|
||||
fail "Orphaned sites-enabled symlink: $name"
|
||||
orphaned=1
|
||||
fi
|
||||
done
|
||||
[ "$orphaned" -eq 0 ] && pass "No orphaned sites-enabled symlinks"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 6. No port-80 listeners in any custom server block
|
||||
# (port 80 is blocked at the firewall; all traffic is HTTPS only)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- Port 80 listener check ---"
|
||||
http_blocks=$(grep -rnP 'listen\s.*\b80\b' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v 'sites-available/default' \
|
||||
| grep -v '^\s*#' || true)
|
||||
if [ -n "$http_blocks" ]; then
|
||||
fail "Port 80 listeners found in custom site configs:"
|
||||
printf '%s\n' "$http_blocks" | sed 's/^/ /'
|
||||
else
|
||||
pass "No port 80 listeners in custom server blocks"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 7. ssl_certificate and ssl_certificate_key counts match per file
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- SSL certificate directive pairing check ---"
|
||||
ssl_errors=0
|
||||
for conf in "$NGINX_DIR/sites-available/"*.conf; do
|
||||
certs=$(grep -cP 'ssl_certificate\b(?!_key)' "$conf" 2>/dev/null || echo 0)
|
||||
keys=$(grep -c 'ssl_certificate_key' "$conf" 2>/dev/null || echo 0)
|
||||
if [ "$certs" != "$keys" ]; then
|
||||
fail "$(basename "$conf"): $certs ssl_certificate vs $keys ssl_certificate_key (must match)"
|
||||
ssl_errors=1
|
||||
fi
|
||||
done
|
||||
[ "$ssl_errors" -eq 0 ] && pass "All ssl_certificate directives are correctly paired"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 8. All plain-HTTP proxy_pass targets are local
|
||||
# (https:// proxy_pass is permitted for intentional external proxying,
|
||||
# e.g. CDN reverse-proxying to object storage over TLS)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- proxy_pass locality check ---"
|
||||
external=$(grep -rn 'proxy_pass\s\+http://' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '#' \
|
||||
| grep -vP 'proxy_pass\s+http://(127\.0\.0\.1|localhost|0\.0\.0\.0)' || true)
|
||||
if [ -n "$external" ]; then
|
||||
fail "Plain-HTTP proxy_pass to non-local target found:"
|
||||
printf '%s\n' "$external" | sed 's/^/ /'
|
||||
else
|
||||
pass "All plain-HTTP proxy_pass targets are local"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 9. All SSL cert paths use /etc/letsencrypt/live/
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- SSL certificate path convention check ---"
|
||||
nonstandard_certs=$(grep -rn 'ssl_certificate' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '#' \
|
||||
| grep -vP '/etc/letsencrypt/live/' || true)
|
||||
if [ -n "$nonstandard_certs" ]; then
|
||||
fail "SSL certs not under /etc/letsencrypt/live/:"
|
||||
printf '%s\n' "$nonstandard_certs" | sed 's/^/ /'
|
||||
else
|
||||
pass "All SSL certificate paths use /etc/letsencrypt/live/"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 10. ssl_certificate uses fullchain.pem, ssl_certificate_key uses privkey.pem
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- SSL certificate filename convention check ---"
|
||||
cert_name_errors=0
|
||||
wrong_certs=$(grep -rnP 'ssl_certificate\b(?!_key)' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '#' \
|
||||
| grep -v 'fullchain\.pem' || true)
|
||||
wrong_keys=$(grep -rn 'ssl_certificate_key' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '#' \
|
||||
| grep -v 'privkey\.pem' || true)
|
||||
if [ -n "$wrong_certs" ]; then
|
||||
fail "ssl_certificate not using fullchain.pem:"
|
||||
printf '%s\n' "$wrong_certs" | sed 's/^/ /'
|
||||
cert_name_errors=1
|
||||
fi
|
||||
if [ -n "$wrong_keys" ]; then
|
||||
fail "ssl_certificate_key not using privkey.pem:"
|
||||
printf '%s\n' "$wrong_keys" | sed 's/^/ /'
|
||||
cert_name_errors=1
|
||||
fi
|
||||
[ "$cert_name_errors" -eq 0 ] && pass "All SSL certs use fullchain.pem / privkey.pem"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 11. No server_name directives use raw IP addresses
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- IP address as server_name check ---"
|
||||
ip_names=$(grep -rh --include="*.conf" 'server_name' "$NGINX_DIR/sites-available/" \
|
||||
| grep -v '#' \
|
||||
| sed 's/.*server_name\s*//' \
|
||||
| sed 's/\s*;//' \
|
||||
| tr ' ' '\n' \
|
||||
| grep -P '^\d{1,3}(\.\d{1,3}){3}$' || true)
|
||||
if [ -n "$ip_names" ]; then
|
||||
fail "server_name uses raw IP addresses:"
|
||||
printf '%s\n' "$ip_names" | sed 's/^/ /'
|
||||
else
|
||||
pass "No server_name directives use raw IP addresses"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 12. No conf.d files conflict with built-in nginx conf.d conventions
|
||||
# (i.e. no stray default.conf or catch-all templates left over)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- conf.d stray file check ---"
|
||||
stray=$(find "$NGINX_DIR/conf.d" -name "*.conf" \
|
||||
| grep -vP '/(logging|tuning|cloudflare_ips)\.conf$' || true)
|
||||
if [ -n "$stray" ]; then
|
||||
fail "Unexpected files in conf.d (only logging.conf, tuning.conf, cloudflare_ips.conf expected):"
|
||||
printf '%s\n' "$stray" | sed 's/^/ /'
|
||||
else
|
||||
pass "conf.d contains only expected files"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 13. Server blocks within each sites-available file are sorted
|
||||
# alphabetically by server_name (LC_ALL=C; regex/wildcard excluded)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "--- Alphabetical server_name order check ---"
|
||||
sort_errors=0
|
||||
for conf in "$NGINX_DIR/sites-available/"*.conf; do
|
||||
[ "$(basename "$conf")" = "default" ] && continue
|
||||
|
||||
mapfile -t actual < <(grep -P '^\s*server_name\s' "$conf" \
|
||||
| grep -v '^\s*#' \
|
||||
| sed 's/.*server_name\s*//' \
|
||||
| sed 's/\s*;//' \
|
||||
| awk '{print $1}' \
|
||||
| grep -vP '^~|^\*\.|^_$')
|
||||
|
||||
mapfile -t expected < <(printf '%s\n' "${actual[@]}" | LC_ALL=C sort)
|
||||
|
||||
for ((i = 0; i < ${#actual[@]}; i++)); do
|
||||
if [ "${actual[$i]}" != "${expected[$i]}" ]; then
|
||||
fail "$(basename "$conf"): not sorted — found '${actual[$i]}', expected '${expected[$i]}'"
|
||||
sort_errors=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
[ "$sort_errors" -eq 0 ] && pass "All sites-available files have alphabetically sorted server blocks"
|
||||
echo ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Summary
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
echo "==================================="
|
||||
printf "Results: %d passed, %d failed\n" "$PASS" "$FAIL"
|
||||
echo "==================================="
|
||||
[ "$FAIL" -gt 0 ] && exit 1
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user