Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Secured HLS setup with Nginx as media server

Secured HLS setup with Nginx as media server

This example is part of this article.

This is an example for an HLS delivery with basic security. Nginx compiled with nginx-rtmp-module & secure-link is used as media server. Features:

  • Domain filtering
  • Referrer filtering
  • Embed buster
  • Session token for playlist, segments and AES keys
  • AES encryption
  • HTTPS only

Throughout this example the host is assumed to be example.com. if you want to use this configurations, be sure to replace all instances of example.com with your domain.

Compiling Nginx

# install deps (Ubuntu)
sudo apt-get install -y build-essential libpcre3 libpcre3-dev libssl-dev

wget http://nginx.org/download/nginx-1.10.1.tar.gz
tar -xf nginx-1.10.1.tar.gz
cd nginx-1.10.1

./configure --with-http_ssl_module --add-module=../nginx-rtmp-module --with-http_secure_link_module

make -j
sudo make install
# nginx is now installed in /usr/local/nginx

Pushing video to Nginx

In order to push video to nginx i'm going to use ffmpeg which well supports RTMP as its output. I'm going to create an I frame roughly every 2 seconds which will allow nginx to achieve the 4s segment target. For simplicity i'll be using a static mp4 file and ingest it in infinite loop.

ffmpeg -hide_banner \
-stream_loop -1 \
-re -i test-video.mp4 \
-c:a aac -c:v h264 -g 48 \
-f flv rtmp://localhost:1935/show/live

I'm using live as the stream name, the output hls will carry that same name - e.g. live.m3u8.

Generating Session token

The session token is based on this format (note the spaces): MD5("EXPIREY_DATE_IN_SECONDS CLIENT_IP_ADDRESS SECRET")

here are several examples of generating the token:

BASH

get_customer_url() {
  local IP=${1:-127.0.0.1}
  local SECRET=${2:-VERY_COOL_SECRET}
  local EXPIRES="$(date -d "today + 30 minutes" +%s)";
  local token="$(echo -n "${EXPIRES} ${IP} ${SECRET}" | openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =)"
  echo "https://example.com/video/hls/${token}/${EXPIRES}/live.m3u8"
}

get_customer_url 10.20.1.55 "uigfp(@#tfpIUDGPFiouGDF"

Node.JS (Javascript)

var crypto = require('crypto');

function generateSecurePathHash(expires, client_ip, secret) {
    if (!expires || !client_ip || !secret) throw new Error('Must provide all token components');

    var input = expires + ' ' + 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, '_');
}

function getStreamUrl(ip, secret) {
    const expiresTimestamp = new Date(Date.now() + (1000 * 60 * 30)).getTime();
    const expires = String(Math.round(expiresTimestamp / 1000));
    
    const token = generateSecurePathHash(expires, ip, secret);

    return `https://example.com/video/hls/${token}/${expires}/live.m3u8`;
}

