Publishing MAS with Nginx

January 24, 2023

Introduction

Using a reverse proxy is a common requirement for publishing web applications. A reverse proxy receives requests from an external source and relays it to an internal server and then relays the response back to the external requestor. This helps to obfuscate internal network addresses, server names and other details while also providing a gateway to monitor and limit access to internal resources. Often it also provides a more user friendly application address and translating something such as manage.mas.home.apps.azure.org.xyzcompany.com to a more easily remembered manage.xyzcompany.com.

A reverse proxy also allows the front end to expose a publicly trusted SSL certificate and allow the backend system to use an internally trusted certificate authority. This can dramatically reduce the maintenance overhead of deploying and renewing commercially available SSL certificates within systems such as OpenShift, which heavily rely on interconnected services secured with SSL certificates requiring frequent renewal.

In theory this is quite simple, but often times, as is the case with the Maximo Application Suite (MAS), the requests and responses contain resource references with internal domains and URLs. These references must be rewritten as they pass through the reverse proxy, ensuring that the payload and headers of the request are properly translated for the internal resource and the response is rewritten back for the external request.

This translation is often non-trivial, especially for the HTTP Headers that determine security behavior of the browser. Ensuring that Cookies have the proper domain so the browser can access them, that authentication redirect URLs are properly formatted, that CORS policies declare the correct hosts and methods (Maximo and CORS) and that Content-Security-Policy headers contain the correct references all must be properly, rewritten, otherwise the browser will reject requests to load resources and the application will not function correctly.

In this post we will review the configuration of Nginx as a reverse proxy for MAS and provide a sample configuration to properly rewrite the incoming request and outbound response.

