Create an Nginx Default Catch-All Site on Ubuntu
When Nginx receives a request for an unknown domain, it may serve the first enabled site if there is no explicit default server. On a server with multiple WordPress sites or web apps, that can expose the wrong site for a domain that is not configured.
A default catch-all site fixes this by handling unmatched HTTP and HTTPS requests before they fall through to a real site.
What the Script Does
The deployment script this article is based on:
- Creates
/etc/nginx/sites-available/000-catch-all.conf. - Enables it with a symlink in
/etc/nginx/sites-enabled/. - Removes Ubuntu's default enabled site if present.
- Supports a dry run mode.
- Requires confirmation before making changes.
- Backs up existing files.
- Runs
nginx -t. - Rolls back if validation fails.
- Reloads Nginx only after validation succeeds.
Why a Catch-All Site Helps
Without a catch-all, an unknown hostname can accidentally show whichever Nginx server block is first.
That can cause problems such as:
- A staging site responding to the wrong hostname.
- A WordPress site appearing for unrelated DNS records.
- SSL confusion when an unmatched HTTPS request reaches a real site.
- Extra noise in access logs.
The catch-all gives Nginx a deliberate default.
Create the Catch-All Config
Create the file:
sudo nano /etc/nginx/sites-available/000-catch-all.conf
Add:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
For HTTP, return 444 tells Nginx to close the connection without sending a normal response.
For HTTPS, ssl_reject_handshake on; rejects unmatched TLS handshakes instead of serving a certificate for the wrong site.
Enable the Catch-All Site
Create the symlink:
sudo ln -sfn /etc/nginx/sites-available/000-catch-all.conf /etc/nginx/sites-enabled/000-catch-all.conf
If the default Ubuntu site is still enabled, back it up and remove the enabled symlink:
sudo mkdir -p /opt/server-backups/nginx
timestamp="$(date +%Y%m%d%H%M%S)"
if [ -e /etc/nginx/sites-enabled/default ] || [ -L /etc/nginx/sites-enabled/default ]; then
sudo cp -a /etc/nginx/sites-enabled/default "/opt/server-backups/nginx/sites-enabled-default.before.${timestamp}.bak"
sudo rm -f /etc/nginx/sites-enabled/default
fi
Validate and Reload
Test the config:
sudo nginx -t
If validation succeeds, reload Nginx:
sudo systemctl reload nginx
If validation fails, remove the catch-all symlink and restore your backup before reloading.
Safer Script Pattern
For production servers, use a script pattern with explicit flags:
sudo bash ./fix-nginx-catch-all.sh --dry-run sudo bash ./fix-nginx-catch-all.sh --confirm
A --dry-run mode lets you preview changes. A --confirm flag prevents accidental changes when someone runs the script without reading it.
You can also support a no-reload option:
sudo bash ./fix-nginx-catch-all.sh --confirm --no-reload
That is useful if you want a deployment system to validate now and reload Nginx later.
Full Script
Here is the full Bash script used for this Nginx default catch-all workflow:
#!/usr/bin/env bash
__show_script_usage() {
cat <<'__SCRIPT_USAGE__'
# Fix-NginxDefaultCatchAll.sh
Creates an explicit Nginx default catch-all vhost so unknown domains do not fall through to the first WordPress site.
## Example Usage
Preview:
```bash
cd /opt/DevOps/Scripts/ServerDeployment
sudo bash ./Fix-NginxDefaultCatchAll.sh --dry-run
```
Apply:
```bash
sudo bash ./Fix-NginxDefaultCatchAll.sh --confirm
```
Apply without reloading Nginx:
```bash
sudo bash ./Fix-NginxDefaultCatchAll.sh --confirm --no-reload
```
## Notes
This writes `/etc/nginx/sites-available/000-catch-all.conf`, enables it as `/etc/nginx/sites-enabled/000-catch-all.conf`, disables Ubuntu's default enabled site if present, runs `nginx -t`, and reloads Nginx only after validation succeeds.
__SCRIPT_USAGE__
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
case "${1:-}" in
-h|--help|--usage)
__show_script_usage
exit 0
;;
esac
fi
set -euo pipefail
CATCH_ALL_AVAILABLE="/etc/nginx/sites-available/000-catch-all.conf"
CATCH_ALL_ENABLED="/etc/nginx/sites-enabled/000-catch-all.conf"
UBUNTU_DEFAULT_ENABLED="/etc/nginx/sites-enabled/default"
BACKUP_DIR="/opt/DevOps/Backups/Nginx"
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
DRY_RUN="false"
CONFIRM="false"
RELOAD_NGINX="true"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN="true"
shift
;;
--confirm)
CONFIRM="true"
shift
;;
--no-reload)
RELOAD_NGINX="false"
shift
;;
-h|--help|--usage)
__show_script_usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
exit 1
;;
esac
done
if [[ "${DRY_RUN}" != "true" && "${CONFIRM}" != "true" ]]; then
echo "ERROR: Use --dry-run to preview or --confirm to apply." >&2
exit 1
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "ERROR: Please run this script with sudo." >&2
exit 1
fi
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
run() {
if [[ "${DRY_RUN}" == "true" ]]; then
printf 'DRY RUN: %s\n' "$*"
else
"$@"
fi
}
write_catch_all_config() {
if [[ "${DRY_RUN}" == "true" ]]; then
cat <<EOF
DRY RUN: write ${CATCH_ALL_AVAILABLE}:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
EOF
return
fi
cat > "${CATCH_ALL_AVAILABLE}" <<'EOF'
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
EOF
}
rollback() {
log "Rolling back catch-all changes."
if [[ -n "${CATCH_ALL_BACKUP:-}" && -e "${CATCH_ALL_BACKUP}" ]]; then
cp -a "${CATCH_ALL_BACKUP}" "${CATCH_ALL_AVAILABLE}"
else
rm -f "${CATCH_ALL_AVAILABLE}"
fi
rm -f "${CATCH_ALL_ENABLED}"
if [[ -n "${ENABLED_BACKUP:-}" && -e "${ENABLED_BACKUP}" ]]; then
cp -a "${ENABLED_BACKUP}" "${UBUNTU_DEFAULT_ENABLED}"
fi
}
if [[ "${DRY_RUN}" == "true" ]]; then
log "Previewing Nginx default catch-all remediation."
else
log "Applying Nginx default catch-all remediation."
mkdir -p "${BACKUP_DIR}"
fi
if [[ -e "${CATCH_ALL_AVAILABLE}" ]]; then
CATCH_ALL_BACKUP="${BACKUP_DIR}/000-catch-all.conf.before.${TIMESTAMP}.bak"
run cp -a "${CATCH_ALL_AVAILABLE}" "${CATCH_ALL_BACKUP}"
fi
if [[ -e "${UBUNTU_DEFAULT_ENABLED}" || -L "${UBUNTU_DEFAULT_ENABLED}" ]]; then
ENABLED_BACKUP="${BACKUP_DIR}/sites-enabled-default.before.${TIMESTAMP}.bak"
run cp -a "${UBUNTU_DEFAULT_ENABLED}" "${ENABLED_BACKUP}"
run rm -f "${UBUNTU_DEFAULT_ENABLED}"
else
log "No enabled Ubuntu default site found at ${UBUNTU_DEFAULT_ENABLED}."
fi
write_catch_all_config
run ln -sfn "${CATCH_ALL_AVAILABLE}" "${CATCH_ALL_ENABLED}"
if [[ "${DRY_RUN}" == "true" ]]; then
log "Dry run complete. Re-run with --confirm to apply."
exit 0
fi
log "Testing Nginx configuration."
if ! nginx -t; then
rollback
echo "ERROR: nginx -t failed. Changes were rolled back." >&2
exit 1
fi
if [[ "${RELOAD_NGINX}" == "true" ]]; then
log "Reloading Nginx."
systemctl reload nginx
else
log "Skipping Nginx reload because --no-reload was provided."
fi
log "Nginx default catch-all remediation complete."
Test the Result
After reloading Nginx, test an unmatched HTTP host:
curl -I -H "Host: unknown.example.com" http://127.0.0.1/
For HTTPS, test with a hostname that should not match a real site:
curl -k -I --resolve unknown.example.com:443:127.0.0.1 https://unknown.example.com/
The exact client output can vary, but the important result is that the request should not serve one of your real websites.
Quick Reference
sudo nginx -t sudo ln -sfn /etc/nginx/sites-available/000-catch-all.conf /etc/nginx/sites-enabled/000-catch-all.conf sudo systemctl reload nginx
A default catch-all site is a small Nginx hardening step. It makes unmatched domains fail intentionally instead of drifting into the first configured website.