Logo

GeoServer Behind Nginx: Public and Private Access with HTTPS

GeoServer Behind Nginx: Public and Private Access with HTTPS



A practical guide to reverse proxying GeoServer through multiple endpoints, handling CSRF/origin validation, authentication, and troubleshooting


Table of Contents

  1. Introduction
  2. Architecture Overview
  3. Nginx Reverse Proxy Configuration
  4. GeoServer Proxy Configuration
  5. Supporting Multiple Access Endpoints
  6. Public-Only Authentication
  7. GeoServer CSRF and Origin Validation
  8. Restarting GeoServer Correctly
  9. Troubleshooting Flow
  10. Common Errors and Fixes
  11. Production Checklist
  12. Quick Reference

1. Introduction

GeoServer is commonly deployed behind Nginx to provide HTTPS termination, reverse proxying, authentication, request limits, and centralized access control.

A particularly useful production architecture is to expose the same GeoServer through two different entry points:

Private network
│
▼
Private endpoint
│
└──────────────┐
│
▼
Nginx
│
▼
GeoServer
▲
│
┌──────────────┘
│
Public Internet
│
▼
Public hostname

This allows internal users to access GeoServer through the private network while external users access it through a public hostname.

The two endpoints can have different security policies.

For example:

EndpointHTTPSNginx authentication
PrivateYesOptional
PublicYesBasic Auth

The important part is that both endpoints ultimately reach the same GeoServer instance.


2. Architecture Overview

2.1 Recommended architecture

┌─────────────────────┐
│ INTERNET │
└──────────┬──────────┘
│
│ HTTPS
▼
┌─────────────────────┐
│ NGINX │
│ :443 │
└──────────┬──────────┘
│
┌─────────────────┴─────────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Public endpoint │ │ Private endpoint │
│ │ │ │
│ Basic Auth │ │ Optional Auth │
└────────┬─────────┘ └────────┬─────────┘
│ │
└────────────────┬─────────────────┘
│
│ HTTP
▼
┌─────────────────────┐
│ GeoServer │
│ :8080 │
└─────────────────────┘

The important concept is:

Nginx provides multiple access paths; GeoServer remains a single backend service.


2.2 Request flow

A public request:

Browser
│
│ HTTPS
▼
Nginx :443
│
│ X-Forwarded-* headers
▼
GeoServer :8080

A private request follows the same backend path:

Private Client
│
│ HTTPS
▼
Nginx :443
│
▼
GeoServer :8080

GeoServer therefore needs to understand that the original request was HTTPS even though its connection from Nginx is HTTP.


3. Nginx Reverse Proxy Configuration

The basic GeoServer location should look like:

location /geoserver/ {

proxy_pass http://127.0.0.1:8080/geoserver/;

proxy_http_version 1.1;

proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Port $server_port;

proxy_redirect off;

proxy_connect_timeout 1h;
proxy_send_timeout 1h;
proxy_read_timeout 1h;
}

Why these headers matter

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

tell GeoServer about the original request.

Without them, GeoServer may think the external URL is something like:

http://127.0.0.1:8080/geoserver

instead of:

https://public-host/geoserver

4. GeoServer Proxy Configuration

GeoServer must also be configured to understand the reverse proxy.

In:

WEB-INF/web.xml

or the GeoServer configuration where appropriate, make sure the proxy configuration is consistent with the Nginx headers.

In global.xml, the relevant configuration can be:

<proxyBaseUrl>
${X-Forwarded-Proto}://${X-Forwarded-Host}/geoserver
</proxyBaseUrl>

and:

<useHeadersProxyURL>true</useHeadersProxyURL>

This creates the relationship:

Nginx:

X-Forwarded-Proto: https
X-Forwarded-Host: public-host
X-Forwarded-Port: 443

↓

GeoServer:

https://public-host/geoserver

This becomes particularly important for the GeoServer Web UI, Wicket AJAX requests, redirects, generated links, and service URLs.


5. Supporting Multiple Access Endpoints

Use separate Nginx server blocks.

Private endpoint

server {
listen 443 ssl;
server_name PRIVATE_ENDPOINT;

ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;

location /geoserver/ {

proxy_pass http://127.0.0.1:8080/geoserver/;

proxy_http_version 1.1;

proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Port $server_port;

proxy_redirect off;
}
}

