life runs on code

bending technology to do your bidding

Mastodon with IIS as an SSL Terminating Reverse Proxy

Overview

Mastodon is a distributed microblogging platform that runs on Linux and is primarily written in Ruby on Rails and JavaScript. The default Mastodon configuration uses nginx as a reverse proxy on the same machine as the application to forward connections into the application's webserver. The bulk of the content served up by Mastodon is done internally from Ruby On Rails using Puma, however, there is some static content that is expected to be served by nginx (the contents of /live/public). The Mastodon documentation is fairly detailed and it provides guidance for an internet facing VM installation using nginx as the web server with the reverse proxy on the same sever as the application. I however was interested in running a Mastodon instance on an internally facing Debian virtual machine which had Apache installed. I wanted to run an SSL terminating reverse proxy on an internet facing Windows Server VM with IIS sending traffic to the internal Mastodon instance. Below you'll find the configuration I have cobbled together to make all of this work.

Configuration

This guide is based on Mastodon v4.2.
My configuration consists of the following components:

Install the Mastodon software as documented onto the Debian virtual machine with the following modifications:

System Packages

You can skip installing the nginx and python-certbot-nginx system packages since we'll be using Apache. If you don't already have the apache2 system package installed, you'll want to do that.

Setting up nginx

Skip this section in the Mastodon installation instructions. We'll be using Apache to host the static content. I already had a site running under Apache on on port 80, so I setup the Mastodon content as a new site on port 8082.

Create the following file:
/etc/apache2/sites-available/mastodon.conf

<VirtualHost *:8082>
  Protocols h2 http/1.1

  DocumentRoot /home/mastodon/live/public

  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

  <LocationMatch "^/(assets|avatars|emoji|headers|packs|shortcuts|sounds|system)">
    Header always set Cache-Control "public, max-age=2419200, must-revalidate"
    Require all granted
  </LocationMatch>

  <Location "/">
    Require all granted
  </Location>

  ErrorDocument 500 /500.html
  ErrorDocument 501 /500.html
  ErrorDocument 502 /500.html
  ErrorDocument 503 /500.html
  ErrorDocument 504 /500.html

</VirtualHost>

Now enable the site in Apache by executing:
sudo a2ensite mastodon

Next, we will need to tell Apache to listen on port 8082.
Edit the /etc/apache2/ports.conf file and add the directive to listen on 8082.

Listen 80
Listen 8082

Now we need to restart Apache for our changes to take effect.
sudo systemctl restart apache2

Acquiring an SSL certificate

Skip this section. We'll be using IIS as an SSL terminating reverse proxy along with Windows ACME Simple (WACS) on the Windows Server to obtain an SSL certificate.

Setting up systemd services

By default, the services bind to localhost. After installation we need to do the following to have the services bind to something other than 127.0.0.1

