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:
- Redirect directly to the cloud content.
- Proxy the content from the web app directly.
- 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.
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.
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.
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.