Public endpoint

server {
listen 443 ssl;
server_name PUBLIC_HOSTNAME;

ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;

location /geoserver/ {

auth_basic "Restricted GeoServer";
auth_basic_user_file /etc/nginx/.htpasswd;

proxy_pass http://127.0.0.1:8080/geoserver/;

proxy_http_version 1.1;

proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Port $server_port;

proxy_redirect off;
}
}

This provides:

Private
↓
Nginx
↓
GeoServer

Public
↓
Nginx
↓
Basic Auth
↓
GeoServer

6. Public-Only Authentication

A useful security model is to protect only the public endpoint with Nginx Basic Auth.

auth_basic "Restricted GeoServer";
auth_basic_user_file /etc/nginx/.htpasswd;

Place these directives inside the public GeoServer location.

Do not put them in a global configuration if private users should bypass that authentication.

For example:

location /geoserver/ {

auth_basic "Restricted GeoServer";
auth_basic_user_file /etc/nginx/.htpasswd;

proxy_pass http://127.0.0.1:8080/geoserver/;
}

The private endpoint can omit auth_basic.


7. GeoServer CSRF and Origin Validation

This is one of the most important parts of a multi-endpoint GeoServer deployment.

GeoServer's Web UI performs many AJAX requests.

For example:

GET /geoserver/web/...

may work correctly while:

POST /geoserver/web/wicket/...

returns:

400 Bad Request

with:

Origin does not correspond to request

This is a strong indication that the request is being rejected by origin/CSRF validation rather than by the Nginx proxy itself.


7.1 Why this happens

The browser sends an origin such as:

Origin: https://public-host

or:

Origin: https://private-endpoint

GeoServer must recognize the legitimate endpoint.

When GeoServer is accessed through multiple hostnames or addresses, all legitimate endpoints need to be considered.


7.2 Configure the CSRF whitelist

In GeoServer's:

WEB-INF/web.xml

configure the trusted endpoints:

<context-param>
<param-name>GEOSERVER_CSRF_WHITELIST</param-name>
<param-value>
PRIVATE_ENDPOINT,PUBLIC_HOSTNAME
</param-value>
</context-param>

Use the actual trusted endpoints for your environment.

Do not add arbitrary origins.


8. Restarting GeoServer Correctly

This was one of the key lessons from troubleshooting.

Changing:

WEB-INF/web.xml

does not automatically reload the configuration into an already-running Java process.

After modifying the configuration:

sudo systemctl restart geoserver

Then verify:

sudo systemctl status geoserver

and:

ps aux | grep '[g]eoserver'

If GeoServer is systemd-managed

Prefer:

sudo systemctl restart geoserver

over manually running:

./shutdown.sh
./startup.sh

The systemd service may have its own:

  • Java options
  • environment variables
  • Jetty configuration
  • working directory
  • data directory
  • stop port
  • user account

Therefore, always determine how the service is actually running.

systemctl status geoserver

is a good first check.


9. Troubleshooting Flow

When something fails, use this order.

Request fails
│
▼
Does the page load?
/ \
YES NO
│ │
▼ ▼
AJAX/POST? Check Nginx
/ \ server block
YES NO
│ │
▼ ▼
Check 400 Check proxy
response configuration
│
▼
"Origin does not correspond?"
│
YES
│
▼
Check CSRF whitelist
│
▼
Restart GeoServer
│
▼
Test again

Step 1 — Test Nginx

sudo nginx -t

Expected:

syntax is ok
test is successful

Then:

sudo systemctl reload nginx

Step 2 — Check which server block is being used

sudo nginx -T

Look for the relevant:

server_name

Make sure the requested hostname is being handled by the intended server block.


Step 3 — Test GeoServer directly

From the server:

curl -I http://127.0.0.1:8080/geoserver/

If this works but the Nginx URL doesn't, focus on Nginx.

If this fails too, focus on GeoServer/Jetty.


Step 4 — Check the browser request

Open DevTools → Network.

Look at:

Request URL
Request Method
Status Code
Origin
Referer
Host

A useful distinction is:

GET → 200
POST → 400

This often indicates an application-level validation issue rather than basic proxy connectivity.


Step 5 — Check GeoServer logs

For a systemd-managed installation:

sudo journalctl -u geoserver -f

