how to allow list IPs? (multiple IPs)

Hello everyone, I hope everything is well with you. I was wondering how I could dynamically whitelist IP addresses using access control as in my current scenarios. What I'm doing is creating a kvm and storing the IP in a variable, then retrieving this IP from the kvm and storing it in a variable, then passing this variable to the access entity policy, however this is just for a single IP;

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<KeyValueMapOperations async="false" continueOnError="false" enabled="true" name="Key-Value-Map-Operations-1" mapIdentifier="Dynamic_IP">
    <DisplayName>Key Value Map Operations-1</DisplayName>
    <Properties/>
    <ExclusiveCache>false</ExclusiveCache>
    <ExpiryTimeInSecs>300</ExpiryTimeInSecs>
    <Get assignTo="ip">
        <Key>
            <Parameter>kvm.ip.value</Parameter>
        </Key>
    </Get>
    <Get assignTo="i">
        <Key>
            <Parameter>ip1</Parameter>
        </Key>
    </Get>
    <Scope>environment</Scope>
</KeyValueMapOperations>

 

and then I use access entity polices

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AccessControl async="false" continueOnError="false" enabled="true" name="Access-Control-1">
    <DisplayName>Access Control-1</DisplayName>
    <Properties/>
    <IPRules noRuleMatchAction="ALLOW">
        <MatchRule action="DENY">
            <SourceAddress mask="32">{ip}</SourceAddress>
        </MatchRule>
    </IPRules>
</AccessControl>

 

but how can I whitelist 100s of IPs? I am happy to create 100s of entry of in the same KVM but the thing is that how can i fetch all the 100s of kvm entries(IP) at a time, If we create 100s of variable in the KVM policy that would be tricky and might be cause of problematics   I don't want to create separate variable  for separate KVM entries in the Policy.

0 7 1,443
7 REPLIES 7

Even if you store 100s of IPs in the KVM and then are able to retrieve them, you would then need to have hundreds of SourceAddress elements in your AccessControl policy.  A dynamic number of elements in fact. And that means modifying the AccessControl policy for each change, and that means redeploying the proxy.  This is probably a bad way to do things. 

A better way might be to use KVM to store a LIST of IP Addresses, and then use JS to parse the list and compare the inbound IP to the members of the list.  If you want to update the list, no problem, just use the KVM API to do it.  Then the next time the KVM is loaded, the JS step will see the updated list. 

Assuming the list stored in the KVM is a JSON-formatted array, the JS would be something like this: 

 

var ips = JSON.parse(context.getVariable('allowlist-variable-loaded-from-kvm'));
// find true Client IP on Apigee X: 
// see https://www.googlecloudcommunity.com/gc/Cloud-Product-Articles/How-to-Find-the-True-Client-IP-in-Apigee-X/ta-p/165568
var xffArray = context.getVariable("request.header.x-forwarded-for.values");
var xffArrayLength = context.getVariable("request.header.X-Forwarded-For.values.count");
var clientIp = context.getVariable("request.header.x-forwarded-for." + (xffArrayLength - 2));

var found = ips.find(function(item) { return item == clientIp; });
if (!found) {
  throw new Error('The client IP address is not on the allowlist');
}

 

Throwing an error from within a JS causes a fault.  You could then handle that Fault with the FaultRules and issue the desired response to the client.  

This will work for 2 to 1000 or more IP addresses. 

And of course there are variations on this idea: you could register valid IP addresses for each client ID, as a custom attribute on the Application/Client. In the API Proxy, after VerifyApiKey or VerifyAccessToken, the custom attribute is automatically loaded into a context variable - no need for a call to KVM.  You could use basically the same JavaScript above to validate the IP address by client.  Just reference a different variable in line 1.

There are other variations possible, too. 

That's a good approach using JavaScript.

I just have a question on this. This will work for the condition mask value 32, that is for a single IP Address. How would I have to use this approach if I have to use mask value less than 32? That means the IP Address range will have multiple IP Addresses.

You can convert IP addresses and CIDR blocks to integers and compare them.  Something like this should work: 

 

function ipReducer(int, oct) {
  return (int << 8 ) + parseInt(oct, 10);
}

function ip4ToInt(ip) {
  return ip.split('.').reduce(ipReducer, 0) >>> 0;
}

function isIp4InCidr(ip) {
  const normalizedIp = ip4ToInt(ip);
  return function(cidr) {
    const [range, bits] = cidr.split('/');
    const mask = ~(Math.pow(2, (32 - bits)) - 1);
    const normalizedRange = ip4ToInt(range);
    return (normalizedIp & mask) == (normalizedRange & mask);
  };
}

function isIp4InCidrs(ip, cidrs) { return cidrs.some(isIp4InCidr(ip)); }

// usage: 
// var check = isIp4InCidrs('192.168.1.5', ['10.10.0.0/16', '192.168.1.1/24']); // true

 

That works very well. Thanks for the solution, @dchiesa1 !

Just a note, the 2nd line of the code is actually "return ( int << 8 ) + parseInt(oct, 10);". Looks like it got messed up due to formatting.

yes, thanks for that correction!  Not sure what happened to the code formatting.

It would be really helpful if you provide ip6 counterpart of the same @dchiesa1 

@surbhi123 thanks for raising a very common problem.

@dchiesa1 Thank You! for sharing solution. This works.