5

Standard practice for RTMP is still to have a plain text stream key out on the wires.

I want to acccept RTMPS streams from encoders to NGINX however the RTMP module does not yet have RTMPS.

I'm not interested in all the relay solutions to allow taking an RTMP stream and sending to a place like facebook over RTMPS because the same security flaw is still there because at some point you are passing the keys over plain text.

My question is where can I find the reference specs on RTMPS? I'd like to know what keys are needed to make a proper handshake between an RTMPS source such as OBS and NGINX and then I will use the connection with the RTMP module. Can normal keys and an authority like Let's Encrypt be used on a server so that it can make the handshake with a RTMPS encoder?

I've seen stunnel used to wrap RTMP in TLS. Is it possible to do the reverse -- use stunnel to receive RTMPS and convert back to RTMP for the RTMP module?

johnsonjp34
  • 183
  • 1
  • 1
  • 6

4 Answers4

7

Since NGINX is capable of terminating TLS for upstream TCP servers, this should take care of it, using only NGINX (simply added stream section to config from @Esa Jokinen):

stream {
    upstream backend {
        server 127.0.0.1:1936;
    }
    server {
        listen 1935 ssl;
        proxy_pass backend;
        ssl_certificate /etc/letsencrypt/live/rtmp.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/rtmp.example.com/privkey.pem;
    }
}

rtmp {
    server {
        listen 127.0.0.1:1936;
        chunk_size 4096;

        application app-secret-stream-key {
            live on;
            record off;
            allow publish 127.0.0.1;  # for streaming through stunnel
            allow play 127.0.0.1;     # for the pull from /live
        }

        application live {
            live on;
            record off;
            deny publish all;         # no need to publish on /live
            allow play all;           # playing allowed

            pull rtmp://127.0.0.1:1936/app-secret-stream-key;
        }
    }
}

http {
    server {
        listen 80;
        server_name rtmp.example.com;

        location ^~ /.well-known/acme-challenge/ {
            root /var/www/letsencrypt;
        }
        location / {
            return 404;
        }
    }
}
Esa Jokinen
  • 46,944
  • 3
  • 83
  • 129
Danila Vershinin
  • 5,286
  • 5
  • 17
  • 21
  • I just fixed some details to make the configuration more coherent. It now uses the Let's Encrypt certificates that the HTTP part of the configuration is for, and the backend name was different on the `upstream{}` than on the `server{}`. Elegent solution without stunnel, indeed! – Esa Jokinen May 30 '20 at 16:46
6

Nginx RTMPS + secret publishing key + IP address based access control

I decided to post this as another answer, as my first answer is still a good explanatory answer to keep, and I also wanted to give credits to Danila Vershinin for pointing out using Nginx's stream{}. However, while both these answers increases security by encrypting the contents including the key, they also remove the ability of access control using allow/deny [play|publish] address|subnet|all of the rtmp{} module.

The stream{} i.e. proxied TCP has own access control, but (unlike rtmp{}) it can't distinguish publishing from playing: with a single stream{} proxy everyone can both publish & play – or is denied from doing neither one. Therefore, an access control using both keys and IP restrictions requires a structure with separate proxies for both publishing and streaming: a separete TCP port for proxying the publishing with the key. The following diagram demonstrates this design:

enter image description here

Here, I use the standard port 1935/tcp for the RTMPS-play and an additional 1936/tcp for the RTMPS-publish. For the internal unencrypted RTMP connections I use similar ports 19351 and 19361. The red color represents unencrypted connections & untrusted networks, whereas the green color represents encrypted connections & trusted networks.

The proxied TCP now has two (RTMPS) configurations, but both can still use the same certificate:

stream {
    upstream publish {
        server 127.0.0.1:19361;
    }
    server {
        listen 1936 ssl;        # additional port for publishing
        proxy_pass publish;
        ssl_certificate /etc/letsencrypt/live/rtmp.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/rtmp.example.com/privkey.pem;

        allow 192.0.2.1;        # allow publish from this IP
        allow 192.0.2.0/24;     # -- also supports CIDR notation!
        deny all;               # deny publish from the rest
    }

    upstream live {
        server 127.0.0.1:19351;
    }
    server {
        listen 1935 ssl;        # standard RTMP(S) port
        proxy_pass live;
        ssl_certificate /etc/letsencrypt/live/rtmp.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/rtmp.example.com/privkey.pem;

        allow all;              # this is public (this is also the default)
    }
}

Likewise, we now need two separate (local loopback) RTMP servers for each application:

rtmp {
    server {
        listen 127.0.0.1:19361;
        chunk_size 4096;

        application secret-key {
            live on;
            record off;
            allow publish 127.0.0.1;  # publishing through rtmps://rtmp.example.com:1936
            allow play 127.0.0.1;     # for the pull from rtmp://localhost:19351/live
        }
    }

    server {
        listen 127.0.0.1:19351;
        chunk_size 4096;

        application live {
            live on;
            record off;
            deny publish all;         # no need to publish on /live -- IMPORTANT!!!
            allow play 127.0.0.1;     # playing through rtmps://rtmp.example.com:1935/live

            pull rtmp://127.0.0.1:19361/secret-key;
        }
    }
}

The actual IP based access control is done on the stream{} section, so only the deny publish all; is mandatory for preventing direct publishing using the /live application. I've added the allow directives to the rtmp{} section just to clarify (and comment on) the default behaviour of the RTMP access control.