Then reproduce the error.


10. Common Errors and Fixes

Error: 400 Origin does not correspond to request

Likely cause

GeoServer's origin/CSRF validation doesn't recognize the endpoint being used.

Check

WEB-INF/web.xml

for:

GEOSERVER_CSRF_WHITELIST

Fix

Add the legitimate private/public endpoints and restart:

sudo systemctl restart geoserver

Error: GeoServer redirects to the wrong hostname

Possible cause

GeoServer doesn't have the correct reverse-proxy information.

Check Nginx:

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;

Check GeoServer:

<proxyBaseUrl>
${X-Forwarded-Proto}://${X-Forwarded-Host}/geoserver
</proxyBaseUrl>

Error: Public Basic Auth keeps asking for credentials

Check whether:

auth_basic

is being applied to the correct location.

Also verify that there aren't conflicting Nginx server blocks handling the same hostname.

Test:

sudo nginx -T

Error: Private endpoint unexpectedly asks for Basic Auth

Check that auth_basic wasn't inherited from a higher-level block.

It should only be configured where required.


Error: 403 Forbidden

If the configuration contains:

allow ...
deny all;

remember that Nginx access control is evaluated before the request reaches GeoServer.

Temporarily remove IP restrictions while diagnosing whether the problem is:

Nginx access control

or:

GeoServer

Error: nginx: conflicting server name

Example:

conflicting server name ... on 0.0.0.0:443

This means multiple server blocks are competing for the same hostname/listener combination.

Find duplicates:

sudo nginx -T | grep -n "server_name"

Then remove or consolidate the conflicting configuration.


11. Production Checklist

Before deploying, verify each item.

Nginx

  • nginx -t succeeds
  • Private and public endpoints have the intended server_name
  • HTTPS certificates are correct
  • GeoServer proxy location is correct
  • X-Forwarded-* headers are configured
  • proxy_redirect off is appropriate
  • Timeouts are appropriate for long-running GeoServer operations
  • Upload limits are appropriate
  • Public authentication is applied only where intended
  • No conflicting server_name declarations exist

GeoServer

  • GeoServer is running under the expected service
  • Correct GEOSERVER_DATA_DIR is being used
  • proxyBaseUrl is correct
  • useHeadersProxyURL is enabled when using forwarded headers
  • Legitimate endpoints are included in CSRF configuration
  • GeoServer is restarted after web.xml changes
  • GeoServer logs are accessible

Testing

Test both:

Private endpoint
├── Login
├── GeoServer Web UI
├── Create/edit style
├── Upload
├── AJAX operations
└── REST/API operations

Public hostname
├── Nginx authentication
├── GeoServer Web UI
├── Create/edit style
├── AJAX operations
└── REST/API operations

Most importantly, don't test only the initial page load.

Test an operation that generates a POST/AJAX request.


12. Quick Reference

Nginx

location /geoserver/ {

proxy_pass http://127.0.0.1:8080/geoserver/;

proxy_http_version 1.1;

proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Port $server_port;

proxy_redirect off;
}

GeoServer

<proxyBaseUrl>
${X-Forwarded-Proto}://${X-Forwarded-Host}/geoserver
</proxyBaseUrl>

<useHeadersProxyURL>true</useHeadersProxyURL>

CSRF

<context-param>
<param-name>GEOSERVER_CSRF_WHITELIST</param-name>
<param-value>PRIVATE_ENDPOINT,PUBLIC_HOSTNAME</param-value>
</context-param>

Restart

sudo systemctl restart geoserver

Verify

sudo systemctl status geoserver

Nginx test

sudo nginx -t
sudo systemctl reload nginx

GeoServer logs

sudo journalctl -u geoserver -f

Nginx configuration

sudo nginx -T

Final Debugging Rule

When maintaining a GeoServer installation behind Nginx, remember this simple rule:

Page doesn't load
│
▼
Check Nginx / connectivity

but:

Page loads
│
└── specific Web UI action → 400
│
▼
Inspect Origin / Referer
│
▼
"Origin does not correspond"
│
▼
Check GeoServer CSRF
│
▼
Restart GeoServer

This distinction is especially valuable in production because it prevents repeatedly changing a working Nginx reverse-proxy configuration when the actual problem is inside GeoServer's request validation.