getStreamUrl('127.0.0.1', 'uigfp(@#tfpIUDGPFiouGDF');
// https://example.com/video/hls/LdS-kcC-JGVHGNTFlX-6Sw/1526373776/live.m3u8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>example.com</title>
<script>
if (top != self) {
top.location.replace(self.location.href);
}
</script>
<script src="//cdn.jsdelivr.net/clappr/latest/clappr.min.js"></script>
</head>
<body>
<div id="player" style="width:640px"></div>
<script>
var player = new window.Clappr.Player({
// this is an example url - for this to work you'll need to generate fresh token
source: 'https://example.com/video/hls/CIfZTIu8ygWzhXBSoQfPIQ/1526373226/live.m3u8',
parentId: '#player'
});
</script>
</body>
</html>
worker_processes auto;
events {
worker_connections 4096;
}
# RTMP configuration
rtmp {
server {
listen 1935; # Listen on standard RTMP port
chunk_size 4000;
application show {
live on;
# Turn on HLS
hls on;
hls_path /mnt/hls/;
hls_fragment 4;
hls_playlist_length 60;
# Setup AES encryption
hls_keys on;
hls_key_path /mnt/hls/keys;
hls_key_url keys/;
hls_fragments_per_key 10;
# disable consuming the stream from nginx as rtmp
deny play all;
}
}
}
http {
sendfile off;
tcp_nopush on;
directio 512;
default_type application/octet-stream;
access_log off;
error_log off;
# HTTPS certificate and key
ssl_certificate ssl/example.com.cert;
ssl_certificate_key ssl/example.com.key;
server {
listen 443 ssl;
server_name example.com;
root /mnt/;
# Disable cache
add_header 'Cache-Control' 'no-cache';
index index.html;
default_type "text/html";
types {
application/dash+xml mpd;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
plain/text key;
}
location =/ {
# CORS setup
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length' always;
add_header 'X-Frame-Options' 'DENY' always;
}
location /video {
rewrite /hls/([a-zA-Z0-9_\-]*)/([0-9]*)/(.*)\.(ts|m3u8|key)$ /hls/$3.$4?token=$1&expires=$2;
root /mnt/not-exist;
}
location /hls {
internal;
# The secure link is base on the folowing format
# MD5("EXPIREY_DATE_IN_SECONDS CLIENT_IP_ADDRESS SECRET")
# here is a BASH function that generates a secure link
# get_token() {
# local expires="$(date -d "today + 30 minutes" +%s)";
# local token="$(echo -n "${expires} 127.0.0.1 VERY_COOL_SECRET" | openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =)"
# echo "${token}/${expires}"
# }
# echo "https://example.com/video/hls/$(get_token)/live.m3u8"
secure_link $arg_token,$arg_expires;
secure_link_md5 "$secure_link_expires $remote_addr VERY_COOL_SECRET";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
# Referrer protection
valid_referers server_names;
if ($invalid_referer) {
return 403;
}
# allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://example.com';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
server {
listen 443 ssl default;
server_name _;
return 444;
}
}
@comerciosite

This comment has been minimized.

Copy link

@comerciosite comerciosite commented Mar 19, 2019

Hello

I tried to install on ubuntu 16 or 18 on digitalocean, this one giving error.

Can you check if you have an update?

awaiting

Thank you very much in advance

@inat-tv

This comment has been minimized.

Copy link

@inat-tv inat-tv commented Apr 8, 2019

buddy has errors in installation

@inat-tv

This comment has been minimized.

Copy link

@inat-tv inat-tv commented Apr 8, 2019

./configure: error: no ../nginx-rtmp-module/config was found

@ajhansen91

This comment has been minimized.

Copy link

@ajhansen91 ajhansen91 commented Apr 22, 2019

Any help with nginx.conf using rtmps url and stream key for facebook live . I am using windows as well. very novice and really need some help it stopped working and use this for my streams and have been crippled due to this issue. I have been looking into ssl certificates for nginx but getting no where and came across this saying you can basically loop it to an RTMP to my understanding to repush to be able to brodcast live using nginx for multiple platforms.

@clovrd

This comment has been minimized.

Copy link

@clovrd clovrd commented Oct 25, 2019

Works like charm, thx. There's a little err in the node example though. It has to be
var input = expires + ' ' + client_ip + ' ' + secret;
otherwise the token will not be correct.

Cheers

@elmadkoun

This comment has been minimized.

Copy link

@elmadkoun elmadkoun commented Dec 17, 2019

Thanks for the explanation
How do I run bash in order to create token pleas

@luismanolo

This comment has been minimized.

Copy link

@luismanolo luismanolo commented Dec 18, 2019

Good work.

Many thanks.

@seanbranagh

This comment has been minimized.

Copy link

@seanbranagh seanbranagh commented Apr 7, 2020

Thank you, This works but what if I want to have a short expiry (say a couple of minutes) on the token but I want the HLS to keep running beyond this if it has been started already?
Can I secure the m3u8 file only?

@blindscan

This comment has been minimized.

Copy link

@blindscan blindscan commented May 7, 2020

Hi
I have nginx 1.7.4 on port 8880 and it works fine using the default configuration
is not working with Secured HLS setup
I generate the token using bash but the url is not working in jwplayer or in jsdelivr.net player. The hls files are generated in the right directory- /mnt/hls - live.m3u8 and live-*.ts files
Should work with my nginx version and on port 8880 not secure?

nginx version: nginx/1.7.4
built by gcc 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC)
TLS SNI support enabled
configure arguments: --with-http_stub_status_module --with-http_realip_module --with-http_secure_link_module --with-http_ssl_module --add-module=../nginx-rtmp-module-master

bash -x script

What I did wrong ?

Thank you

@blindscan

This comment has been minimized.

Copy link

@blindscan blindscan commented May 7, 2020

Thanks for the explanation
How do I run bash in order to create token pleas

bash -x script

@michaeltoohig

This comment has been minimized.

Copy link

@michaeltoohig michaeltoohig commented Aug 26, 2020

Why did you choose to have the HLS URL point to /video then rewrite it to /hls with the token and expire appended to the query string? Why not directly write the token and expire timestamp directly such as https://example.com/hls/live.m3u8?token=foo&expire=1234.

I noticed the /hls location is internal so perhaps that has a part to do with it or were you just following the pattern as described in the secure link documentation?

@iamandi

This comment has been minimized.

Copy link

@iamandi iamandi commented Oct 10, 2020

./configure: error: no ../nginx-rtmp-module/config was found

You first need to install nginx-rtmp module. https://github.com/arut/nginx-rtmp-module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.