zuavra / nginx-ip-whitelister

Backend for ngx_http_auth_request_module that whitelists remote IP address if a key is presented in URL.
MIT License
47 stars 3 forks source link
connect docker docker-compose dockerfile javascript nginx nginx-proxy-manager ngx-http-auth-request node node-js

nginx-ip-whitelister

This is a Node.js server app that acts as a companion for an Nginx install configured as reverse proxy and compiled with the ngx_http_auth_request_module.

It validates proxy requests against a list of IP addresses. In order for an IP to be added to the list a key must be presented in the query string. Once whitelisted, each IP will be valid for a configurable amount of time.

This app was designed to be particularly easy to integrate with Nginx Proxy Manager running in Docker.

1. Security warning

1.1. Don't use this

⚠️ Never use this while on public WiFi at a cafe, hotel, airport, festival, mall etc., on a cellular connection (cell tower), or at work.

By granting access to your public IP in such a place you grant access to an entire building or even an entire area full of people.

In such situations you MUST use a secure tunnel (VPN/SSH) or an IAM provider that uses cookies over TLS.

1.2. There are better ways to secure your server

nginx-ip-whitelister was designed to be a security improvement over leaving your Emby/Jellyfin completely open to the whole Internet. That's not a very high bar.

There are better methods. If you've arrived to this page because you want to secure your server please read through the following comparisons first:

  1. Ask your friends to use a VPN like OpenVPN/WireGuard/Tailscale or an SSH tunnel on their PC, laptop, phone or tablet. VPN/SSH lets you access Emby/Jellyfin in your browser or from the Emby/Jellyfin apps.
    • Pros: they can use Jellyfin from the browser or 3rd party Emby/Jellyfin apps; excellent security; each friend uses their own acces key; they can safely use it anywhere, even at public venues or at a hotel.
    • Cons: can't cast to media devices (TV, Chromecast etc.)
  2. Ask friends to login to IAM provider like Authelia/Authentik or vouch-proxy in their browser.
    • Pros: also excellent security; can be used in public venues; easier to use than a VPN.
    • Cons: can't cast to media devices; can't use 3rd party Emby/Jellyfin apps (you can only access it in a browser).
  3. Set up a VPN/SSH tunnel client on your relative/friend's router and let the router handle the VPN stuff.
    • Pros: good security; lets people use any browser or 3rd party app; all devices on their LAN can connect so you can cast to media devices.
    • Cons: not portable, and the router needs to be a prosumer model to support VPN/SSH.
  4. Set up a VPN/SSH tunnel client on someone's phone and have them activate WiFi hotspot on the phone when they want to grant access to others. (If they don't want to share access then just see bullet 1, no need for hotspot.)
    • Pros: good security; can be used in public venues or at hotels; any device that connects to the hotspot can access Emby/Jellyfin from browsers or 3rd party apps, which means people can cast to media devices too.
    • Cons: more complicated for everybody involved; all devices need to connect to the hotspot to access Emby/Jellyfin; it will consume from the phone's cellular data plan; probably eats up the battery too.
  5. Use SSH to browse the files on your server remotely. Use a mobile player that can stream from SSH or a file explorer like Solid Explorer that has the ability to relay SSH files to any player app. Use an app like BubbleUPnP as the player app if you want to also cast to a LAN renderer.
    • Pros: good security; can be used in public venues or at hotels; can cast to media devices.
    • Cons: restricted to mobile phones and a specific combination of apps.
  6. nginx-ip-whitelister:
    • Pros: you can use it from browser or 3rd party apps; it lets you cast to media devices.
    • Cons: not as secure as the other options; MUST NOT be used from public venues, hotels etc.

In conclusion, it's true that nginx-ip-whitelister solves some problems, like allowing you to use Emby/Jellyfin apps or letting you cast to media devices; but granting access to whatever public IP you may be using at the time can and will backfire if you don't know what you're doing.

1.3. How do I mitigate the security risk?

In case you're still foolish enough to use this:

1.4. What to do if you suspect trouble

2. Prerequisites

In order to use nginx-ip-whitelister you must have already accomplished the things below:

It is beyond the scope of this documentation to explain how to achieve all this. For what it's worth I recommend using Nginx Proxy Manager because it makes some of the things above a lot easier.

3. How do I use it?

