Dynamic IP-address filtering for Azure

Putting applications in the cloud is great, and offers a lot of benefits (as well as some complications). We get great scalability, elasticity, low cost of ownership etc. One problem however, is that the cloud is very public. I guess this isn’t a problem in most cases, but if what you are putting up there is supposed to be secret, or at least needs to limit who gets to use it, it becomes an issue.

I am currently working on a project like this. I am not going to talk about the project as such as it is under NDA, but the fact that it is is a service in the cloud that should only be used by certain clients is not uncommon.

The service has a front end that consists of WCF services, hosted in a web role, which is what we need to secure. The worker roles behind the web roles are by default secure as they do not communicate with the outside world at all.

We are using basicHttpBindings for different reasons, and have decided to secure the communication using BasicHttpSecurityMode.TransportWithMessageCredential. We have also decided that due to the limited amount of clients, and their static locations, IP-address filtering would add another layer of security. It isn’t fail safe, but it is at least another layer that you would need to get through.

Setting up IP-address filtering isn’t hard on a local IIS, but require a bit more work in the cloud. Why? Well, the feature isn’t installed by default. And to adding to that, we need to be able to reconfigure the IP-addresses without having to redeploy the application.

But let’s start with enabling the IP-address filtering module in IIS, which is done using a startup task. The IP-address whitelisting can wait for now. One thing at the time.

A startup task is a cmd-file that is run when the role is being set up. You can find a lot more information about these tasks on the interwebs. This information will tell you that you can run them as background tasks and foreground tasks and probably do all sorts of things. In this case, we can ignore that…

Let’s start by creating a startup task. I do this by creating a new text file called Startup.cmd (name doesn’t matter). I do however make sure it is saved as a UTF-8 encoded file without a byte order mark. VS doesn’t do this easily, so create the file using Notepad instead. I put it in the root of my application, but it can probably be anywhere…(I didn’t read up on all the startup task features, but I assume you can just change the path to it…)

Also, make sure to set the “Build Action” in VS to none, and the “Copy to Output Directory” setting to “Copy always” or “Copy if newer”. This will make sure that the file is included in the package, but isn’t embedded.

Inside the file, we can place any command that we want to run on the server. In this case, we need to install the "IPv4 Address and Domain Restrictions" feature, and unlock the IP-security section in the configuration. I have blatantly stolen this script somewhere on the web, but I can’t remember where. I have however seen it in a LOT of different places when looking for it, so I assume it is ok for me to use it as well…

@echo off

@echo Installing "IPv4 Address and Domain Restrictions" feature
%windir%\System32\ServerManagerCmd.exe -install Web-IP-Security

@echo Unlocking configuration for "IPv4 Address and Domain Restrictions" feature
%windir%\system32\inetsrv\AppCmd.exe unlock config -section:system.webServer/security/ipSecurity

As you can see, it uses ServerManagerCmd.exe to install the feature, and AppCmd.exe to unlock the config section for modification. Without unlocking it, we are not allowed to make changes to it…which we need to do…

To get the startup task to run, I open the ServiceDefinition.csdef and add the following fragment to the WebRole element

<Startup>
<Task commandLine="Startup.cmd" executionContext="elevated" taskType="simple" />
</Startup>

As you can see, I am telling it to run my Startup.cmd from the root of the project, using elevated privileges.

If you were to run the current setup, it would just work. Well…kind of… It would install the feature alright, but it wouldn’t do any filtering. To get it to actually do something, you would have to change the web.config file to till IIS to actually filter. This is done in the configuration/system.webServer/security element. You can find more information about it here for example.

I do want to highlight a little thing here. The call to AppCmd.exe to unlock the config section will actually do this to your local machine. The Azure emulator is just using your local machine when it is running, and when using a full IIS web role, it uses IIS to host the application. So any call you do in the cmd-file might affect your machine. In this case it will modify the ApplicationHost.config in %system%\inetsrv\config, thus actually affecting your entire machine.

If you want to avoid this you can use a technique Steve Marx shows in this post

But back to the problem at hand! I need to make it a bit more complicated than a hardcoded list of IP-addresses. It needs to be able to be configured using the Azure config instead. The reason? Well, they can be changed on a running instance, while the web.config can’t. A web.config change requires a redeploy of the application. This last part proved to be a bit more complicated than I thought though.

I started out by adding a new configuration setting definition called WhiteListedIPs in the service definition, and updated each of the role configurations with a list of good IP-addresses. Or rather just the cloud one… Locally I am going to do it a little differently…

I then set about trying to use the string in my cmd-file. However, after having failed several times, I came to the conclusion that I sucked at writing cmd-files…and in a vain attempt to explain why, I just want to say that I wasn’t interested in computers when cmd-files where common. So I really have very little knowledge about how you author cmd-files. I am however quite good at building C# apps, and also at coming up with cowboy-ways of solving problems.