Edit the `/home/mastodon/live/.env.production' file. Set the following values:

ALTERNATE_DOMAINS=<hostname of the Debian VM>
TRUSTED_PROXY_IP=<Local IP Address of the IIS reverse proxy>
BIND=<Local IP Address of the Debian VM>

You may also wish to customize the LOCAL_DOMAIN and WEB_DOMAIN settings. This allows my users to have a Mastodon address of username@unknownrealm.org instead of a username@mastodon.unknownrealm.org

For example, I have mine set as follows:

LOCAL_DOMAIN=unknownrealm.org
WEB_DOMAIN=mastodon.unknownrealm.org 

This type of configuration requires a finger redirect rule on the IIS site hosting unknownrealm.org to the IIS site hosting mastodon.unknownrealm.org as detailed in the IIS configuration section.

Now we need to modify the /etc/systemd/system/mastodon-web.service configuration to bind the web service to the local IP Address of the Debian virtual machine. The default configuration has the service listening to requests only on 127.0.0.1. Find the Environment="BIND=127.0.0.1" line in the file and replace the IP Address with the local IP of your VM, then restart the service. 

sudo systemctl restart mastodon-website

IIS Configuration

Install the URL Rewrite and Application Request Routing (ARR) modules.

Install the IIS WebSocket Feature by opening an elevated PowerShell prompt and running the following command:

Install-WindowsFeature -name Web-WebSockets

Create two IIS Sites (unknownrealm.org and mastodon.unknownrealm.org) with an http binding leveraging hostname headers. 

Install Windows ACME Simple (WACS). Use WACS to Create a Certificate (default settings) for the unknownrealm.org and mastodon.unknownrealm.org IIS sites. You can go with the defaults for all of the subsequent options.

In IIS Manager pull up the binding properties of the unknownrealm.org and mastodon.unknownrealm.org IIS sites to verify and/or adjust the https binding to Require SNI.

Server: Application Request Routing Requires the following configuration

Open IIS Manage and select the server node in the tree view on the left hand side.
Open the "Application Request Routing" feature.

Once the Application Request Routing Configuration displays, select Server Proxy Settings... under Actions in the far right pane.

Enable and Configure Application Request Routing as depicted below: 

Site: LOCAL_DOMAIN Site Configuration

The unknownrealm.org IIS site which serves as our LOCAL_DOMAIN needs a redirect rule to properly forward requests to the mastodon.unknownrealm.org site in order to handle finger request. To do this, we add the following Mastodon Finger Redirect Rewrite rule to the web.config of the unknownrealm.org site.

<system.webServer>
	<rewrite>
	  <rules>
		<rule name="Mastodon Finger Redirect" stopProcessing="true">
			<match url="(.*)" />
			<conditions>
				<add input="/{R:1}" pattern=".well-known/webfinger" />
			</conditions>
			<action type="Redirect" url="https://mastodon.unknownrealm.org/{R:1}" />
		</rule>
	  </rules>
	</rewrite>
</system.webServer>

Site: WEB_DOMAIN Site Configuration

The mastodon.unknowrealm.org IIS site serves as our WEB_DOMAIN. Its the SSL terminating reverse proxy that forwards all traffic to the internally facing Debian server which is running the Mastodon application. in IIS Manager, select the mastadon.unknownrealm.org side and click on URL Rewrite.

Once the URL Rewrite configuration displays, select the View Server Variables... under Actions in the far right pane.

Define the following Server Variables so that we can use them in our SSL terminating Reverse Proxy:

HTTP_X_REAL_IP - Source client IP address of the request
HTTP_X_FORWARDED_PROTO - Protocol the client used to connect to the proxy
HTTP_X_FORWARDED_FOR - Source client IP address for which the proxy request is being forwarded
HTTP_SEC_WEBSOCKET_EXTENSIONS - Reset the header to eliminate potential permessage-deflate IIS issue

Now we will configure the rules to proxy the traffic and set the headers:

<system.webServer>
	<rewrite>
		<rule name="Reverse Proxy Inbound Static Content" enabled="true" stopProcessing="true">
			<match url="(.*)" />
			<conditions logicalGrouping="MatchAny" trackAllCaptures="true">
					<add input="{R:0}" pattern="^(500.html|sw.js|robots.txt|manifest.json|browserconfig.xml|mask-icon.svg)$" />
					<add input="{R:0}" pattern="^((assets|avatars|emoji|headers|packs|sounds|system)/.*)" />
					<add input="{R:0}" pattern="^(.*\.(png|ico)$)" />
			</conditions>
			<action type="Rewrite" url="http://tatooine.unknownrealm.org:8082/{R:1}" appendQueryString="true" logRewrittenUrl="true" />
			<serverVariables>
				<set name="HTTP_X_FORWARDED_PROTO" value="https" />
				<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />					
				<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
			</serverVariables>
		</rule>
		<rule name="Reverse Proxy Inbound Web Socket" enabled="true" stopProcessing="true">
			<match url="(.*)" />
			<conditions logicalGrouping="MatchAny">
					<add input="{R:0}" pattern="^(api/v1/streaming/.*)" />
			</conditions>
			<action type="Rewrite" url="http://tatooine.unknownrealm.org:4000/{R:1}" appendQueryString="true" logRewrittenUrl="true" />
			<serverVariables>
				<set name="HTTP_SEC_WEBSOCKET_EXTENSIONS" value="" />
				<set name="HTTP_X_FORWARDED_PROTO" value="https" />
				<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
				<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
			</serverVariables>
		</rule>
		<rule name="Reverse Proxy Inbound Default Rule" enabled="true" stopProcessing="true">
			<match url="(.*)" />
			<conditions logicalGrouping="MatchAny">
			</conditions>
			<action type="Rewrite" url="http://tatooine.unknownrealm.org:3000/{R:1}" appendQueryString="true" logRewrittenUrl="true" />
			<serverVariables>
				<set name="HTTP_X_FORWARDED_PROTO" value="https" />
				<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />					
				<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
			</serverVariables>
		</rule>
	  </rules>
	</rewrite>
	<caching enabled="false" enableKernelCache="false" />
	<urlCompression doStaticCompression="true" doDynamicCompression="true" />
</system.webServer>

The sequence of the rules above is important in order to properly route the request to the correct Mastodon back-end for processing. 

Reverse Proxy Inbound Static Content - Routes static content requests to the Apace web server.
Reverse Proxy Inbound Web Socket - Routes requests to the back-end Mastodon WebSocket API hosted by Puma
Reverse Proxy Inbound Default Rule - Catch all rule that routes any web requests to the Mastodon Puma hosted http service. 

Additional Rule: HTTP to HTTPS Rewrite

I recommend tacking on the following rewrite rule to force the rewrite of any HTTP requests to HTTPS on both the unknownrealm.org and mastodon.unknowrealm.org sites. This should be the first rule in your <rules> collection.

<system.webServer>
	<rewrite>
	  <rules>
		<rule name="HTTP to HTTPS redirect" enabled="true" stopProcessing="true">
		  <match url="(.*)" />
		  <conditions>
			<add input="{HTTPS}" pattern="off" ignoreCase="true" />
		  </conditions>
		  <action type="Redirect" redirectType="Found" url="https://{HTTP_HOST}/{R:1}" />
		</rule>
	</rewrite>
</system.webServer>