Whitelisting IP subnets rather than individual addresses can alleviate some of the drudge of managing your security configuration.
IP whitelisting – defining a set of approved client IP addresses for your system – has its flaws; keeping on top of those approvals can be a bit of a pain and you have to keep an eye on the risks of IP spoofing. But for those of us with a belt-and-braces approach to security it offers a little extra peace of mind and can be a useful tool when working with multiple-barrier security policies.
Whitelisting individual addresses
When it comes to Enterprise Java applications, discovering the client IP address seldom requires too much digging. In a Java Web application for example, simply calling getRemoteAddr() on the HttpServletRequest passed into your code will give you what you need and implementing your whitelist in a Filter will secure your entire web application.
Typically a simple string match on the client address will suffice:-
public interface NetworkAddressValidator { public boolean isApproved(String ipAddress); } public class SimpleNetworkAddressValidator implements NetworkAddressValidator { private String[] approvedAddressList = null; public SimpleNetworkAddressValidator( String[] approvedAddressList) { this.approvedAddressList = approvedAddressList; } @Override public boolean isApproved(String ipAddress) { for(String approvedAddress : approvedAddressList) { if(approvedAddress.equals(ipAddress)) { return true; } } return false; } }
Here we’ve defined an interface for our IP address validator and a simple implementation based on a plain string comparison of the supplied IP address with each address on our whitelist. Our implementation accepts the list as a constructor argument so could easily be configured through Spring.
Let’s create a quick unit test for our implementation – this seems an ideal candidate for a Spock test-case:-
class SimpleNetworkAddressValidatorTest extends spock.lang.Specification { def "Allowed Addresses"() { def cut = new SimpleNetworkAddressValidator( ["127.0.0.1", "192.168.0.1", "192.168.0.2", "192.168.0.3"] as String[]) expect: cut.isApproved(ipAddress) == true where: ipAddress | _ "127.0.0.1" | _ "192.168.0.1" | _ "192.168.0.2" | _ "192.168.0.3" | _ } def "Disallowed Addresses"() { def cut = new SimpleNetworkAddressValidator( ["127.0.0.1", "192.168.0.1", "192.168.0.2", "192.168.0.3"] as String[]) expect: cut.isApproved(ipAddress) == false where: ipAddress | _ "127.0.0.2" | _ "192.168.0.4" | _ "192.168.0.6" | _ "1.2.3.4" | _ } }
If we need to whitelist large numbers of IP addresses our simple implementation has scope for improvement. We could replace our private String array with a HashSet which will give us much faster responses on a long list. But long lists can be a pain to manage, and if they contain a lot of contiguous addresses (subnets) a solution which captures the subnet rather than all its individual addresses in our configuration would be better.
Whitelisting subnets
A quick and dirty solution would be to use startsWith() rather than equals() in our string comparison – this would let us whitelist 192.168.0.[0-255] for example simply by including 192.168.0 in our configuration. This breaks down though when subnets are more tightly refined. If we wanted to whitelist 192.168.0.[0-7] for example it’s a non-starter.
Fortunately the Apache Commons Net library has a rather useful and lightweight SubnetUtils class which can help. To get it, either add the following Maven dependency to your project or download the latest JAR.
<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.3</version> </dependency>
It will accept a subnet definition in CIDR format (e.g. 192.168.0.0/29) or with an old-school network mask (192.168.0.0 255.255.255.248) and a call to isInRange() will tell you whether the client IP address is included or not:-
import org.apache.commons.net.util.SubnetUtils; public class BetterNetworkAddressValidator implements NetworkAddressValidator { private SubnetUtils[] approvedAddressList = null; public BetterNetworkAddressValidator( String[] approvedAddressList) { this.approvedAddressList = new SubnetUtils[approvedAddressList.length]; for(int i = 0; approvedAddressList.length > i; i++) { if(approvedAddressList[i].indexOf("/") > 0) { this.approvedAddressList[i] = new SubnetUtils(approvedAddressList[i]); } else { this.approvedAddressList[i] = new SubnetUtils(approvedAddressList[i] + "/32"); } this.approvedAddressList[i].setInclusiveHostCount(true); } } @Override public boolean isApproved(String ipAddress) { for(SubnetUtils approvedAddress : approvedAddressList) { if(approvedAddress.getInfo().isInRange(ipAddress)) { return true; } } return false; } }
Our constructor now takes a list of either IP addresses or subnets in CIDR format. Where it finds an individual address it converts it into CIDR format by appending /32 – equivalent to a 255.255.255.255 network mask and therefore a subnet of one. Note the call to setInclusiveHostCount() for each subnet we create; without this the edge cases will be treat as excluded rather than included.
Finally we’ll tidy up our unit test to remove the contiguous IP list and replace it with the associated subnet:-
class BetterNetworkAddressValidatorTest extends spock.lang.Specification { def "Allowed Addresses"() { def cut = new BetterNetworkAddressValidator( ["127.0.0.1", "192.168.0.0/30"] as String[]) expect: cut.isApproved(ipAddress) == true where: ipAddress | _ "127.0.0.1". | _ "192.168.0.1" | _ "192.168.0.2" | _ "192.168.0.3" | _ } def "Disallowed Addresses"() { def cut = new BetterNetworkAddressValidator([ "127.0.0.1", "192.168.0.0/30"] as String[]) expect: cut.isApproved(ipAddress) == false where: ipAddress | _ "127.0.0.2". | _ "192.168.0.4" | _ "192.168.0.6" | _ "1.2.3.4" | _ } }
Having the option to whitelist contiguous blocks of IP addresses rather than just individual addresses can seem a little daunting if you’re planning to manage the math yourself. Fortunately it’s only a little extra work to use the SubnetUtils class and it can ease some of the pain of whitelisting a lot of addresses.