Esa Jokinen
  • 46,944
  • 3
  • 83
  • 129
  • This was very helpful to me. Would yo be kind enough to suggest where to use exec ffmpeg for hls_variant? I tried putting it the secret-key app but it results in the stream not getting recorded/saved at all. – Sheikh Azad May 02 '21 at 02:50
  • I was able to resolve it. The push directive was commented – Sheikh Azad May 02 '21 at 03:01
  • I have above setup, but fail to publish with `ffmpeg -re -i "A.mkv" -f flv rtmps://localhost:1936/live/` or `ffmpeg -re -i "A.mkv" -f flv https://localhost:1936/live/`, although `ffmpeg -re -i "A.mkv" -f flv rtmp://localhost:19361/secret-key` working fine. Any hint what may be wrong? – Manthan Tilva May 04 '23 at 06:11
4

UPDATE: This is my original answer that describes pretty well the issues one may face while implementing RTMPS with Nginx. However, I've added an improved version for more fine-tuned access control, and I recommend using the configuration from it, instead.


Yes, this is possible with stunnel, as RTMPS is just a RTMP session wrapped inside a standard TLS session. The examples on the Internet are mostly RTMP→RTMPS i.e. the stunnel is working as a plain text server and TLS client, which is configured with client = yes. Without that, the client defaults to no, which is the server mode.

The stunnel configuration could look like this:

[rtmps]
accept = 1935
connect = 127.0.0.1:1936
cert=/etc/letsencrypt/live/rtmp.example.com/fullchain.pem
key=/etc/letsencrypt/live/rtmp.example.com/privkey.pem

With this:

  • The Nginx should be listening for RTMP on local loopback, port 1936/tcp.
  • As you can't renew the Let's Encrypt ertificate using RTMP, you might need a HTTP server block for the HTTP-01 challenge, too.
  • As the connection to Nginx always comes from the stunnel i.e. from the 127.0.0.1, you can't use the allow/deny directives to limit connection based on IP addresses anymore. This means your access control would be limited to the key, alone, but at the same time it's less of a problem, as it's transmitted encrypted.

    However, this still causes problems, as you'd push the stream from the same IP than the clients that are using it, but you can't allow them to publish to your stream. Luckily, you don't have to push the stream from the application with the key, but you can also pull it from the public application (/live).

The following Nginx example configuration takes these considerations into account:

rtmp {
    server {
        listen 127.0.0.1:1936;
        chunk_size 4096;

        application app-secret-stream-key {
            live on;
            record off;
            allow publish 127.0.0.1;  # for streaming through stunnel
            allow play 127.0.0.1;     # for the pull from /live
        }

        application live {
            live on;
            record off;
            deny publish all;         # no need to publish on /live
            allow play all;           # playing allowed

            pull rtmp://127.0.0.1:1936/app-secret-stream-key;
        }
    }
}

http {
    server {
        listen 80;
        server_name rtmp.example.com;

        location ^~ /.well-known/acme-challenge/ {
            root /var/www/letsencrypt;
        }
        location / {
            return 404;
        }
    }
}

However, this is just an example, so you can and should modify it to fit your exact needs. (Also, I haven't tested this configuration but written it solely based on the documentation, so please feel free to correct, if I got something wrong.)

Esa Jokinen
  • 46,944
  • 3
  • 83
  • 129
  • 1
    I believe all the same can be done with NGINX alone (`stream { } ` with TLS config). – Danila Vershinin May 30 '20 at 14:52
  • 1
    If you have some time, please provide this alternative solution as an answer. I'd upvote it for sure! – Esa Jokinen May 30 '20 at 15:02
  • 1
    Don't know if it's worthwhile a separate answer, but I've added it. – Danila Vershinin May 30 '20 at 15:55
  • I've added another answer, an improved version using @DanilaVershinin's idea of using `stream{}`, but with an ability for IP based access control preventing publishing from anywhere. I hope @johnsonjp34 could choose it as the accepted answer, as it wraps everything up. – Esa Jokinen Jun 06 '20 at 12:52
2

Not sure if this is valid here but I used as simple SSL Termination as above with proxy protocol to keep client IP Addresses. The RTMP module supports proxy protocol so you can still allow deny by IP address. Done some basic tests and it seems to work just fine, correct messages in the log and deny by IP address worked to.

Maybe I have miss understood but it seems to be doing what I think it should be, i.e. allowing SSL connections via RTMPS and keeping the client IP addresses for access control in the RTMP module.

My stream block looks like this:

stream {

    upstream backend {
        server 127.0.0.1:1935;
    }

    server {
        listen 1936 ssl;
        proxy_pass backend;
        proxy_protocol on;
        ssl_certificate /etc/..../fullchain.pem;
        ssl_certificate_key /etc/.../privkey.pem;
    }
}

And the relevant bits of my RTMP Section: (Note: Direct connections probably wont work but not tried it...suggests this in the blog (see below) but I don't need direct connections).

rtmp {
    server {

        listen 1935 proxy_protocol;
        chunk_size 4000;

     
        application stream_app{
            live on;
            .....
            .....
            .....
            }
        }
    }
}

You can read about it in the RTMP blog http://nginx-rtmp.blogspot.com.

Hope it helps someone and thanks for the answers above as they pointed me in the right direction.

FYI I deny play all as I am pushing out HLS, don't need play or anything else at the moment.

Dave M
  • 4,514
  • 22
  • 31
  • 30
Code Rider
  • 21
  • 1
  • This is much easier to understand than the other answers. Added an `allow publish x.x.x.x` with my IP address in rtmp > server > application, set ffmpeg to stream to `rtmps://example.com:1936/x` and it just works – Dark Mar 19 '22 at 17:34