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:
- A user makes a request to an O365 application like Outlook.
- The request goes to the Nginx stream proxy listening on port 8443.
- 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.
- The HTTP proxy terminates SSL using dynamically generated self-signed certificates.
- The proxy inserts the necessary tenant restriction headers into the request.
- The request is forwarded on to Azure AD/O365, which reads the headers and allows or denies access based on the specified tenants.
- 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:
- Loads the OpenResty Lua SSL and JSON modules
- Defines helper functions for loading certificates from disk
- 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). - 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!