Nginx (https://www.nginx.com/) is a popular open source high performance web server that is often used for reverse proxy applications.

Nginx Prerequisites

For our example we are using Ubuntu 22.04 and the Nginx 1.22.1. Digital Ocean provides an excellent step by step tutorial for installing Nginx on Ubuntu 22.04 here: https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-22-04.

If you are using a different operating system or version of Nginx there may be slight differences in file locations and commands, but the example configurations should still be valid.

We are also using njs scripting (https://nginx.org/en/docs/njs/), which is an optional module for Nginx that must be enabled. Details for installing njs scripting can be found here: https://nginx.org/en/docs/njs/install.html.

Alternatively, the following commands will install a precompiled version for Ubuntu 22.04.

sudo apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
sudo echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" | tee /etc/apt/sources.list.d/nginx.list
sudo apt update
sudo apt install nginx-module-njs

Nginx Configuration

Using the configuration below replace [EXTERNAL_DOMAIN] with the external domain for MAS and for [INTERNAL_DOMAIN] with the internal domain where MAS is deployed. For our example [EXTERNAL_DOMAIN] is mas.sharptree.io and [INTERNAL_DOMAIN] is mas.sharptree.internal.

Replace [INTERNAL_DNS_SERVER] with the IP address of the internal DNS server used to resolve the internal server names.

Finally, replace the [PATH_TO_SSL_CERTIFICATE] with the path to your SSL certificate and [PATH_TO_SSL_CERTIFICATE_KEY] with the path to your certificate key.

If you don't have a public SSL certificate, Let's Encrypt is a great way to get one. Digital Ocean has another excellent step by step tutorial for obtaining and deploy Let's Encrypt certificates here: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04

# Explicitly map external host names to internal ones.
# This could be done with a regex, but being explicit is clearer
map $host $internal_host {
admin.[EXTERNAL_DOMAIN] admin.[INTERNAL_DOMAIN];
api.[EXTERNAL_DOMAIN] api.[INTERNAL_DOMAIN];
auth.[EXTERNAL_DOMAIN] auth.[INTERNAL_DOMAIN];
home.[EXTERNAL_DOMAIN] home.[INTERNAL_DOMAIN];
[INSTANCE_ID].manage.[EXTERNAL_DOMAIN] [INSTANCE_ID].manage.[INTERNAL_DOMAIN];
}
server {
set $domain_in '[EXTERNAL_DOMAIN]';
set $domain_out '[INTERNAL_DOMAIN]';
# Internal DNS Server for upstream proxy
resolver [INTERNAL_DNS_SERVER];
server_name *.$domain_in;
# Path to a root folder for the web server, here we use /var/www/html
root /var/www/html;
# Some of the upstream header responses are large, increase the buffer size to accommodate
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
# Set the path were Nginx scripts are stored.
js_path /etc/nginx/njs;
# Import the functions from the mas_rewrites.js script.
js_import main from mas_rewrites.js;
# Replace the query values and return the result to the $query variable.
js_set $query main.replace_query;
location / {
# Call the cookies_filter function in the mas_rewrites.js script.
js_header_filter main.cookies_filter;
# Call the body_filter function in the mas_rewrites.js script.
js_body_filter main.body_filter;
# Call the rewrite_cookie function and store the result to the $rewritten_cookie variable
js_set $rewritten_cookie main.rewrite_cookie;
proxy_ssl_name $host;
proxy_pass https://$internal_host$uri$query;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_ssl_session_reuse on;
proxy_set_header Host $internal_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Do not accept upstream gzip otherwise replacement doesn't work.
proxy_set_header Accept-Encoding "";
# Replace the Cookie header with the rewritten version.
proxy_set_header Cookie $rewritten_cookie;
# Replace the internal domain (e.g. sharptree.local) with the external domain (e.g. sharptree.io)
proxy_cookie_domain mas.$domain_out mas.$domain_in;
add_header x-public-uri $host;
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "POST,GET,HEAD,PUT,OPTIONS";
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Expose-Headers csrftoken;
add_header Access-Control-Allow-Headers "authorization, content-type, maxauth, x-public-uri" ;
sub_filter_types *
sub_filter_once off;
sub_filter "$domain_out" "$domain_in";
}
listen 443 ssl;
ssl_certificate [PATH_TO_SSL_CERTIFICATE];
ssl_certificate_key [PATH_TO_SSL_CERTIFICATE_KEY];
}
server {
// SSL redirect
if ($host = [EXTERNAL_DOMAIN]) {
return 301 https://$host$request_uri;
}
server_name [EXTERNAL_DOMAIN] ;
listen 80;
return 404;
}

Create a new configuration text file in the /etc/nginx/sites-available/ directory and paste the edited configuration from above. For our example we are using mas.sharptree.io and the nano text editor.

sudo nano /etc/nginx/sites-available/mas.sharptree.io

Assuming you are using nano, after pasting the configuration, press ctrl+x and then Y to exit and save the changes.

Next, create a directory named njs in the /etc/nginx directory.

sudo mkdir -p /etc/nginx/njs

Create another new text file named mas_rewrites.js in the /etc/nginx/njs directory.

sudo nano /etc/nginx/njs/mas_rewrites.js

Copy and paste the following script into the new file and press ctrl+x then Y to save and close the file.

The Content-Security-Policy header, often referred to as the csp header, informs the browser which URLs are safe to load objects such as css, script, image and font resources from. The MAS Manage version 8.5 csp header omits the gstatic.com and walkme.com URLs and the fonts and scripts that are referenced by MAS Manage are blocked for these URLs.

The script below provides a fix for the missing csp policies util corrected in a future version of MAS.

A full explanation of the Content-Security-Header is available at the Mozilla Developer Network, here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

import qs from 'querystring';
function rewrite_cookie(r) {
var cookies = r.headersIn['Cookie'];
if (cookies) {
return cookies.split(r.variables.domain_in).join(r.variables.domain_out);
} else {
return "";
}
}
function cookies_filter(r) {
var cookies = r.headersOut['Set-Cookie'];
if (cookies) {
r.headersOut['Set-Cookie'] = cookies.map(c => c.replace(r.variables.domain_out, r.variables.domain_in));
}
var location = r.headersOut.location;
if (location) {
r.headersOut.location = location.split(r.variables.domain_out).join(r.variables.domain_in);
}
// There are missing entries for gstatic and walkme in the out of the box implementation,
// fixing that here and also adding the appropriate internal references.
r.headersOut['content-security-policy'] = "default-src 'self' https://*." + r.variables.domain_in + "; img-src https: data:; font-src 'self' data: https://fonts.gstatic.com https://1.www.s81c.com ; script-src https://cdn.walkme.com 'unsafe-eval' 'unsafe-inline' https://*." + r.variables.domain_in + "; object-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src https://*." + r.variables.domain_in + " https://*.walkme.com ;"
// If future versions of MAS fix the CSP issues:
// * Comment out the line above, as it is no longer necessary
// * Uncomment the code below to simply swap domains where needed.
// var content_security_policy = r.headersOut["content-security-policy"];
// if (content_security_policy) {
// r.headersOut["content-security-policy"] = content_security_policy.split(r.variables.domain_out).join(r.variables.domain_in);
// }
}
function body_filter(r, data, flags) {
// for string data replace the out domain (e.g. sharptree.local) with the in domain (e.g. sharptree.io)
if (typeof data === 'string') {
r.sendBuffer(data.split(r.variables.domain_out).join(r.variables.domain_in), flags);
} else {
r.sendBuffer(data, flags);
}
}
function replace_query(r) {
if (typeof r.variables.args !== 'undefined') {
let query = qs.parse(r.variables.args);
if (query['redirect_uri']) {
query['redirect_uri'] = query['redirect_uri'].replace(r.variables.domain_in, r.variables.domain_out);
}
return "?" + qs.stringify(query);
}
return "";
}
export default { cookies_filter, body_filter, replace_query, rewrite_cookie };

Next create a new symbolic link in the /etc/nginx/site-enabled directory for the configuration file that was created in the /etc/nginx/sites-available directory. This will cause Nginx to load the new configuration file as part of active configuration when the server is restarted.

Continuing with our example, we used mas.sharptree.io and created the link with the following command.

sudo ln -s /etc/nginx/sites-available/mas.sharptree.io /etc/nginx/sites-enabled/mas.sharptree.io

Finally, restart Nginx to activate the configuration.

sudo systemctl restart nginx

For those that don't enjoy living dangerously, the command nginx -t can be used to test and verify the configuration before you restart the server.

Conclusion

In this post we provided the configuration and script necessary to publish MAS through a Nginx reverse proxy. This configuration allows hosting MAS on an internal network and making it available to users on a public network without exposing internal host names or internal configurations. With the reverse proxy users can access MAS using an external domain with different and often more user friendly addresses. In our example we made mas.sharptree.local available as mas.sharptree.io to keep the examples simple, but this could be used for entirely different domain names and addresses.

If you have any questions or comments please reach out to us at [email protected]

In the time it took you to read this blog post...

You could have deployed Opqo, our game-changing mobile solution for Maximo.

Opqo is simple to acquire, simple to deploy and simple to use, with clear transparent monthly pricing that is flexible to your usage.