After completing the requirements and activating the whitelister in the proxy config, your Emby/Jellyfin install should show 403 errors to any visitor when accessed through the reverse proxy.

To make it work you need to use a link like this:

https://your.domain[:PORT]/?ACCESS-KEY[:TOTP]

This will record your current IP and allow it normal access for a period of time. If you're using someone's WiFi all the devices using it will have access too, meaning you can cast to local media devices, TVs etc.

If you want to stop allowing your IP before the timeout runs out use "LOGOUT" as a key:

https://your.domain[:PORT]/?LOGOUT

You can use the proxy host configuration to pass additional configuration options to the validator as HTTP headers. It's a good idea to configure different keys for different people, at the very least. Please read the configuration section to find out more.

4. How does it work under the hood?

The link goes to the Nginx reverse proxy, where it runs against the Nginx proxy configuration for your.domain.

You add the auth_request directive to the proxy configuration to cause requests to be validated against a 3rd party URL.

That 3rd party URL belongs to the nginx-ip-whitelister – which needs to be running at an address that the Nginx host can access.

Whenever nginx-ip-whitelister sees a valid access key in a request URL it adds the visitor's IP address to a whitelist. Once that happens, all requests from the IP (which usually means everybody and everything in their LAN) will be allowed through.

You can optionally configure more conditions for the visitors such as IP netmasks, GeoIP, TOTP codes etc.

5. How to run the whitelister

5.1. Running as a standalone app

Copy .env.example to .env and edit to your liking. Then:

$ npm install --omit-dev
$ node index.js

You may want to use a tool like supervisor or nodemon that will restart the whitelister if it fails.

It's also a good idea to redirect output to a log file that you can examine later if something goes wrong.

5.2. Running with Docker

5.2.1. Run a standalone Docker container

You can run this app as a Docker container that listens on the host's network interface. Use this if your Nginx or Nginx Proxy Manager are able to communicate directly with the host network.

Download the docker-compose-standalone.yaml file then run docker-compose up -d from the same directory.

5.2.2. Run a Docker container alongside a Nginx Proxy Manager container

If you intend to run both Nginx Proxy Manager and nginx-ip-whitelister as Docker containers you need to define a Docker network between them so the proxy will be able to reach the validator.

The file docker-compose-proxy-manager.yaml contains an example configuration that will deploy both NPM and this app in separate containers, but allow them to communicate through a Docker network.

You will need to create the Docker network:
docker network create nginx-network

Then run docker-compose up -d from the same directory where you've downloaded the .yaml file.

5.2.3. Pull the pre-built Docker image

You can find pre-built Docker images for this project on the GitHub Container Repository:
https://github.com/users/zuavra/packages/container/package/nginx-ip-whitelister

The example .yaml files provided with the source code already refer to the pre-built image, for your convenience.

However, you can also pull the ready-made image manually, to use in Docker configurations you've written yourself, or as a layer for other Docker images, or simply when you wish to update to the latest version of the app.

To do that, run:
docker pull ghcr.io/zuavra/nginx-ip-whitelister:latest

After pulling the latest image please remember that you also have to stop, remove, and then remake any Docker containers based on it.

5.2.4. Build a Docker image yourself

If you'd like to build a local Docker image yourself, for example if you've modified the source code, you can use the Dockerfile and .dockerignore included in the package and run the following command from the project root:

$ docker build --tag zuavra/nginx-ip-whitelister .

Yes, there's a dot at the end of the command.

This will build the image and publish it to your machine's local image repository, where it's now ready for being used by Docker containers.

Please remember to change your .yaml files or docker run commands to use the name of the local image (zuavra/nginx-ip-whitelister:latest) rather than the GHCR pre-built image (ghcr.io/zuavra/nginx-ip-whitelister:latest). You will also have to stop,remove and remake containers in order to use the new image.

6. How to integrate with Nginx

6.1. Nginx proxy host configuration

In order to tell Nginx to use nginx-ip-whitelister you need to use the auth_request directive to validate requests against the correct verification URL, and pass to it the original URI and the remote IP address.

In order to be able to use auth_request, Nginx needs to include the ngx_http_auth_request_module. Check the output of nginx -V for the module name.

The auth_request directive is pretty flexible and can be used in http, server or location contexts.

