Securing URLs with the Secure Link Module in NGINX and NGINX Plus

Both the open source NGINX software and NGINX Plus are very secure and reliable as web servers, reverse proxies, and caches for your content. For additional protection against access by unauthorized clients, you can use directives from the Secure Link module to require that clients include a specific hashed string in the URL of the asset they are requesting.

In this blog post, we will discuss how to configure the two methods implemented in the Secure Link module. The sample configuration snippets protect HTML and media playlist files, but can be applied to any type of HTTP URL. The methods apply to both NGINX and NGINX Plus, but for the sake of brevity we’ll refer only to NGINX Plus for the rest of the blog.

An Overview of the Methods in the Secure Link Module

The Secure Link module verifies the validity of a requested resource by comparing an encoded string in the URL of the HTTP request with the string it computes for that request. If a link has a limited lifetime and the time has expired, the link is considered outdated. The status of these checks is captured in the $secure_link variable and used to control the flow of processing.

As mentioned, the module provides two methods. Only one of them can be configured in a given http, server, or location context.

  • The first and simpler mode is enabled by the secure_link_secret directive. The encoded string is an MD5 hash computed on the concatenation of two text strings: the final part of the URL and a secret word defined in the NGINX Plus configuration. (For specifics about the first text string, see Using Basic Secured URLs.)

    To access the protected resource, the client must include the hash right after the URL prefix, which is an arbitrary string without any slashes. In this sample URL, the prefix is videos and the protected resource is the file bunny.m3u8:

    /videos/80e2dfecb5f54513ad4e2e6217d36fd4/hls/bunny.m3u8

    One use case for this method is when a user uploads an image or document to a server for sharing but wants to prevent anyone who knows the filename from accessing it until the official link is published.

  • The second, more flexible, method is enabled by the secure_link and secure_link_md5 directives. Here the encoded string is an MD5 hash of variables defined in the NGINX Plus configuration file. Most commonly, the $remote_addr variable is included to restrict access to a particular client IP address, but you can use other values, for example $http_user_agent, which captures the User-Agent header and so restricts access to certain browsers.

    Optionally, you can specify an expiration date after which the URL no longer works even if the hash is correct.

    The client must append the md5 argument to the request URL to specify the hash. If an expiration date is included in the string that is hashed, the client also must append the expires argument to specify the date, as in this sample URL for requesting the protected file pricelist.html:

    /files/pricelist.html?md5=AUEnXC7T-Tfv9WLsWbf-mw&expires=1483228740

The Secure Link module is included in prebuilt open source NGINX binaries from nginx.org, the NGINX packages provided by operating system vendors, and in NGINX Plus. It is not included by default when you build NGINX from source; enable it by including the --with-http_secure_link_module argument to the configure command.

Using Basic Secured URLs

The more basic way to secure URLs is with the secure_link_secret directive. In the following sample snippet, we secure an HTTP Live Streaming (HLS) media playlist file named /bunny.m3u8. It’s stored in the /opt/secure/hls directory, but is exposed to clients using a URL that starts with the videos prefix.

server {
listen 80;
server_name secure-link-demo;

location /videos {
secure_link_secret enigma;
if ($secure_link = "") { return 403; }

rewrite ^ /secure/$secure_link;
}

location /secure {
internal;
root /opt;
}
}

With this configuration, to access the file /opt/secure/hls/bunny.m3u8 clients must present the following URL:

/videos/80e2dfecb5f54513ad4e2e6217d36fd4/hls/bunny.m3u8

The hashed string comes right after the prefix, which is an arbitrary string without any slashes (here, videos).

The hash is computed on a text string that concatenates two elements:

  • The part of the URL that follows the hash, here hls/bunny.m3u8.
  • The parameter to the secure_link_secret directive, here enigma.

If the client’s request URL does not have the correct hash, NGINX Plus sets the $secure_link variable to the empty string. The if test fails and NGINX Plus returns the 403 Forbidden status code in the HTTP response.

Otherwise (meaning the hash is correct), the rewrite directive rewrites the URL – in our example to /secure/hls/bunny.m3u8 (the $secure_link variable captures the part of the URL that follows the hash). URLs beginning with /secure are handled by the second location block. The root directive in that block sets /opt as the root directory for requested files and the internal directive specifies that the block is used only for internally generated requests.

