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
andsecure_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 theUser-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, hereenigma
.
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 -I 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 -I 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 -I 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_addr
enigma
– through three commands:
- The
openssl
md5
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 -I --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 -I --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 -I --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 -I --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 to discuss your use cases.