Securing O365 with Nginx: Enforce Tenant Restrictions Using a Transparent Proxy

Introduction

Microsoft‘s Office 365 is one of the most widely used enterprise cloud platforms, providing a suite of productivity applications and services. With the increasing adoption of O365, organizations need robust access controls to protect sensitive data and prevent unauthorized access.

One powerful security feature is O365 Tenant Restrictions. Tenant Restrictions allow you to limit which Azure AD tenants your users can access, reducing the risk of data leakage to external organizations. Microsoft‘s built-in implementation requires an on-premises proxy server to insert HTTP headers specifying the allowed tenants.

In this guide, we‘ll walk through how to use the popular Nginx web server as a transparent proxy to seamlessly enforce O365 Tenant Restrictions. By the end, you‘ll have a secure setup that gives you greater control over your organization‘s O365 usage.

Understanding O365 Tenant Restrictions

Before we dive into the technical details, let‘s cover some background on what O365 Tenant Restrictions are and why they matter for security.

In a typical O365 setup, your organization has its own Azure AD tenant that provides authentication and single sign-on for O365 applications. However, by default, your users can also access resources in other organizations‘ O365 tenants. This opens up the risk of sensitive information being inadvertently shared with external parties.

O365 Tenant Restrictions solve this problem by letting you specify a list of allowed tenants. When a user authenticates, Azure AD checks the "Restrict-Access-To-Tenants" HTTP header. If the tenant the user is trying to access isn‘t on the allow list, they are blocked with an access denied message.

Implementing these restrictions gives you much tighter control over O365 access and helps prevent data leaks. However, the catch is that you need an on-premises proxy to insert the necessary HTTP headers. That‘s where Nginx comes in.

How the Nginx Proxy Enforces Restrictions

Nginx is a flexible open-source web server and reverse proxy. For our purposes, we‘ll use Nginx to create a transparent proxy that sits between clients and O365.

Here‘s a simplified overview of how the traffic flows:

  1. A user makes a request to an O365 application like Outlook.
  2. The request goes to the Nginx stream proxy listening on port 8443.
  3. Nginx inspects the hostname in the request using the SNI field. If it matches an Azure AD authentication endpoint, the traffic is routed to the Nginx HTTP proxy on port 9443.
  4. The HTTP proxy terminates SSL using dynamically generated self-signed certificates.
  5. The proxy inserts the necessary tenant restriction headers into the request.
  6. The request is forwarded on to Azure AD/O365, which reads the headers and allows or denies access based on the specified tenants.
  7. The response flows back through the proxies to the user.

The key points are that only the Azure AD authentication traffic is decrypted and modified, not the actual O365 application traffic. This avoids performance and security issues with inspecting all traffic.

Nginx‘s stream proxy functionality is used to efficiently route requests based on hostname. And the HTTP proxy leverages Lua scripting to dynamically handle the SSL certificates and header insertion.

Now that you have the big picture, let‘s walk through the setup process step-by-step.

Prerequisites

Before configuring Nginx, you‘ll need to install a couple prerequisites on your proxy server:

  • OpenResty: This is an enhanced distribution of Nginx bundled with Lua support and other useful modules.
  • OpenSSL: You‘ll need the OpenSSL command line tool to generate self-signed certificates.

You can install these however is appropriate for your OS, such as via a package manager like apt on Ubuntu.

Configuring the Nginx Stream Proxy

The first part of the setup is configuring Nginx as a stream proxy to route Azure AD traffic to the HTTP proxy. Here‘s the Nginx configuration:

stream {
  map $ssl_preread_server_name $server {
    login.microsoftonline.com 127.0.0.1:9443;
    login.microsoft.com      127.0.0.1:9443;
    login.windows.net        127.0.0.1:9443;
    default                  $ssl_preread_server_name:443;
  }

  server {
    resolver 8.8.8.8;

    listen 8443;
    ssl_preread on;
    proxy_ssl_session_reuse off;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate ca.crt;

    proxy_pass $server;
  }
}

The stream block configures Nginx to act as a layer 4 TCP proxy. The map directive specifies that traffic to the 3 Azure AD authentication hostnames should be sent to the HTTP proxy at 127.0.0.1:9443. All other traffic passes through untouched.

The server block listens on port 8443. The ssl_preread directive allows inspecting the SNI hostname without terminating SSL. proxy_ssl_verify enables certificate verification, which requires specifying the trusted root CA certificate file.

Configuring the Nginx HTTP Proxy

The meat of the tenant restriction enforcement happens in the HTTP proxy configuration:

http {
  default_type text/html;
  access_log off;

  ssl on;
  ssl_session_cache shared:SSL:10m;
  ssl_certificate /etc/nginx/default.crt;
  ssl_certificate_key /etc/nginx/default.key;
  ssl_certificate_by_lua_file "lua/certs_main.lua";

  lua_package_path ‘${prefix}../../src/?.lua;;‘;

  server {
    listen 9443;
    resolver 8.8.8.8;

    location / {
      access_by_lua_block {
        ngx.req.set_header("Restrict-Access-To-Tenants", 
          "mytenant.onmicrosoft.com,myothertenant.onmicrosoft.com");
      }

      proxy_pass https://$host;
    } 
  }
}

This block configures Nginx to proxy HTTP traffic. The ssl directives enable terminating SSL connections using the default self-signed certificate and key.

The magic happens in the ssl_certificate_by_lua_file directive, which executes a Lua script to dynamically return the appropriate certificate based on the hostname. This allows the proxy to masquerade as the Azure AD authentication endpoints.