Generating the Hash on the Client for a Basic Secured URL

To obtain the MD5 hash in hexadecimal format that the client must include in the URL, we run the openssl md5 command with the -hex option:

# echo -n 'hls/bunny.m3u8enigma' | openssl md5 -hex
(stdin)= 80e2dfecb5f54513ad4e2e6217d36fd4

For a discussion of generating hashes programmatically, see Generating the Hash Programatically.

Server Response to Basic Secured URLs

The following sample curl commands show how the server responds to different secured URLs.

If the URL includes the correct MD5 hash, the response is 200 OK:

# curl -Is http://secure-link-demo/videos/80e2dfecb5f54513ad4e2e6217d36fd4/hls/bunny.m3u8 | head -n 1
HTTP/1.1 200 OK

If the MD5 hash is incorrect, the response is 403 Forbidden:

# curl -Is http://secure-link-demo/videos/2c5e80de986b6fc80dd33e16cf824123/hls/bunny.m3u8 | head -n 1
HTTP/1.1 403 Forbidden

If the hash for bunny.m3u8 is used for a different file, the response is also 403 Forbidden:

# curl -Is http://secure-link-demo/videos/80e2dfecb5f54513ad4e2e6217d36fd4/hs/oven.m3u8 | head -n 1
HTTP/1.1 403 Forbidden

Using Secured URLs that Expire

The more flexible method for securing URLs uses the secure_link and secure_link_md5 directives. In this example, we use them to allow access to the /var/www/files/pricelist.html file only from clients on IP address 192.168.33.14 and only through December 31, 2016.

Our virtual server listens on port 80 and handles all secured HTTP requests under the location/files block, where the root directive sets /var/www as the root directory for requested files.

The secure_link directive defines two variables that capture arguments in the request URL: $arg_md5 is set to the value of the md5 argument, and $arg_expires to the value of the expires argument.

The secure_link_md5 directive defines the expression that is hashed to generate the MD5 value for the request; during URL processing, the hash is compared to the value of $arg_md5. The sample expression here includes the expiration time passed in the request (captured in the $secure_link_expires variable), the URL ($uri), the client IP address ($remote_addr), and the word enigma.

server {
listen 80;
server_name secure-link-demo;

location /files {
root /var/www;
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri$remote_addr enigma";

if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
}
}

With this configuration, to access /var/www/files/pricelist.html, a client with IP address 192.168.33.14 must send this request URL before Sat Dec 31 23:59:00 UTC 2016:

/files/pricelist.html?md5=AUEnXC7T-Tfv9WLsWbf-mw&expires=1483228740

If the hash in the URL sent by the client (captured in the $arg_md5 variable) does not match the hash calculated from the secure_link_md5 directive, NGINX Plus sets the $secure_link variable to the empty string. The if test fails and NGINX Plus returns the 403 Forbidden status code in the HTTP response.

If the hashes match but the link has expired, NGINX Plus sets the $secure_link variable to 0; again the if test fails but this time NGINX Plus returns the 410 Gone status code in the HTTP response.

Generating the Hash and Expiration Time on a Client

Now let’s see how a client calculates the md5 and expires arguments to include in the URL.

The first step is to determine the Unix time equivalent of the expiration date, because that value is included in the hashed expression in the form of the $secure_link_expires variable. To obtain the Unix time – the number of seconds since Epoch (1970-01-01 00:00:00 UTC) – we use the date command with the -d option and the +%s format specifier.

In our example we’re setting the expiration time to Sat Dec 31 23:59:00 UTC 2016, so the command is:

# date -d "2016-12-31 23:59" +%s
1483228740

The client includes this value as the expires=1483228740 argument in the request URL.

Now we run the string defined by the secure_link_md5 directive – $secure_link_expires$uri$remote_addrenigma – through three commands:

  • The opensslmd5 command with the -binary option generates the MD5 hash in binary format.
  • The openssl base64 command applies Base64 encoding to the hashed value.
  • The tr commands replace the plus sign ( + ) with the hyphen ( - ) and the slash ( / ) with the underscore ( _ ), and delete the equal sign (=) from the encoded value.