To modify the configuration from C#, you can use a class called ServerManager from the Microsoft.Web.Administration assembly. It opens up any config file you want, and lets you manipulate it's content. Just what I need!

So in the OnStart method of my web role, I add the following code

using (var serverManager = new ServerManager())
{
var config = serverManager.GetApplicationHostConfiguration();
var ipSecuritySection = config.GetSection("system.webServer/security/ipSecurity");
var ipSecurityCollection = ipSecuritySection.GetCollection();
ipSecuritySection["allowUnlisted"] = false;

ipSecurityCollection.Clear();

var addElement = ipSecurityCollection.CreateElement("add");
addElement["ipAddress"] = @"127.0.0.1";
addElement["subnetMask"] = @"255.0.0.0";
addElement["allowed"] = true;
ipSecurityCollection.Add(addElement);

var addresses = RoleEnvironment.GetConfigurationSettingValue(“WhiteListedIPs”).Split(new [] { ";" }, StringSplitOptions.RemoveEmptyEntries);

foreach (var address in addresses)
{
addElement = ipSecurityCollection.CreateElement("add");
addElement["ipAddress"] = address;
addElement["subnetMask"] = @"255.255.255.255";
addElement["allowed"] = true;
ipSecurityCollection.Add(addElement);
}

serverManager.CommitChanges();
}

In this case I call the ServerManager.GetApplicationHostConfiguration(). This opens up the ApplicationHost.config file, just like the cmd-file did.

I admit that changing the host config instead of the individual web.config file seems a little little shooting mosquitos with a cannon, but it was the way I did it. And it works… As this is

Next, I make sure to set the allowUnlisted parameter to false so that any address not listed will be denied access. Then I go ahead and clean out the existing addresses and configured the allowed IP-addresses.

Do NOT forget to add 127.0.0.1 as well. If you do, debugging will fail. I also had some issues when not setting the subnet mask to a “wider” value. But changing that made debugging work. Don’t ask me why, but please tell me why if you know!

This whole thing can be put inside an if-statement that skips it if you are in emulated mode. This, together with Marx’s emulated trick, would keep your machine completely clean, but still work in the cloud…I did however think of that a bit too late…

After the local address is added, I split the list of IP-addresses from the config, and add them one at the time. Finishing the whole thing off with a call to CommitChanges().

Running this will now use the startup task to install the IP-address filtering feature, unlock the config and set up the allowed IP-addresses and blocks everyone else.

Well…at least almost. Running this will give you an Exception. The Web Role is run under a user account  with limited permissions. So it doesn’t have permission to modify the config file… This is fairly easy to change though. In the ServiceDefinition.csdef, add an element that looks like this

<Runtime executionContext="elevated" />

as a child to the <WebRole /> element. This will make the web role process is run with elevated permission, allowing you to make the change.

Ok, so to the part about reconfiguring it on the fly. This is actually not that hard. Hook up a handler for the RoleEnvironment.Changed event, which is raised whenever the config is changed. Inside the handler, make sure you execute the above configuration changing code again. Or even better, refactor that code into its own method and call that method from both places. DRY dude, DRY! Smile

This will cause the IP-filtering config to change when you modify your Azure config, without it restarting. At least if you don’t do anything else. If you however hook into the RoleEnvironment.Changing event and happen to set the Cancel flag of the eventargs to true, you will force a restart anyway…just saying…

There is one more thing to note though! One that screwed me the first time around when building this. When running this locally, the IP-filtering did not kick in straight away. Rather, it would let the first request go through, even if I blocked it. And the reason for this is that the emulator piggybacks on IIS. So it configured the IIS for us during start up, and then the IIS is actually responsible for handling the requests. This however means that there is a short period where the IIS is configured, but the OnStart method hasn’t yet executed.

In the real cloud, this isn’t going to happen. Up there, the IIS configuration and role startup and stuff is handled differently, causing the OnStart method to execute before the load balancer is told that it is ok to send through requests. So it should not happen there.

There is more information about that here and here. Especially that last one cleared up a bunch of things for me… I recommend having a look at it…

If you are looking at hosting multiple virtual applications in IIS, might also want to look here. It is probably a bit of an extreme/edge case, but the post contains some intersting and cool information…

That’s it! As usual, I hope you got some info you needed. There are a lot of samples on how to run startup tasks out there. There are even a bunch about setting up IP-filtering. But I couldn’t find one that could take the configuration from the Azure config, and thus couldn’t be changed at runtime. Not a big difference, but it does add a little bit of extra stuff to handle…

In a different scenario with more frequent IP-changes, I would probably have put the IP-addresses in a DB instead, and have a WCF service method that could be called to make the change. But since the IP-address changes are very likely to be infrequent in this project, this sufficed.

No code this time…sorry!

Pingbacks and trackbacks (1)+

Comments are closed