Inside the server block, the location handles incoming requests. The access_by_lua_block directive inserts the tenant restriction headers, specifying the allowed O365 tenants for your organization. Make sure to replace these example tenant names with your actual tenant name(s).

Finally, the request is passed on via proxy_pass, which forwards it to the actual O365 endpoint.

Dynamically Returning SSL Certificates with Lua

The last piece of the puzzle is the Lua script that dynamically selects the certificate based on the hostname. Here‘s what that looks like:

local ssl = require("ngx.ssl")
local cjson = require("cjson")
local CERT_DIRECTORY = ‘/etc/nginx/certs/‘

local function load_cert_from_cache(name)
  local f, err = io.open(CERT_DIRECTORY .. name .. ‘.crt‘)
  if not f then
    return nil, err
  end
  local cert = f:read("*a") 
  f:close()

  local f, err = io.open(CERT_DIRECTORY .. name .. ‘.key‘)
  if not f then
    return nil, err
  end

  local key = f:read("*a")
  f:close()

  return {
    cert = cert, 
    key = key,
  }  
end

local function load_cert_matching(name)
  local cert_data = load_cert_from_cache(name)
  if cert_data then
    return cert_data
  end

  -- Try loading a wildcard cert, e.g. *.mydomain.com
  local wildcard = "*" .. string.sub(name, string.find(name, "%.",  1, true))
  return load_cert_from_cache(wildcard)
end

local function certs_main()
  local ok, err = ssl.clear_certs()
  if not ok then
    ngx.log(ngx.ERR, "failed to clear existing certificates")
    return ngx.exit(ngx.ERROR)
  end

  local name, err = ssl.server_name()
  if not name then
    ngx.log(ngx.ERR, "failed to get SNI: ", err)
    return ngx.exit(ngx.ERROR)
  end

  local cert_data = load_cert_matching(name)
  if not cert_data then
    ngx.log(ngx.ERR, "no matching cert found for: ", name)
    return ngx.exit(ngx.ERROR)
  end

  local der_cert, err = ssl.cert_pem_to_der(cert_data.cert)
  if not der_cert then
    ngx.log(ngx.ERR, "failed to convert PEM cert: ", err)
    return ngx.exit(ngx.ERROR)
  end

  local ok, err = ssl.set_der_cert(der_cert)
  if not ok then
    ngx.log(ngx.ERR, "failed to set cert: ", err)
    return ngx.exit(ngx.ERROR)
  end

  local der_key, err = ssl.priv_key_pem_to_der(cert_data.key)
  if not der_key then
    ngx.log(ngx.ERR, "failed to convert PEM key: ", err)
    return ngx.exit(ngx.ERROR)
  end 

  local ok, err = ssl.set_der_priv_key(der_key)
  if not ok then
    ngx.log(ngx.ERR, "failed to set privkey: ", err)
    return ngx.exit(ngx.ERROR)  
  end
end

certs_main()

This script does the following:

  1. Loads the OpenResty Lua SSL and JSON modules
  2. Defines helper functions for loading certificates from disk
  3. The load_cert_matching function first attempts to load a certificate file matching the full hostname. If that fails, it tries to load a wildcard certificate (e.g. for *.microsoft.com).
  4. In the certs_main function (executed for each request):
    • Clears any existing certificates
    • Gets the hostname from the SNI information
    • Loads the matching cert and key using the helper functions
    • Converts the cert and key from PEM to DER format
    • Sets the certificate and private key to be used for this request

Make sure to generate self-signed certificates for each Azure AD authentication hostname and store them in the /etc/nginx/certs/ directory with the corresponding hostnames. You can do this using OpenSSL:

openssl req -new -nodes -x509 -days 365 -newkey rsa:4096 \
  -keyout login.microsoft.com.key -out login.microsoft.com.crt

Repeat for login.microsoftonline.com and login.windows.net, or any other Azure AD endpoints you need to intercept.

Limitations and Considerations

While this Nginx-based approach to enforcing O365 tenant restrictions is powerful, there are a few things to keep in mind:

  • The proxy is terminating SSL connections and could theoretically access sensitive authentication data. Make sure the proxy server is properly secured.
  • We‘re only intercepting Azure AD authentication traffic, not the actual O365 application traffic like mail data. Those connections continue directly.
  • The Lua script only loads the first matching certificate. If you need to intercept dozens of hostnames, you may want to use a wildcard certificate.
  • Nginx and Lua are very efficient, but you‘ll still want to monitor performance and resource usage on the proxy server, especially with high traffic volumes.

Despite these caveats, this Nginx proxy strategy gives you an easy way to take advantage of O365 Tenant Restrictions without modifying clients or using a full-blown enterprise proxy solution.

Conclusion

Properly restricting access to only authorized O365 tenants is a key security best practice. By using Nginx as a transparent proxy, you can seamlessly enforce tenant restrictions across your organization with minimal configuration on clients.

The approach outlined in this post leverages Nginx‘s stream routing capabilities and Lua scripting support to efficiently process authentication traffic. Although there are some limitations to consider, this solution provides robust access controls for O365.

Hopefully this guide provides a clear overview of how to use Nginx to secure your O365 deployment. The complete configuration files are available [link to GitHub repo or similar]. Feel free to adapt it to your environment and requirements.

With cyber threats always evolving, it‘s crucial to make use of security features like O365 Tenant Restrictions. Nginx provides a flexible, open-source way to enforce these restrictions without the complexity of a full-blown proxy. Give it a try and level up your organization‘s cloud security!

Similar Posts