In many software development environments, test websites are hosted internally on an IIS server. These websites may use internal development domains such as:
*.project.dev.internal-domain.com
However, there are situations where those internal websites need to be accessed from the internet using a different public domain, for example:
*.project.dev.public-domain.com
A common and effective solution is to use a public Nginx reverse proxy. The Nginx server receives the public request, translates the hostname, and forwards the traffic to the correct internal IIS website.
This setup is especially useful for development, staging, QA, UAT, and customer testing environments where multiple subdomains need to be routed dynamically without manually configuring every public hostname one by one.
Example Scenario
Assume there are several IIS test websites running internally under a domain like:
site1.project.dev.internal-domain.com
site2.project.dev.internal-domain.com
client-a.project.dev.internal-domain.com
The goal is to make them publicly available through a different domain:
site1.project.dev.public-domain.com
site2.project.dev.public-domain.com
client-a.project.dev.public-domain.com
The public domain should point to an Nginx reverse proxy hosted on a different machine with a public IP address.
The reverse proxy then forwards the request to the internal IIS server while preserving or translating the hostname correctly.
Is This Possible?
Yes, this is possible.
Nginx can receive requests for wildcard subdomains and forward them to an IIS server. It can also rewrite the Host header so that IIS receives the hostname it already knows how to handle.
This is important because IIS uses site bindings to decide which website should serve a request. If IIS expects:
site1.project.dev.internal-domain.com
but the request arrives with:
site1.project.dev.public-domain.com
IIS may not match the request to the correct website unless that public hostname is also configured as a binding.
A cleaner solution is to let Nginx translate the public hostname into the internal IIS hostname before forwarding the request.
High-Level Architecture
The flow looks like this:
User browser
↓
site1.project.dev.public-domain.com
↓
Public DNS wildcard record
↓
Public Nginx reverse proxy
↓
Nginx rewrites Host header
↓
site1.project.dev.internal-domain.com
↓
Internal IIS server
This allows users to access public-looking URLs while keeping the actual IIS websites hosted internally.
Step 1: Configure Wildcard DNS
First, create a wildcard DNS record for the public domain.
Example:
*.project.dev.public-domain.com A PUBLIC_NGINX_IP
For example:
*.project.dev.public-domain.com A 203.0.113.10
This means that any subdomain matching this pattern will resolve to the public Nginx server:
site1.project.dev.public-domain.com
site2.project.dev.public-domain.com
client-a.project.dev.public-domain.com
Important note: a wildcard DNS record usually does not cover the root hostname itself.
So this record:
*.project.dev.public-domain.com
does not automatically cover:
project.dev.public-domain.com
If the parent domain also needs to work, add a separate DNS record for it.
Step 2: Configure Nginx as a Reverse Proxy
Nginx can use a regular expression in the server_name directive to capture the dynamic part of the hostname.
For example, in this hostname:
site1.project.dev.public-domain.com
the dynamic part is:
site1
Nginx can capture that value and reuse it when forwarding the request to:
site1.project.dev.internal-domain.com
Example Nginx configuration:
server {
listen 80;
server_name ~^(?<site>[^.]+)\.project\.dev\.public-domain\.com$;
location / {
proxy_pass http://192.168.1.100;
proxy_set_header Host $site.project.dev.internal-domain.com;
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_set_header X-Forwarded-Host $host;
}
}
In this example:
192.168.1.100
is the internal IP address of the IIS server.
When a user accesses:
site1.project.dev.public-domain.com
Nginx forwards the request to the IIS server with this host header:
Host: site1.project.dev.internal-domain.com
That allows IIS to match the existing binding.
Step 3: Why the Host Header Matters
The Host header is one of the most important parts of this setup.
When several websites are hosted on the same IIS server, IIS uses bindings to determine which website should respond. A binding may include:
Protocol: HTTP or HTTPS
IP address: specific IP or all unassigned
Port: 80 or 443
Hostname: site1.project.dev.internal-domain.com
If the incoming request does not contain a hostname that matches an IIS binding, IIS may return the wrong website, a default site, or an error.
That is why this Nginx directive is important:
proxy_set_header Host $site.project.dev.internal-domain.com;
It tells Nginx to send the internal hostname to IIS instead of the public hostname.
Step 4: Add HTTPS Support
For public access, HTTPS should be used.
The Nginx reverse proxy can terminate SSL using a wildcard certificate for:
*.project.dev.public-domain.com
Example HTTPS Nginx configuration:
server {
listen 443 ssl;
server_name ~^(?<site>[^.]+)\.project\.dev\.public-domain\.com$;
ssl_certificate /etc/letsencrypt/live/project.dev.public-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/project.dev.public-domain.com/privkey.pem;
location / {
proxy_pass http://192.168.1.100;
proxy_set_header Host $site.project.dev.internal-domain.com;
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 https;
proxy_set_header X-Forwarded-Host $host;
}
}
In this configuration, HTTPS is handled by Nginx. The connection between Nginx and IIS may remain HTTP if the network is private and trusted.
For higher security, HTTPS can also be used between Nginx and IIS, but that requires IIS to have valid SSL bindings for the internal hostnames.
Step 5: Redirect HTTP to HTTPS
After HTTPS is working, HTTP traffic should be redirected to HTTPS.
Example:
server {
listen 80;
server_name ~^(?<site>[^.]+)\.project\.dev\.public-domain\.com$;
return 301 https://$host$request_uri;
}
This ensures that users who access:
http://site1.project.dev.public-domain.com
are automatically redirected to:
https://site1.project.dev.public-domain.com
Step 6: Generate a Wildcard SSL Certificate
A wildcard SSL certificate is needed for:
*.project.dev.public-domain.com
With Let’s Encrypt, wildcard certificates require DNS validation.
A manual Certbot command may look like this:
certbot certonly \
--manual \
--preferred-challenges dns \
-d "*.project.dev.public-domain.com"
During the process, Certbot asks for a TXT DNS record to be added.
For production or long-term usage, automatic renewal is recommended. This usually requires a DNS plugin for the DNS provider.
For example, if the DNS is managed by Cloudflare, Route 53, DigitalOcean, or another supported provider, Certbot can automate the DNS challenge.
Step 7: Make Sure Nginx Can Reach IIS
The public Nginx server must be able to connect to the internal IIS server.
This can be done through:
- VPN
- private network routing
- firewall rule
- site-to-site tunnel
- public firewall rule restricted only to the Nginx IP
A simple test from the Nginx server is:
curl -I -H "Host: site1.project.dev.internal-domain.com" http://192.168.1.100
If IIS responds correctly, Nginx should also be able to proxy the traffic.
If the command fails, check:
- firewall rules
- IIS bindings
- internal DNS resolution
- network routing
- VPN connectivity
- Windows Firewall rules
- port 80 or 443 access
Step 8: Watch Out for Application Redirects
Even if the reverse proxy works correctly, the application hosted in IIS may generate redirects using the internal hostname.
For example, a user may open:
https://site1.project.dev.public-domain.com/login
but the application may redirect to:
https://site1.project.dev.internal-domain.com/login
This happens when the application builds absolute URLs based on the Host header received by IIS.
Because Nginx is sending the internal hostname to IIS, some applications may think the internal hostname is the public URL.
To help with this, Nginx should also pass:
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
Then the application can use these headers to detect the original public URL.
However, the application must support forwarded headers. If it does not, there are two common solutions.
Option 1: Keep IIS Internal Bindings and Rewrite Host in Nginx
This is usually the simplest option.
Nginx receives:
site1.project.dev.public-domain.com
and forwards to IIS using:
site1.project.dev.internal-domain.com
Advantages:
- No need to add public bindings in IIS.
- Existing IIS configuration can remain unchanged.
- New subdomains can work automatically if they already exist internally.
- Centralized public access through Nginx.
- Cleaner separation between public and internal domains.
Disadvantages:
- Applications may generate internal URLs if they do not support forwarded headers.
- Some login flows, redirects, or absolute links may need additional configuration.
- Cookies may need to be checked if they are domain-specific.
Option 2: Add Public Hostname Bindings in IIS
Another option is to add the public domain bindings directly to IIS.
For example, IIS would accept both:
site1.project.dev.internal-domain.com
site1.project.dev.public-domain.com
Then Nginx can preserve the original host:
proxy_set_header Host $host;
Advantages:
- Applications see the public hostname directly.
- Fewer issues with redirects and absolute URLs.
- Easier for applications that are not reverse-proxy aware.
Disadvantages:
- More IIS bindings to maintain.
- Every new test website may need an additional public hostname binding.
- More operational overhead.
- Wildcard binding behavior in IIS may not match every desired scenario.
Option 3: Use Nginx Redirect Rewriting
If the application redirects to the internal hostname, Nginx can sometimes rewrite redirects using proxy_redirect.
Example:
proxy_redirect http://$site.project.dev.internal-domain.com/ https://$site.project.dev.public-domain.com/;
proxy_redirect https://$site.project.dev.internal-domain.com/ https://$site.project.dev.public-domain.com/;
This can help when the backend sends redirect headers pointing to the internal domain.
However, this does not fix every situation. It may not rewrite links inside HTML, JavaScript, API responses, or emails generated by the application.
Recommended Setup
For most internal IIS test environments, the recommended setup is:
- Create a wildcard DNS record for the public domain.
- Point it to the public Nginx reverse proxy.
- Use a wildcard SSL certificate on Nginx.
- Use Nginx regex hostname capture.
- Rewrite the
Hostheader to the internal IIS hostname. - Pass
X-Forwarded-HostandX-Forwarded-Proto. - Make sure applications understand forwarded headers where necessary.
- Restrict direct access to IIS from the internet.
Recommended flow:
site1.project.dev.public-domain.com
↓
Nginx public reverse proxy
↓
Host rewritten to site1.project.dev.internal-domain.com
↓
Internal IIS website
Full Example Nginx Configuration
Below is a more complete example.
server {
listen 80;
server_name ~^(?<site>[^.]+)\.project\.dev\.public-domain\.com$;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name ~^(?<site>[^.]+)\.project\.dev\.public-domain\.com$;
ssl_certificate /etc/letsencrypt/live/project.dev.public-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/project.dev.public-domain.com/privkey.pem;
location / {
proxy_pass http://192.168.1.100;
proxy_set_header Host $site.project.dev.internal-domain.com;
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 https;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
This configuration handles:
- wildcard public subdomains
- HTTPS
- HTTP to HTTPS redirect
- hostname translation
- original client IP forwarding
- forwarded protocol and host headers
Testing the Setup
After configuration, test DNS first:
nslookup site1.project.dev.public-domain.com
or:
dig site1.project.dev.public-domain.com
It should return the public IP of the Nginx server.
Then test Nginx:
curl -I https://site1.project.dev.public-domain.com
Then test the internal IIS response from the Nginx machine:
curl -I -H "Host: site1.project.dev.internal-domain.com" http://192.168.1.100
If the internal test works but the public test does not, the problem is probably in Nginx, DNS, SSL, or firewall configuration.
If the internal test does not work, the problem is probably in IIS bindings, routing, or firewall access between Nginx and IIS.
Common Problems
1. IIS Shows the Wrong Website
This usually means the Host header does not match the expected IIS binding.
Check:
proxy_set_header Host $site.project.dev.internal-domain.com;
Also confirm that IIS has a binding for:
site1.project.dev.internal-domain.com
2. SSL Works but the Website Redirects to the Internal Domain
This usually means the application is generating absolute redirects based on the internal hostname.
Possible fixes:
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
Also configure the application to trust forwarded headers.
If needed, use:
proxy_redirect
3. Wildcard DNS Does Not Work
Check that the DNS record exists:
*.project.dev.public-domain.com A PUBLIC_NGINX_IP
Also remember that wildcard DNS does not always cover the parent hostname itself.
Add a separate record if needed:
project.dev.public-domain.com A PUBLIC_NGINX_IP
4. Nginx Cannot Reach IIS
Check connectivity from the Nginx server:
curl -I http://192.168.1.100
Also check:
- Windows Firewall
- IIS listening ports
- network routing
- VPN connection
- security groups
- public/private firewall restrictions
5. Cookies or Login Sessions Do Not Work
Some applications set cookies for a specific domain.
If the backend application sets cookies for:
.project.dev.internal-domain.com
they may not work correctly on:
.project.dev.public-domain.com
Possible solutions include:
- configure the application’s public base URL
- adjust cookie domain settings
- use forwarded headers
- use
proxy_cookie_domainin Nginx if necessary
Example:
proxy_cookie_domain .project.dev.internal-domain.com .project.dev.public-domain.com;
Security Recommendations
Even for test websites, security matters.
Recommended practices:
- Use HTTPS on the public Nginx proxy.
- Restrict IIS access so only the Nginx proxy can reach it.
- Do not expose the IIS server directly to the internet unless necessary.
- Use firewall rules between Nginx and IIS.
- Add basic authentication for sensitive test environments if needed.
- Keep test environments separated from production.
- Avoid exposing admin tools publicly.
- Log public requests on Nginx.
- Monitor failed requests and suspicious traffic.
- Keep certificates renewed automatically.
For additional protection, Nginx can also use basic authentication:
auth_basic "Restricted Test Environment";
auth_basic_user_file /etc/nginx/.htpasswd;
This is useful for staging, QA, and internal customer preview websites.
Final Conclusion
Exposing internal IIS test websites through a public Nginx reverse proxy using wildcard subdomains is not only possible, but also a clean and scalable solution.
The key parts are:
Wildcard DNS
+
Nginx reverse proxy
+
Host header translation
+
IIS hostname bindings
+
HTTPS wildcard certificate
The most important technical detail is the Host header. IIS must receive the hostname it expects, otherwise it may not route the request to the correct website.
A good final architecture is:
*.project.dev.public-domain.com
↓
Public Nginx reverse proxy
↓
Host rewritten by Nginx
↓
*.project.dev.internal-domain.com
↓
Internal IIS website
This approach allows development and staging websites to remain internally hosted while being safely and conveniently exposed through a controlled public reverse proxy.
I can also make a shorter LinkedIn-style version or a more technical DevOps documentation version.


