Overview
When production servers need to be taken completely offline, the goal is not just uptime messaging. It is trust. Visitors should see a calm, intentional maintenance page over HTTPS, not browser warnings, Cloudflare error screens, or raw 503 output.
This article walks through a practical pattern for building a dedicated NGINX maintenance server that can safely replace production during maintenance windows using NAT, while remaining compatible with HSTS and Cloudflare.
Requirements
- HTTPS-only (no port 80)
- Compatible with HSTS
- Works with Cloudflare (proxied or DNS-only)
- Supports multiple unrelated domains
- No dependency on production NGINX configs
- Predictable behavior during outages
Architectural Approach
Separate Responsibilities
- Production servers route traffic to applications
- Maintenance server only terminates TLS and returns a maintenance response
This separation avoids configuration drift and reduces risk during outages. The maintenance server never needs to know how applications work.
Why You Cannot Use a “Wildcard TLS Handler”
TLS certificates are selected before NGINX request logic runs. Because of this:
- Certificates must be declared explicitly
- A single wildcard certificate cannot span unrelated domains
- NGINX cannot dynamically choose certificates at runtime
The correct approach is one wildcard certificate per apex domain, selected via SNI.
Certificate Strategy (DNS-01)
Using Cloudflare’s DNS API allows certificates to be issued without any HTTP endpoints:
- One wildcard certificate per domain
- No HTTP challenges
- No dependency on running services
- Fully HSTS-safe
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d domain.com \
-d '*.domain.com'
NGINX Maintenance Configuration
Key Design Points
- Always return HTTP 503
- Serve a custom HTML page
- No proxying
- No application logic
- Identical behavior for all paths
server {
listen 443 ssl;
server_name domain.com *.domain.com;
ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;
root /var/www/maintenance;
index index.html;
error_page 503 =503 /index.html;
location / {
add_header Retry-After 300 always;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
return 503;
}
}
NAT Failover Model
During maintenance:
- The public IP address remains unchanged
- NAT forwards port 443 to the maintenance server
- Cloudflare continues to function normally
This avoids DNS changes and eliminates propagation delays.
Common Pitfalls (and Fixes)
Host Firewall Blocking HTTPS
firewall-cmd --add-service=https --permanent
firewall-cmd --reload
SELinux Blocking Static Files
On RHEL-family systems, NGINX requires the correct SELinux context.
semanage fcontext -a -t httpd_sys_content_t "/var/www/maintenance(/.*)?"
restorecon -Rv /var/www/maintenance
Without this, NGINX will return 403 even when filesystem permissions are correct.
Testing Without Impacting Production
You can validate the maintenance server without touching DNS:
curl -I --resolve domain.com:443:10.0.0.10 https://domain.com
This confirms TLS, certificate selection, HTTP status, and content.
Synchronization Strategy
Instead of syncing full NGINX configurations:
- Maintain a simple domain list
- Issue certificates only for domains that may fail over
- Generate maintenance configs from a template
This keeps the system predictable and auditable.
Final Result
- Calm, branded maintenance pages
- Correct HTTP semantics
- No browser warnings
- No Cloudflare error pages
- Minimal moving parts
Most importantly, maintenance feels intentional rather than broken.