Doorman Authenticating Proxy
At Movable Ink, we have a number of different backend services, many of which have administrative web interfaces. We want to give employees access to these services without opening the services up to the world, so we need to authenticate user access. Some of the services have built-in authentication, but few support delegated authentication and we didn’t want to have to manage users in many different places.
All of our services live firewalled off inside VPCs in Amazon’s EC2. One method for allowing access might be to let our employees set up VPN connections into our datacenters. With the VPN connected, employees could make internal requests via direct access to our internal network.
The problem with that approach is that we have different levels of authorization within our organization. Someone on our devops team may need direct access into our internal network, but many of our employees may just need access to a handful of web services. It’s possible to implement fine-grained access control using networking, but it is complicated and error-prone. Google recently blogged about the same drawbacks.
We developed Doorman to act as an authenticating proxy between the world and our internal services. It’s written in node.js, and leans heavily on node-http-proxy for proxying and everyauth for service authentication. We use express’s middleware pattern to chain everything together. An annotated excerpt from Doorman:
var app = express();
// Log the request to stdout
app.use(logMiddleware);
// Redirect http requests to https
app.use(tls);
// If the user is already authenticated, read their signed cookie
app.use(cookieParser(conf.sessionSecret));
// Create the user's session from the cookie
app.use(doormanSession);
// Display flash messages when appropriate
app.use(flash());
// Ensure the user is valid and allowed to access the service, this
// also halts the chain and proxies to the internal service if successful
app.use(checkUser);
// Parse POST bodies
app.use(bodyParser.urlencoded({extended: false}));
// Register paths for oAuth redirects and callbacks
app.use(everyauth.middleware());
// `/_doorman` serves assets for doorman login form
app.use(express.static(__dirname + "/public", {maxAge: 0 }));
// display the login page if the user isn't authenticated
app.use(loginPage);
Each middleware runs successively and has the opportunity to modify the request/response, halt it and return, or continue down the chain. For example, the tls
middleware tells the response
to send a 301 Moved
and halts the chain, while cookieParser
tacks some information onto the request
object and continues down to the next middleware. In the event none of the middlewares halt the chain and finish the response, we serve a 404 Not Found
.
One particular challenge we ran into was how to use Doorman for multiple internal services at once. A simple way would be to run a separate Doorman app for each internal service, but it’s often overkill to have a dedicated Doorman for each service. We’re planning on introducing multiple domain support, which can route to different internal services based on the hostname, and have different access control for each service. Due to SSL limitations, it will likely require that all of the internal services be on different subdomains and have Doorman configured to use a wildcard certificate. Another possibility would be to use LetsEncrypt with SNI support.
If you’re interested in using or contributing to Doorman, you can clone it on Github.