nox.im · All Snippets · All in Ubuntu · All in Server

Ubuntu Nginx Reverse Proxy CORS Headers

When we’re exposing services behind an Nginx reverse proxy and our service is an API powering a web frontend, we need to add Access-Control-Allow-Origin headers to avoid Cross-Origin Resource Sharing (CORS) errors.

CORS headers allow a server to specify domains (and schemes, ports) from which a browser should permit loading resources. This is required for example when loading a frontend client in web3 from a domain, while loading an API from a different domain, or even talking to a localhost node while in development.

A preflight request is a small request that is sent by the browser before the actual request in order to check if the CORS protocol is understood and a server is aware of headers.

Debugging CORS with curl:

curl -H "Origin: http://domain-or-ip-i-want-to-test.com/" \
  -H "Access-Control-Request-Method: POST" \
  -X OPTIONS -v \
http://localhost:8899

We should see the request headers:

* Connected to localhost (::1) port 8899 (#0)
> OPTIONS / HTTP/1.1
> Host: localhost:8899
> Origin: http://domain-or-ip-i-want-to-test.com/
> Access-Control-Request-Method: POST
> Access-Control-Request-Headers: X-Requested-With

and the relevant response headers, here desired:

< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, OPTIONS, HEAD
< Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept

indicating that we allow all origins, and the requested method and headers are supported.

We can add these headers with Nginx as follows:

server {
    location / {
        # ...

        proxy_set_header Origin http://myservice;
        proxy_hide_header Access-Control-Allow-Origin;

        if ($request_method ~* "(GET|POST)") {
            add_header "Access-Control-Allow-Origin"  *;
        }

        # preflight
        if ($request_method = OPTIONS ) {
            add_header "Access-Control-Allow-Origin"  *;
            add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
            add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
            return 204;
        }
    }
}

And restart Nginx by testing the config and reloading it if it’s ok:

nginx -t
nginx -s reload

CORS Headers on an SSH Tunnel

upstream solanaRPC {
    server 127.0.0.1:8899;
}


upstream solanaWS {
    # sticky IP session
    ip_hash;
    server 127.0.0.1:8900;
}

server {
  listen 8891;
  server_name _;

  location / {
        proxy_pass http://solanaRPC;

        proxy_set_header Origin http://solanaRPC;
        proxy_hide_header Access-Control-Allow-Origin;

        # Simple requests
        if ($request_method ~* "(GET|POST)") {
            add_header "Access-Control-Allow-Origin"  *;
        }

        # Preflighted requests
        if ($request_method = OPTIONS ) {
            add_header "Access-Control-Allow-Origin"  *;
            add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
            add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
            return 204;
        }
    }
}

server {
  listen 8901;
  server_name _;

  location / {
        proxy_pass http://solanaWS;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header Origin http://solanaWS;
        proxy_hide_header Access-Control-Allow-Origin;

        # Simple requests
        if ($request_method ~* "(GET|POST)") {
            add_header "Access-Control-Allow-Origin"  *;
        }

        # Preflighted requests
        if ($request_method = OPTIONS ) {
            add_header "Access-Control-Allow-Origin"  *;
            add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
            add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
            return 204;
        }
    }
}

And forward the ports to the defaults on localhost:

ssh -L 8899:10.3.141.135:8891 -L 8900:10.3.141.135:8901 -N -f root@10.3.141.135

Note that if you’re connecting through the tunnel to localhost, you may encounter the error:

Access to XMLHttpRequest at 'http://localhost:8899/' from origin 'http://10.3.141.135' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `local`.

Chrome has implemented CORS-RFC1918, which prevents public network resources from requesting private-network resources. We can disable it at chrome://flags/#block-insecure-private-network-request link for Chrome.

Chrome set block insecure private network requests to disable

The best way we found to make this work for development while not interfering with security of our normal browsing experience by accident, is to install Chromium. That is, if you’re not using it normally.

\open -n ~/Downloads/chrome-mac/Chromium.app --args --disable-web-security

Exposing a service on a path instead of a port

A note for completion, for SSL supported sites we don’t usually want to mix content and use a path next to our static assets instead of exposing a service on a port. We can add to the default site a new Nginx location to point to /api/ (with trailing slash):

	location /api/ {
		proxy_pass http://127.0.0.1:8081/;
	}

The full config looks then as follows:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/html;
        error_page 404 =200 /index.html;
        server_name _;

        location /api/ {
                proxy_pass http://127.0.0.1:8081/;
        }

        location / {
                try_files $uri $uri/ =404;
        }
}

Last modified on Saturday, Jun 18, 2022.
Go back