Our customer in the B2B sector is encountering a challenge with their public API. Despite having implemented a custom method for generating long-lived API keys, they find themselves unable to enforce rate-limiting effectively. This absence of rate-limiting mechanisms poses significant challenges, potentially resulting in the overloading of their system due to excessive requests or the exploitation of their API by unauthorized users. Without proper rate-limiting controls in place, the customer faces risks to both the performance and security of their API infrastructure, necessitating a solution to mitigate these concerns and ensure the smooth operation of their services for their clients. Our customers wants to offer two tiers of service level agreements (SLAs) - gold and standard. Complicating matters further, the API key, integral to authentication, is transmitted via a custom HTTP header.
My solution involves leveraging the API Protection feature of BIG-IP APM in conjunction with a custom iRule. By utilizing this combination, our customer can effectively extract the API Keys from HTTP requests and enforce rate limiting on specific API endpoints. As for now they only want to enforce rate limiting on the POST endpoints. This approach empowers the customer to secure their API while efficiently managing and controlling access to critical endpoints, ensuring optimal performance and safeguarding against abuse or overload.
For my lab I used BIG-IP 16.1.
For developing a solution I needed an API. Since I was at a stage of my life, where I thought learning go might be beneficial, I found a simple boilerplate in Go and adjusted it to my liking. My API is available here: Gin API for Managing Gin Spirits.
My server has an OpenAPI file, you can either download it from my github or from https://api-server-ip:8000/openapi.json.
Start by creating an API Protection profile in the Access section, go to Access ›› API Protection : Profile and click Create. Give it a name and upload the OpenAPI file. Choose SSL Profile, and DNS Resolver. Check the Ignore Default Server box. Hit the Save button.
On the Paths tab everything is populated automatically from the OpenAPI file, you don't have to change anything here. Also on the Responses you have to change anything.
On the Rate Limiting tab create a Key with the following attributes:
- Name:
YourChoice
- Key Name: SourceIP
- Key Value:
%{perflow.client.ip.address}
Also on the Rate Limiting tab create two Properties with the following attributes:
-
Property Gold
- Name: <YourChoice_gold>
- Keys: SourceIP
- Request Quota: 120 / 1
- Spike Arrest: 1200 / 10
-
Property Standard
- Name: <YourChoice_standard>
- Keys: SourceIP
- Request Quota: 60 / 1
- Spike Arrest: 600 / 10
Now this is the time consuming part. On the Access Control tab click on Edit... next to Per Request Policy.
The Classify API Request (RCA) item is created automatically from the OpenAPI file.
My customer wants Rate Limiting only on the POST requests. In order to use the iRule which we will take a look at later, we need an iRule Event. Add one with the ID read-sla
.
After the iRule Event item add an item of the type Request Classification with two branches. One branch is for enforcing the GOLD SLA the other one is for enforcing the STANDARD SLA. Go the the Advanced view and add the following custom expressions:
expr {[mcget {perflow.custom}] == "gold"}
for the GOLD SLAexpr {[mcget {perflow.custom}] == "standard"}
for the STANDARD SLA
Finally, add for each branch one item of the type API Rate Limiting with the following settings.
- Response:
demo_api_ratelimiting_auto_response_api_rate_limiting3
- Rate Limiting Configuration:
demo_api_ratelimiting_auto_rate_limiting_gold
ordemo_api_ratelimiting_auto_rate_limiting_standard
NOTE: I am using my object names here, make sure you change to the names you used. These are the objects you created on the Rate Limiting tab.
The full Per Request Policy should look like this.
You can for sure summarize the Request Classification and the API Rate Limiting items in a Macro and also you can add logging to this Policy.
Now let's take a look at the iRule.
In the HTTP_REQUEST
event the iRule searches for the HTTP header with the name ApiKey (case-insensitive), looks up the SLA value from the Data Group and saves the value to a variable.
The ACCESS_PER_REQUEST_AGENT_EVENT
checks for the agent ID we set in the Per Request Policy. Then it checks if the agent ID is equal to read-sla
, this way the iRule knows it should execute further and evaluate the SLA.
If true, it will set the custom variable perflow.custom
to the SLA, either gold or standard.
This variable will then be used in the Per Request Policy.
Now we can attach both, the iRule and the API Protection profile to the Virtual Server.
# Enable (1) or disable (0) logging globally
when RULE_INIT {
set static::debug 1
}
# Access and analyze the HTTP header data for SLA value
when HTTP_REQUEST {
set sla [class lookup [HTTP::header value apikey] dg_apikeys]
if { $static::debug } {log local0. "Made it to HTTP_REQUEST event with SLA value $sla."}
}
# Evaluate SLA value during per-request access policy execution
when ACCESS_PER_REQUEST_AGENT_EVENT {
set id [ACCESS::perflow get perflow.irule_agent_id]
if { $id eq "read-sla" } {
if { $static::debug } {log local0. "Made it to iRule agent in perrequest policy with SLA value $sla."}
ACCESS::perflow set perflow.custom "$sla"
}
}
For the iRule to work we need a Data Group of the type string. This Data Group we store the API Keys and the associated SLAs.
Here's how it looks on tmsh
.
[root@ltm-apm-16:Active:Standalone] config # tmsh list ltm data-group internal dg_apikeys
ltm data-group internal dg_apikeys {
records {
9000 {
data gold
}
9001 {
data standard
}
}
type string
}
The solution can be tested with a simple cURL command, just run the following command:
curl --location 'https://192.168.57.100/gins' \
--header 'apikey: 9001' \
--header 'Content-Type: application/json' \
--data '{"id": "4","Name": "No.3 London Dry Gin 0,7 Liter","Description": "No.3 London Dry Gin 0,7 Liter – Die No.1 für einen Dry Martini","price": 39.49}'
--http2
Now in the LTM log (if logging is enabled in the iRule), you should see:
Apr 28 13:03:42 ltm-apm-16.mylab.local info tmm3[17819]: Rule /Common/rule_api_ratelimiting <HTTP_REQUEST>: Made it to HTTP_REQUEST event with SLA value standard.
Apr 28 13:03:42 ltm-apm-16.mylab.local info tmm3[17819]: Rule /Common/rule_api_ratelimiting <ACCESS_PER_REQUEST_AGENT_EVENT>: Made it to iRule agent in perrequest policy with SLA value standard.
This should change with the API Key you use, either the SLA is Gold (9000) or Standard (9001).
In the APM log you should see the following message, once the client exceeds his quota defined in the SLA.
Apr 28 20:12:42 ltm-apm-16.mylab.local notice tmm[11094]: 01870075:5: (null):/Common: API request with weight (1) violated the quota in rate limiting config(/Common/demo_api_ratelimiting_auto_rate_limiting_standard).
Apr 28 20:12:42 ltm-apm-16.mylab.local notice tmm[11094]: 0187008d:5: /Common/demo_api_ratelimiting_ap:Common:6600283561834577940: Execution of per request access policy (/Common/demo_api_ratelimiting_prp) done with ending type (Reject)
And in Postman you should see a HTTP response 429 - Too Many Requests.