Published in Dev Ops
Using NGINX as a Reverse Proxy to Protect Cloud Storage Resources

Using NGINX as a Reverse Proxy to Protect Cloud Storage Resources

Protect secure resources on a cloud storage service by integrating your current app and NGINX.

Let me first describe the problem. Let’s say you have user-uploaded content stored on a cloud storage service. Only authorized users can have access to this content. How do you implement this?

There are several options:

  1. Redirect directly to the cloud content.
  2. Proxy the content from the web app directly.
  3. Use NGINX as a reverse proxy.

If you like, you can skip right to the implementation.

Option 1 - Redirect directly to the cloud content

Redirecting directly to the cloud content is the most straightforward implementation; your web app can authenticate the user and then redirect the request to the content on the cloud storage.

sequenceDiagram Client->>App: Authenticate & Authorize User App-->>Client: 302 Redirect To Cloud Resource Client->>Cloud Storage: Request Content Cloud Storage-->>Client: Resource Data or Chunk

Pros

  • Easy to implement.
  • Don’t require any infrastructure changes.

Cons

  • The redirect is not secure. You can implement signed URLs and set an expiration date. However, if you are serving large content, like videos, you’ll need to ensure the expiration is high enough to support the content. If a user is denied access to the content they had access to, as long as the URL has not expired, they can bypass the authorization step.

Option 2 - Proxy the content from the web app directly

To ensure the content is secure, you could proxy the content from your app.

sequenceDiagram Client->>App: Authenticate & Authorize User App->>Cloud Storage: Request Content Cloud Storage-->>App: Resource Data or Chunk App-->>Client: Resource Data or Chunk

Pros

  • Don’t require any infrastructure changes.

Cons

The cons can depend on the web app language.

  • Some languages will download the entire file before sending it to the client; this could cause problems for large files.
  • Some languages will try to load the entire file in memory before sending it to the client; again, this could cause problems for large files.
  • You have to implement chunking yourself.

Option 3 - Use NGINX as a reverse proxy

Rather than redirect the browser client to the cloud storage, you can use an NGINX server as a proxy to keep the request internal so that the user is unaware of the cloud storage; all they see is the NGINX server.

A user requests a resource via the NGINX server. Then the NGINX server makes an HTTP request to your app to authenticate and authorize the user to access that resource. Your web app then, on success, returns a redirect URL to the resource on the cloud storage. The NGINX server then makes the HTTP requests to the cloud storage. Finally, the cloud storage sends the resource data back to the NGINX server so the NGINX server can relay it back to the user.

sequenceDiagram Client->>Reverse Proxy: Request Resource Reverse Proxy->>App: Authenticate & Authorize User App-->>Reverse Proxy: 305 Redirect To Cloud Resource Reverse Proxy->>Cloud Storage: Request Secure Resource Cloud Storage-->>Reverse Proxy: Resource Data or Chunk Reverse Proxy-->>Client: Resource Data or Chunk

Pros

  • Secure; all requests must go through your app to authenticate and be authorized.
  • Simple NGINX configuration; no requirement for external modules.
  • Can scale the resource management separately from the app.
  • There could be some performance gains by allowing NGINX to proxy the requests rather than the app.

Cons

  • Have to allocate another server type.
  • Have to learn NGINX and maintain another server configuration.
  • Might have to allocate another subdomain for the NGINX server.
  • You must get a wild card certificate for cross-domain cookies if the session is in a cookie and you use a separate subdomain.

Implementation

Note: I assume you already know about NGINX and how to configure the server. I’m also going to assume that you have broken your server configurations from the main nginx.config file.

The beauty of using the built-in functionality of NGINX is that the configuration is straightforward. We’re going to take advantage of two directives proxy_pass and error_page.

Authentication & Authorization

Let’s start with the first step of getting the user’s request authenticated and authorized.

server {
	...
	
	location / {
		...

		proxy_redirect off;
		proxy_pass https://www.example.com/auth/resource/;
	}
}

The proxy_redirect off directive will prevent NGINX from updating the Location HTTP header and keep the cloud storage URL intact.

Handle the redirect with NGINX

Now that we’re authenticating the request via the app, we need to ensure that NGINX and not the user handle the redirect to the cloud storage.

So let’s do that; let’s also have the app return a 305 (Use Proxy).

server {
	...
	
	location / {
		...
		proxy_redirect off;
		proxy_pass https://www.example.com/auth/resource/;

		proxy_intercept_errors on;	
		error_page 305 = @cloud_storage_redirect;	
	}

	location @cloud_storage_redirect {
		resolver 8.8.8.8;

		set $saved_redirect_location '$upstream_http_location';

		...
		proxy_pass $saved_redirect_location;
	 
	 	proxy_intercept_errors on;
	 	error_page 301 302 307 308 = @cloud_storage_redirect;
	}
}

So what is going on here?

The proxy_intercept_errors on directive tells NGINX that we want to allow the current location block to handle any 3XX requests returned by the proxied request.

The error_page 305 = @cloud_storage_redirect directive tells NGINX that we want the @cloud_storage_redirect location block to handle any HTTP 305 responses.

set $saved_redirect_location '$upstream_http_location' saves the HTTP Location header from the redirect response from the app. Passing the value to the proxy_pass directive; proxy_pass $saved_redirect_location.

Sometimes the cloud storage will respond with its redirect, so we need to ensure that NGINX handles them so as not to leak the storage URL. To prevent that, we can use the proxy_intercept_errors on and error_page 301 302 307 308 = @cloud_storage_redirect directives again to keep the request on the NGINX server.

You might have noticed the resolver 8.8.8.8 directive. I found this is needed as NGINX does not handle the DNS resolution correctly inside the error handling block. resolver 8.8.8.8 tells NGINX to use Google’s public DNS resolver.


That’s it. That is the basic configuration for using NGINX as a reverse proxy to secure your cloud storage resources.

See below for some useful things I learned and settings you may want to consider.


Authentication Conflicts

When I first set this up, I used AWS S3, and the app used cookies to store the user session ID. This setup created a conflict, and AWS returned a 403 (Unauthorized) response.

To fix it, I told NGINX to ignore any Authentication and Cookie headers when making the request to AWS.

server {
	...
	
	location @cloud_storage_redirect {
		...
		proxy_set_header Connection '';
	 	proxy_set_header Authorization '';
		proxy_hide_header Set-Cookie;
	 	proxy_ignore_headers Set-Cookie;
	 	proxy_hide_header WWW-Authorization;
	 	proxy_hide_header Authorization;
		...
	}
}

Send the User’s IP, not the App Server IP

For logging purposes, you may want to ensure that NGINX sends the User’s IP address, not the App server’s IP address.

server {
	...
	
	location @cloud_storage_redirect {
		...
		proxy_set_header X-Real-IP $remote_addr;
	 	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		...
	}
}

Secure the HTTP Request to the App Server

You can also secure the communication between NGINX and the app server. You can do this by sending a secret that only the two servers know.

server {
	...
	
	location / {
		...
		proxy_set_header X-Nginx-Secret 'SomeSecret';
		...	
	}
}

Note: See my blog on how to use environment variables in the NGINX configuration to keep the secret out of the plain text.


comments powered by Disqus