Today I had the fun task of taking Cloud9’s build bot and making it more secure. Primarily because it’s now exposed to the outside world and we don’t want random strangers having the ability to ship or revert our code.
Our bot responds to slash commands on Slack, so we can type /ship [appname]
at any time in any channel in slack and the latest tested code will be pushed to production. It also recieves notifications from Jenkins when jobs have started, succeeded or failed.
Securing Slack
The first step was ensuring all Slack commands were actually coming from Slack. Whenever you create a new slash command Slack tells you it will send a specific token with all api calls, and you should use this to verify the call is from Slack.
Now there are multiple routes we wanted our bot to talk to and multiple slash commands to reach them, each with their own tokens. But we don’t want to add if (token == ‘xyz’) to every single route. Firstly because it’s messy, and secondly because then whenever a new developer joins the project they have to remember to do that or they’ll compromise security. So how do we do it? By creating a /slack route that verifies every token for us.
var express = require("express");
var config = require("config);
function verifyToken(req, res, next) {
if (!req.body.token || config.get("slack.tokens").indexOf(req.body.token) === -1) {
return next(new Error("Invalid slack token" + req.body.token));
}
next();
}
var slackRouter = new express.Router();
slackRouter.use(verifyToken);
slackRouter.post("/highfive", highFive.handleRequest.bind(highFive));
slackRouter.post("/ship", ship.startShipping.bind(ship));
app.use("/slack", slackRouter);
We have an array of possible slack tokens stored in our config file and whenever we add or remove commands we can simply add the token to that one list.
Now our routes are /slack/ship
and /slack/highfive
and whenever anyone sends data to them it will always validate that they have a valid slack token. No more manual verification in each route or having new developers forget to add security to their route, it’s all automatic.
Securing Jenkins
Our bot also listens to build hooks from Jenkins so that it can post to our Slack channel letting us know about the stats of various jobs.
We can secure Jenkins in the same way, but because it doesn’t pass any custom data in these job notifications we’ll secure it based on the requester IP address.
var requestIp = require("request-ip");
function verifyIPIsJenkins(req, res, next) {
var reqIp = requestIp.getClientIp(req);
if (!reqIp || config.get("jenkins.ips").indexOf(reqIp) === -1) {
return next(new Error("Jenkins push request came from " + reqIp + " which isn't a known address"));
}
return next();
};
var jenkinsRouter = new express.Router();
jenkinsRouter.use(verifyIPIsJenkins);
jenkinsRouter.post("/success", jenkins.buildSuccess.bind(jenkins));
jenkinsRouter.post("/failed", jenkins.buildFailed.bind(jenkins));
app.use("/jenkins", jenkinsRouter);
Now just like above we have 2 routes at /jenkins/success
and /jenkins/failed
and whenever anyone tries to access them it automatically verifies they are our CI server. If they are not the request will fail.
The reason I enjoy using these routes is they keep the code neat and also ensure that when someone comes to work on this project in the future they can easily add another route and won’t accidently allow hackers in the back door. Keeping things simple and automatic so any developer can pick up this code and run with it is my style of programming.