If you're using Nginx Proxy Manager, edit the proxy host that you're using for Emby/Jellyfin and add this example configuration in the "Advanced" tab.

6.1.1. Simple configuration example

This is a very basic example that only blocks requests without the correct key.

auth_request /__auth;
location = /__auth {
    internal;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass http://nginx-iw:3000/verify;
        proxy_set_header x-nipw-key "AVeryLongStringToBeUsedAsSecretKey";
}

If you're running the app standalone or in a non-networked container please replace nginx-iw with the appropriate hostname or IP address.

6.1.2. Advanced configuration example

This is a more advanced example that shows you how to use some access headers to set up custom access conditions. These aren't all the possible headers, see the next section for a full list.

auth_request /__auth;
location = /__auth {
    # common proxy settings
    internal;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Forwarded-For $remote_addr;

    # the next line has a parameter that makes this proxy instance use
    # a separate IP whitelist called "jellyfin" instead of the default whitelist
    proxy_pass http://nginx-iw:3000/verify?jellyfin; 

    # specific access settings; see the next section for all possible headers
        proxy_set_header x-nipw-key-isolation "disabled";
        proxy_set_header x-nipw-fixed-timeout "3h";
        proxy_set_header x-nipw-sliding-timeout "30m";
        proxy_set_header x-nipw-geoip-allow "DE";
        proxy_set_header x-nipw-key "AVeryLongStringToBeUsedAsSecretKey";
}

6.2. Validator URLs

6.2.1. Validation endpoints

Use /verify from the proxy host config to call the conditional validator. This validator endpoint is subject to the conditions that you have specified using x-nipw-* headers.

You can add an alphanumeric parameter to it (e.g. /verify?ServiceName123) to make it use a specific named whitelist. If the parameter is not provided it will use the default whitelist. This allows you to use different whitelists for different services. Having separated whitelists allows you to quickly reset the access whitelist for only one service from the admin interface without affecting the other services.

The parameter that gives the whitelist name is case-sensitive! Jellyfin and jellyfin will go to different whitelists.

You can also use /approve to always unconditionally pass the check, and /reject to always unconditionally fail the check (for integration tests).

6.2.2. Management endpoints

Use /admin/whitelist directly to see the current whitelist state. It provides links to /admin/delete that allow you to kick out individual addresses or wipe out a whitelist completely. Your own current IP is indicated in red if present.

Admin overview

6.2.3. Security

None of these endpoints are secured by default so do not expose them on the Internet without adding some form of protection (reverse proxy with basic authentication, IAM provider, VPN, SSH etc.)

If you wrap them behind reverse proxy you can use the validator to whitelist access to its own management endpoints but be warned it will suffer from all the shortcomings characteristic to IP whitelisting.

Ideally, validation endpoints should only be exposed on the internal Docker network between the Nginx Proxy Manager container and the validator container; and management endpoints only on the LAN, or properly tunneled.

7. Configuring the validator

7.1. Environment variables

The following variables can be added to the app environment. You can defined them in an .env file placed near index.js if you're running a standalone app, or provide them in the compose configuration, or as docker command line parameters etc.

7.2. Timeout headers

The following headers can optionally be passed to the validator from Nginx to adjust the timeout policy for the whitelist.

The header names are case insensitive. You can only use these headers once each – additional uses will be ignored.

The timeouts are always enforced, whether you use these headers or not. Using the headers merely allows you to adjust the intervals.

Both timeout policies are enforced in parallel – each IP has a fixed time window from when it started as well as a condition to not be inactive for too long.

7.3. Condition headers

The following headers can optionally be passed to the validator from Nginx to impose additional condition upon the requests.

The header names are case insensitive. Most of these headers can be used multiple times (exceptions are noted below).

Please don't use commas or semicolons inside header values, they can sometimes cause header libraries to split the value into separate ones.

Please understand that GeoIP matching is far from perfect. This project uses a "lite" GeoIP database which is not super-accurate, but even the larger databases can make mistakes. Accept the fact that occasionally you will end up blocking (or allowing) an IP that shouldn't be.

8. Validation logic

The logic works in the following order:

Remember that the whitelist is stored in RAM and will be lost every time you stop or restart the app (or its container).

9. Credits

This project uses IP Geolocation by DB-IP. Please note that the dbip-country-lite.mmdb file is licensed under Creative Commons Attribution 4.0 International.