Another Domain Showed Our Website: How We Fixed Nginx Host Routing
An unfamiliar domain displayed our branding and privacy policy. The investigation led back to our own origin server and a missing HTTPS virtual-host boundary.
We found an unfamiliar domain in Google displaying DeployWatchers pages. It carried our branding, layout and privacy policy. Our first explanation was the obvious one: someone had copied the site.
We prepared an abuse complaint. Then the response supplied an unexpected detail: the reported domain was using the same origin IP address as our legitimate website.
That changed the investigation. Before treating the page as a separately hosted copy, we needed to ask a more immediate question: would our own server return our website for a hostname we had never configured?
It did. Our HTTP entry point rejected unfamiliar hosts, but the HTTPS listener still returned the application. We fixed both the default TLS virtual host and the accepted HTTP hostnames, then checked that our real domains continued working.
How another domain can display your site without copying its files
DNS tells a client where to connect. It does not give the owner of that IP address control over every domain that someone else points there.
Nginx must decide which configured website should handle the connection. An explicit server_name identifies matching requests; it does not automatically mean that every other name will be rejected. If no name matches, the listener's default server can handle the request. Without an explicit default, the first applicable server block can become that fallback. The default belongs to the listening address and port, not to the text of server_name. Nginx request-processing documentation (opens in a new tab)
This creates a straightforward failure mode: the fallback happens to be your application, so an unrelated hostname receives your application too.
HTTPS adds another boundary. TLS Server Name Indication, or SNI, helps select the TLS configuration before the HTTP request arrives. The HTTP Host header is considered later. These values can differ; checking one does not fully check the other. Nginx virtual-server selection (opens in a new tab)
If a CDN sits in front, browser-to-CDN TLS and CDN-to-origin TLS are separate connections. A valid certificate shown to a visitor is not evidence that the origin rejects unrelated hostnames. We did not need to establish the other domain's exact CDN configuration to demonstrate the unwanted behavior on our origin.
The important limit: our finding explained how our server could supply the pages. It did not establish how the domain owner configured every part of their service, whether they retained copies, or whether any unrelated compromise had occurred.
The clues that mattered in our investigation
The suspicious page included our privacy policy, making the resemblance difficult to dismiss. The abuse response then identified our origin IP. We compared requests sent directly to that origin rather than relying on the domain's visible CDN addresses.
The decisive difference was between ports:
| Request | What we observed before the fix |
|---|---|
| Legitimate HTTPS hostname | Our application loaded |
| Unfamiliar HTTP hostname | The application was not served |
| Unfamiliar HTTPS hostname | Our application was served |
The HTTP result was useful, but incomplete. It would have been easy to stop after seeing that port 80 rejected the unwanted name. Port 443 had a different default-server configuration.
We kept the correction focused on routing. We did not rotate credentials or describe a database breach without evidence. We also avoided deleting the application's certificate: the legitimate site still needed it.
Reproduce the failure on a server you control
We verified the mechanism with an isolated Nginx 1.29.8 container, temporary certificates and loopback-only published ports. Its server names were reserved .test names; the experiment did not contact the reported domain or modify a production listener.
The fixture first gave Nginx a named HTTPS server without a rejecting default. A request for other.example.test received the application's marker. We then installed the rejecting defaults and explicit host guards shown below and reran the positive and negative checks.
The negative TLS tests use curl's --insecure option deliberately: the lab certificate is self-signed, and these checks isolate routing behavior from certificate validation. This is a diagnostic choice in a controlled fixture, not a recommendation to disable certificate checks in production.
To inspect an origin you own, replace the documentation address and example name below with your actual origin IP and hostname. Run this from an authorized diagnostic machine:
curl --resolve example.com:443:192.0.2.10 \
--connect-timeout 5 --max-time 10 \
--dump-header /tmp/origin-headers.txt \
--output /tmp/origin-body.html \
https://example.com/
--resolve changes the connection target while retaining the URL's hostname for SNI, HTTP Host and certificate verification. Use the appropriate trusted CA when the origin has a private origin certificate. A failed trust check is not a reason to disable validation in your application's normal requests. curl --resolve reference (opens in a new tab)
Before applying a change, save the relevant response status, headers and a minimal content marker. An IP match alone is weaker evidence than observing your origin actually return your site's content to an unwanted hostname.
Fix the default listeners, then validate allowed hosts
On our stack, the first public listener was the host's Nginx, ahead of container Nginx and PHP. That was the first place to enforce the boundary. A PHP middleware cannot reject static files that Nginx serves without PHP.
Inspect the active configuration on the Nginx host:
sudo nginx -T
Look at every applicable listen directive, including IPv6 and any address-specific listeners. Check which server is the default for each socket. Do not publish the complete output: it can contain deployment-specific information.
The following reference blocks belong inside Nginx's http context, commonly through an included configuration file. They are additions to a reviewed configuration, not a complete replacement for an existing site:
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;
return 444;
}
Here, default_server performs the fallback selection. The underscore is just a conventional name, not a wildcard that creates the rejection by itself.
Nginx's nonstandard 444 closes the connection rather than sending a normal HTTP response, so curl commonly reports an empty reply. ssl_reject_handshake rejects TLS at the default server before an HTTP page can be served; it requires Nginx 1.19.4 or newer. Check compatibility before applying this on an older installation. Nginx TLS rejection reference (opens in a new tab)
Do not add a second conflicting default to an occupied listener. Resolve the existing fallback deliberately. If your configuration uses specific IP addresses, review the defaults for those listeners too. An IPv6 example also assumes IPv6 is available on that server.
Next, in each legitimate site server block, allow the incoming HTTP hostname explicitly, before redirects or proxying:
server_name example.com www.example.com;
if ($http_host !~* "^(?:example\.com|www\.example\.com)(?::[0-9]{1,5})?$") {
return 421;
}
Replace both example names with the names that block is actually meant to serve. A staging site should have its own exact allowlist. The optional port suffix admits authorities such as example.com:443; it is not a general-purpose hostname parser.
This guard uses the incoming $http_host. By contrast, $host can fall back to a configured server name when the request supplies no hostname. An allowlist that checks a substituted value can miss the absence of the original authority. The if shown here only returns a response; it does not rewrite a location or change an upstream. Nginx request variables (opens in a new tab)
A hard-coded canonical redirect can follow the allowlist. Avoid redirecting arbitrary incoming hosts using https://$http_host...; reject an unwanted name instead of reflecting it into a destination URL.
Test first, reload, and prove the rejection
Back up the files you are changing. On the Nginx host, validate the candidate configuration before a graceful reload:
sudo nginx -t
sudo systemctl reload nginx
Run the reload only after the syntax check succeeds. Keep the previous configuration available for rollback. A successful syntax check proves that Nginx can parse the files, not that the host-routing policy is correct. Nginx reload behavior (opens in a new tab)
Our verification matrix included combinations that an ordinary uptime request never exercises:
| TLS SNI | HTTP Host | Expected result after the fix |
|---|---|---|
| Legitimate name | Legitimate name | Normal application response |
| Unfamiliar name | Unfamiliar name | TLS handshake rejected |
| Unfamiliar name | Legitimate name | TLS handshake rejected |
| Legitimate name | Unfamiliar name | No application content: 421 or connection closed |
| No TLS, port 80 | Unfamiliar name | Connection closed, no application content |
| Legitimate www name | Legitimate www name | Intended canonical redirect |
To separate SNI from Host against your own origin, keep the legitimate name in the URL and override only the HTTP header:
curl --resolve example.com:443:192.0.2.10 \
--connect-timeout 5 --max-time 10 \
--header 'Host: other.example.test' \
https://example.com/
A failed curl command is expected when the server deliberately closes the connection. Record the exit code and response content instead of interpreting every connection error as a broken deployment. In our controlled lab, rejected handshakes produced exit 35 and dropped HTTP connections produced exit 52. Other TLS libraries or intermediaries may present failures differently.
Also check the public site through its normal CDN route, without bypassing certificate validation. Verify static assets, authentication, health checks and every legitimate hostname. In our actual incident, the original site continued returning its normal response, the www redirect remained available, and the unwanted origin requests stopped receiving application HTML.
Cloudflare, canonical URLs and the limits of this fix
The complaint helped us investigate, but changing the origin removed the immediate routing path. These are separate actions: a provider's abuse process does not automatically correct your server configuration.
An origin guard does not delete pages that someone has already cached, scraped or independently hosted. Search results can also outlive a routing fix. Preserve evidence and pursue abuse or search-removal channels separately where appropriate; do not treat a remaining search result as proof that the origin still accepts the request.
A canonical tag expresses a preferred indexing URL. It does not authorize a request or stop Nginx from serving content to an unrelated hostname.
Restricting origin access to Cloudflare's networks can reduce direct exposure, but shared CDN source networks do not identify your specific domain. Cloudflare also documents that its globally shared Authenticated Origin Pull certificate authenticates the Cloudflare network, not an individual customer account. Stronger origin restrictions need deliberate, account-appropriate configuration and monitoring of legitimate traffic. We did not enable additional mTLS or firewall policies as part of this routing fix. Cloudflare Authenticated Origin Pulls (opens in a new tab)
Application-level host validation can add another boundary for dynamic routes. Configure its proxy trust correctly, and remember that internal readiness probes must send an accepted Host while connecting to the internal service address. Our host protection exposed exactly that mismatch in a later deployment; allowing every Docker service name publicly would have weakened the correction.
Keep negative host checks in the release process
DeployWatchers' normal website checks help detect availability, browser errors, visual changes and other release problems. A check against the legitimate URL alone does not discover every unrelated domain or prove that unknown hostnames are rejected.
We now treat that rejection as a separate deployment regression: valid names must still work, unfamiliar names must not receive the application, and HTTP and HTTPS must both be covered.
If an unfamiliar domain displays your site, investigate the origin path before assuming a copied codebase. In our case, the revealing test was not whether our own hostname worked. It was whether the same origin would serve the application for a name that did not belong there.
Catch problems after your next deployment
Monitor availability, browser JavaScript errors and visual changes with DeployWatchers.