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

NGINX | July 29, 2016

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.

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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:

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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:

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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.

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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:

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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:

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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.

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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:

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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).

[@portabletext/react] Unknown block type "codeBlock", specify a component for it in the `components.types` prop

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.


Share

About the Author

Kunal Pariani
Kunal ParianiTechnical Solutions Architect

More blogs by Kunal Pariani

Related Blog Posts

Automating Certificate Management in a Kubernetes Environment
NGINX | 10/05/2022

Automating Certificate Management in a Kubernetes Environment

Simplify cert management by providing unique, automatically renewed and updated certificates to your endpoints.

Secure Your API Gateway with NGINX App Protect WAF
NGINX | 05/26/2022

Secure Your API Gateway with NGINX App Protect WAF

As monoliths move to microservices, applications are developed faster than ever. Speed is necessary to stay competitive and APIs sit at the front of these rapid modernization efforts. But the popularity of APIs for application modernization has significant implications for app security.

How Do I Choose? API Gateway vs. Ingress Controller vs. Service Mesh
NGINX | 12/09/2021

How Do I Choose? API Gateway vs. Ingress Controller vs. Service Mesh

When you need an API gateway in Kubernetes, how do you choose among API gateway vs. Ingress controller vs. service mesh? We guide you through the decision, with sample scenarios for north-south and east-west API traffic, plus use cases where an API gateway is the right tool.

Deploying NGINX as an API Gateway, Part 2: Protecting Backend Services
NGINX | 01/20/2021

Deploying NGINX as an API Gateway, Part 2: Protecting Backend Services

In the second post in our API gateway series, Liam shows you how to batten down the hatches on your API services. You can use rate limiting, access restrictions, request size limits, and request body validation to frustrate illegitimate or overly burdensome requests.

New Joomla Exploit CVE-2015-8562
NGINX | 12/15/2015

New Joomla Exploit CVE-2015-8562

Read about the new zero day exploit in Joomla and see the NGINX configuration for how to apply a fix in NGINX or NGINX Plus.

Why Do I See “Welcome to nginx!” on My Favorite Website?
NGINX | 01/01/2014

Why Do I See “Welcome to nginx!” on My Favorite Website?

The ‘Welcome to NGINX!’ page is presented when NGINX web server software is installed on a computer but has not finished configuring

Deliver and Secure Every App
F5 application delivery and security solutions are built to ensure that every app and API deployed anywhere is fast, available, and secure. Learn how we can partner to deliver exceptional experiences every time.
Connect With Us