A Partially-Successful Venture Into Improving Reverse Proxies With Domino
Sat Jan 30 16:00:41 EST 2021
- Putting Apache in Front of Domino
- Better Living Through Reverse Proxies
- Domino's Server-Side User Security
- A Partially-Successful Venture Into Improving Reverse Proxies With Domino
- PSA: Reverse-Proxy Regression in Domino 12.0.1
I've long been an advocate for Domino's HTTPEnableConnectorHeaders
notes.ini setting. That's the one that lets you pass some WebSphere-derived headers like $WSRA
and (particularly-dangerously) $WSRU
to Domino and have them fully override the normal values for the incoming host, user, and more.
I'm still a big fan of it, but it always come with the irritating absolute requirement that Domino not be publicly-accessible, lest any schmoe come along and pretend to be any user on your server. That's fine and all, but sometimes it's useful to have Domino be accessible without the proxy, such as for troubleshooting. What I really want it selective enabling of this feature based on source IP. So I set out this weekend to try to implement this.
The Core Idea
The mechanism for doing the lowest-level manipulation you can with HTTP on Domino is to use DSAPI, the specialized API for the web server task specifically. Unfortunately, working with DSAPI means writing a native library with C ABI entrypoints for a handful of known functions. I don't enjoy writing C (though I respect it thoroughly), but I've done it before, so I set out making a project plan.
My goal was to support the X-Forwarded-*
headers that are common among reverse proxies, allowing administrators to use this common idiom instead of the $WS*
headers, and also having the side effect of removing the "act as this user" part from the equation. I figured I'd make up a notes.ini property that took a comma-separated set of IP patterns, look for requests coming in from those hosts, and elevate the X-Forwarded-*
headers in them up to their equivalent real properties.
Initial Side Trip Into Rust
Since C in the hands of a dilettante such as myself has a tendency to result in crashed servers and memory bugs, I figured this might be a good opportunity to learn Rust, the programming language that sprung out of Mozilla-the-organization that focuses heavily on safety, and which has some mechanisms for low-level C interoperability.
I figured this would let me learn the language, have a safer program, and avoid having to bring in third-party C libraries for e.g. regex matching.
I made some progress on this, getting to the point where I was able to actually get the filter compiled and loaded properly. However, I ended up throwing up my hands and shelving it for now when I got lost in the weeds of how to call function pointers contained in pointers to C structs. I'm sure it's possible, but I figured I shouldn't keep spending all my time working out fiddly details before I even knew if what I wanted to do was possible.
Back to C
So, hat in hand, I returned to C. Fortunately, I realized that fnmatch(3)
would suit my pattern-matching needs just fine, so I didn't actually need to worry about regexes after all.
Jaw set and brimming with confidence, I set out writing the filter. I cast my mind back to my old AP Computer Science days to remember how to do a linked list to allow for an arbitrary number of patterns, got my filter registered, and then set about intercepting requests.
However... it looks like I can't actually do what I want. The core trouble is this: while I can intercept the request fairly early and add or override headers, it seems that the server/CGI variables - like REMOTE_HOST
- are read-only. So, while I could certainly identify the remote host as a legal proxy server and then read the X-Forwarded-For
header, I can't do anything with that information. Curses.
My next thought was that I could stick with the $WS*
headers on the proxy side, but then use a DSAPI filter to remove them when they're being sent from an unauthorized host. Unfortunately, I was stymied there too: the acceptance of those headers happens before any of the DSAPI events fire. So, by the time I can see the headers, I don't have any record of the true originating host - only the one sent in via the $WSRA
header.
The Next-Best Route
Since I can't undo the $WS*
processing, I'd have to find a way to identify the remote proxy other than its address. I decided that, while not ideal, I could at least do this with a shared secret. So that's what I've settled on for now: you can specify a value in the HTTPConnectorHeadersSecret
notes.ini property and then the filter will verify that $WS*
-containing requests also include the header X-ConnectorHeaders-Secret
with that value. When it finds a request with a connector header but without a matching secret, it unceremoniously drops the request into processing oblivion, resulting in a 404.
I'm not as happy with this as I would have been with my original plan, but I figure it should work. Or, at least, I figure it will enough that it's worth mulling over for a while to decide if I want to deploy it anywhere. In the mean time, it's up on GitHub for anyone curious.
Additionally, kindly go vote for the X-Forwarded-For
ideas on the Domino Ideas portal, since this should really be built in.