For our example, the complete command is:

# echo -n '1483228740/files/pricelist.html192.168.33.14 enigma' | openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =
AUEnXC7T-Tfv9WLsWbf-mw

The client includes this value as the md5=AUEnXC7T-Tfv9WLsWbf-mw argument in the request URL.

Generating the Hash Programmatically

If your NGINX Plus web server is serving dynamic content from an application server, both NGINX Plus and the application server need to use the same secured URL. You can generate the hash for the md5 argument in the URL programmatically. The following Node.js function generates a hash matching the one defined in the NGINX Plus config snippet above. It takes an expiration time, URL, client IP address, and secret word as arguments and returns the Base64‑encoded binary‑format MD5 hash.

var crypto = require("crypto");

function generateSecurePathHash(expires, url, client_ip, secret) {
if (!expires || !url || !client_ip || !secret) {
return undefined;
}

var input = expires + url + client_ip + " " + secret;
var binaryHash = crypto.createHash("md5").update(input).digest();
var base64Value = new Buffer(binaryHash).toString('base64');
return base64Value.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

To calculate the hash for our current example, we pass in these arguments:

generateSecurePathHash(new Date('12/31/2016 23:59:00').getTime()), '/files/pricelist.html', “192.168.33.14”, "enigma");

Server Response to Secured URLs with Expiration Times

The following sample curl commands show how the server responds to secured URLs.

If a client with IP address 192.168.33.14 includes the correct MD5 hash and expiration time, the response is 200 OK:

# curl -Is --interface "192.168.33.14" 'http://secure-link-demo/files/pricelist.html?md5=AUEnXC7T-Tfv9WLsWbf-mw&expires=1483228740' | head -n 1
HTTP/1.1 200 OK

If a client with a different IP address sends the same URL, the response is 403 Forbidden:

# curl -Is --interface "192.168.33.33" 'http://secure-link-demo/files/pricelist.html?md5=AUEnXC7T-Tfv9WLsWbf-mw&expires=1483228740' | head -n 1
HTTP/1.1 403 Forbidden

If the hash value of the md5 argument is incorrect, the response is 403 Forbidden:

# curl -Is --interface "192.168.33.14" 'http://secure-link-demo/files/pricelist.html?md5=qeUNjiY2FTIVMaXUsxG-7w&expires=1483228740' | head -n 1
HTTP/1.1 403 Forbidden

If the URL has expired (the date represented by the expires argument is in the past), the response is 410 Gone:

# curl -Is --interface "192.168.33.14" 'http://secure-link-demo/files/pricelist.html?md5=Z2rNva2InyVcRTlhqAkT4Q&expires=1467417540' | head -n 1
HTTP/1.1 410 Gone

Example – Securing Segment Files with an Expiration Date

Here’s another example of a secured URL with expiration date, used to protect both the playlist for a media asset and the segment files.

One difference from the preceding example is that we add a map configuration block here to remove the extension from the playlist (.m3u8 file) and from the HLS segments (.ts files) as we capture the filename in the $file_name variable, which gets passed to the secure_link_md5 directive. This serves to secure requests for the individual .ts segments as well as for the playlist.

Another difference from the first example is that we include the $http_user_agent variable (which captures the User-Agent header) in the secure_link_md5 directive, to restrict access to clients on specific web browsers (for example, to have the URL work on Safari but not on Chrome or Firefox).

map $uri $file_name {
default none;
"~*/s/(?<name>.*).m3u8" $name;
"~*/s/(?<name>.*).ts" $name;
}

server {
listen 80;
server_name secure-link-demo;

location /s {
root /opt;
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$file_name$http_user_agent enigma";

if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
}
}

Summary

The Secure Link module in NGINX enables you to protect files from unauthorized access by adding encoded data like the hash of a specific part of the URL. Adding an expiration time also limits how long links are valid, for even greater security.

To try NGINX Plus, start your free 30-day trial today or contact us for a live demo.

Cover image
Are your applications secure?
Learn how to protect your apps with NGINX and NGINX Plus