YARP ("Yet Another Reverse Proxy") is a reverse proxy toolkit for ASP.NET Core. This project extends YARP's functionality by adding firewall capabilities.
Being an extension to YARP, this project follows much of the conventions in the YARP project, both in terms of solution and class structure. This also means that it can be configured in the same way as YARP; it supports configuration files, as well as a configuration API for programmatic, in-process configuration.
This project is currently in an early stage (
At present, Yarp.Extensions.Firewall contains just a custom rule engine. The custom rule engine is heavily influenced by Azure WAF (found as part of Application Gateway and Front Door).
Firewalls with custom rules are configured per route (matching on the RouteId
configured in YARP). Each route's firewall has a few basic settings
RouteId
- the name of the route, as configured in YARPEnabled
- Enable (or disable)Mode
- how the firewall should operate;Detection
(log only), orPrevention
(enforce rules)BlockedStatusCode
- HTTP Status Code to return when a request is deniedRedirectUri
- URL to return when a request is redirectedRules
- the set of custom rules for this
Custom Rules are configured as sets of conditions, and are executed according to their given priority; all conditions in a rule must match the request for the rule to be enforced with the specified action (ie. there is an implicit AND
between all conditions in a rule). If all of a rule's conditions match the request, no other rules are evaluated. If no rules match, the request is implicitly allowed to continue to the YARP middleware.
RuleName
- the name or description given to the rulePriority
- a number indicating what priority the rule should be given.0
is highest priorityAction
- the action that should be taken when all conditionsAllow
- the request is explicitly allowed to continueBlock
- the request is denied and cannot continue; the firewall'sBlockedStatusCode
is returned to the clientLog
- the request is logged, and allowed to continueRedirect
- a Redirect response is returned to the client, with the location set to the firewall'sRedirectUri
Conditions
- the set of conditions that define this rule
A number of conditions are supported, with different configuration options depending on the property of the request being evaluated. All conditions have the below options
MatchType
- the type of value that will be evaluatedSize
- the length of a request property will be evaluatedString
- evaluation depends on a particular value in one of the request's propertiesIPAddress
- match on a given list or range of IP addressesGeoIP
- match on a given country as determined by MaxMind GeoIP2 from the IP address
Negate
- the evaluation result will be inverted; ie. if a match was not found, the condition will returntrue
(and vice versa)
When evaluating an IPAddress
MatchType
, either the client's socket address or the perceived remote address can be used for the match. That value will be evaluated against the given IP address, list of IP addresses, or CIDR range.
MatchVariable
- the property to retrieve the request's IP address fromSocketAddress
- use the IP address from the actual connection; if the request was previously proxied, this might not be the actual client's address but rather the address of the proxyRemoteAddress
- the perceived client's address; at present, this will be the first valid value from theX-Forwarded-For
header, falling back to the socket address if none was found
IPAddressOrRanges
- the IP address(es) to evaluate against, and accepts both IPv4 and IPv6 addresses. This may be either a single IP address, a comma-separated list of IP address, or a CIDR range
When MatchType
is Size
, evaluation is performed by comparing the configured MatchValue
to the length of the configured request property. Some request properties require an additional Selector
that specifies an additional key. A series of transformations can be done on the value prior to the evaluation itself as well.
MatchVariable
- the request property to be evaluated. Valid values areRequestMethod
- the HTTP Method for the request (GET
,POST
,HEAD
etc)QueryParam
- evaluate the length of the query parameter given bySelector
PostArgs
- evaluate the length of the HTTP Form POST parameter given bySelector
RequestPath
- evaluates the length of the relative URI, including the entire query stringRequestHeader
- evaluate the length of the particular request header given bySelector
RequestBody
- evaluate the length of the entire request bodyCookie
- evaluate the size of the particular request cookie given bySelector
Operator
- the type of comparison to use againstMatchValue
after transformations are appliedLessThan
GreaterThan
LessThanOrEqual
GreaterThanOrEqual
Selector
- a key indicating whichCookie
,RequestHeader
,PostArgs
, orQueryParam
value to use, if it existed in the requestMatchValue
- the value to be compared againstTransforms
- a list of transformations to be applied, in order
ASP.NET Core and Kestrel have their own limits on request sizes, and in general these should be preferred over general/global RequestBody
size rules.
Keep in mind that those limits will apply even if the firewall is in Detection
mode, as they are inherent to the underlying server itself.
A MatchType
of String
will evaluate request properties against a list of values to determine a match. Like the Size
evaluators, an additional key specified by Selector
is required for some request properties, and transformations can be applied before the value is evaluated.
MatchVariable
- the request property to be evaluated. Valid values areRequestMethod
- the HTTP Method for the request (GET
,POST
,HEAD
etc)QueryParam
- the query parameter given bySelector
PostArgs
- the HTTP Form POST parameter given bySelector
RequestPath
- the relative URI, including the entire query stringRequestHeader
- the particular request header given bySelector
RequestBody
- the entire request bodyCookie
- the particular request cookie given bySelector
Operator
- the type of case-sensitive string comparison to use for evaluation after transformations are appliedAny
- the property contains any valueEquals
- the property exactly equals one of theMatchValues
Contains
- the property contains any of theMatchValues
StartsWith
- the property starts with one of theMatchValues
EndsWith
- the property ends with one of theMatchValues
Regex
- the property matches one of the regular expression patterns given inMatchValues
Selector
- a key indicating whichCookie
,RequestHeader
,PostArgs
, orQueryParam
value to use, if it existed in the requestMatchValues
- a list of string values to be compared againstTransforms
- a list of transformations to be applied, in order
The GeoIP
value for MatchType
will use a MaxMind GeoIP2 Country database to look up the client's country based on the IP address from either the socket address or remote address, and evaluate this against a list of supplied country names.
MatchVariable
- the property to retrieve the request's IP address fromSocketAddress
- use the IP address from the actual connection; if the request was previously proxied, this might not be the actual client's address but rather the address of the proxyRemoteAddress
- the perceived client's address; at present, this will be the first valid value from theX-Forwarded-For
header, falling back to the socket address if none was found
MatchCountryValues
- a list of country names (not case sensitive) to be evaluated against
The path to a GeoIP2 or GeoLite2 Country database must be provided to use this evaluator, and is configured at the top level (adjacent to RouteFirewalls
) with the GeoIPDatabasePath
configuration property. As with all other configuration values, the database path can be updated without requiring a restart, and as MaxMind frequently updates the databases (at time of writing, twice weekly) frequent updating is encouraged.
No database files are provided in this project, however one to suit your purpose (commercial, enterprise, or free) can be obtained from MaxMind. Note you will need the Country database, and supplying any other type will fail to load the database and any configured GeoIP evaluators.
Tranformations can be applied to the request values for Size
and String
evaluators prior to any comparisons to do things like changing the case, trimming whitespace, or applying URL decoding/encoding. Tranforms
are applied in the order given in the condition configuration.
Uppercase
- convert the request value to upper-caseLowercase
- convert the request value to lower-caseTrim
- remove any whitespace characters from the start and end of the valueUrlDecode
- convert any URL-encoded characters. This also accounts for repeat encodings, a common bypass techniqueUrlEncode
- convert any special characters to their URL-encoded representation
(Case transformations don't affect Size
evaluations, and are automatically ignored in that case.)
Below is an example of what this configuration looks like (as used in ConfigurationConfigProviderTests
).
The parent section to "RouteFirewalls"
must be passed to the .LoadFromConfig()
extension method.
For example, place "RouteFirewalls"
inside the section used for YARP (eg. "ReverseProxy"
), alongside the "Routes"
and "Clusters"
.
{
// ...
"ReverseProxy": {
"Routes": {
"routeA": { ... },
"routeB": { ... }
},
"Clusters": { ... }
"RouteFirewalls": {
"routeA": {
"Enabled": true,
"Mode": "Prevention",
"RedirectUri": "https://localhost:10000/blocked",
"BlockedStatusCode": "Forbidden",
"Rules": {
"stringAndSize": {
"Priority": 10,
"Action": "Block",
"Conditions": [
{
"MatchType": "String",
"Operator": "Contains",
"MatchVariable": "QueryParam",
"Selector": "a",
"MatchValues": [ "1" ],
"Transforms": [
"Trim",
"UrlDecode",
"Uppercase"
]
},
{
"MatchType": "Size",
"Operator": "GreaterThanOrEqual",
"MatchVariable": "Cookie",
"Selector": "b",
"MatchValue": 100
}
]
},
"ipAddress1": {
"Priority": 11,
"Action": "Allow",
"Conditions": [
{
"MatchType": "IPAddress",
"IPAddressOrRanges": "2001::abcd",
"MatchVariable": "SocketAddress"
}
]
}
}
},
"routeB": {
"Enabled": true,
"Mode": "Detection",
"RedirectUri": "https://localhost:20000/blocked.html",
"BlockedStatusCode": "NotFound",
"Rules": {
"ipAddress2": {
"Priority": 5,
"Action": "Allow",
"Conditions": [
{
"MatchType": "IPAddress",
"IPAddressOrRanges": "192.168.0.0/16",
"MatchVariable": "RemoteAddress"
}
]
}
}
}
}
}
}
I'm eager to accept contributions and suggestions in any form. Please feel free to open an issue, discussion, or PR, or to message me on here or Mastodon.