Skip to main content

Network Firewall & Access Control

Category: Security & Compliance · ⭐ Featured

Get this pack →

This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.

Put a configurable network firewall in front of any API — no code, no database. Air Pipe evaluates a declarative network: policy on every request before any action runs: IP allow/deny lists (CIDR + IPv6), proxy-aware client resolution that defeats X-Forwarded-For spoofing, per-IP rate limiting, GeoIP/ASN blocking, a safe log-only rollout mode, and custom deny responses. Set a baseline once for the whole config, then tighten or open it per route.

This pack is self-contained: every route is a pure transform that echoes back the IP the firewall resolved, so you can see each policy decision in the response body. Drive the examples by setting the X-Forwarded-For / X-Real-IP request headers (in production your proxy / load balancer sets these for you).

What it demonstrates​

#CapabilityWhere
1IP introspection — what the firewall actually seesGET /whoami
2IP allow list (CIDR, IPv4/IPv6)GET /admin/dashboard
3Spoof-resistant client IP via trusted_proxiesGET /secure/origin
4Per-IP rate limiting (sliding window)POST /api/echo
5GeoIP + ASN country/network blockingGET /restricted/region
6Monitor (log-only) mode for safe rolloutGET /monitor/canary
7Public exception via inherit: falseGET /health

Plus, applied config-wide as a baseline: a threat deny list with deny_check: all (catches a banned IP anywhere in the proxy chain) and a custom on_deny response.

Endpoints​

MethodRoutePolicyDescription
GET/whoamiinherits baselineEchoes the resolved client / socket / forwarded_chain / real_ip.
GET/admin/dashboardip.allow (CIDR)Only the corporate egress + private ranges may enter.
GET/secure/origintrusted_proxies + ip.allowResolves the true client behind your proxy; a forged leftmost XFF is ignored.
POST/api/echorate_limit 5 / 60s per IPEchoes the body; the 6th request in the window is throttled (429 + Retry-After).
GET/restricted/regiongeo.deny + asn.denyBlocks listed countries/ASNs (needs a GeoIP/ASN provider; fails open without one).
GET/monitor/canarymode: monitor + ip.allowSame lockdown rules, but log-only — nothing is blocked.
GET/healthinherit: falseAlways-200 status route, exempt from the baseline.

How the policy works​

# Config-wide baseline — applies to every interface below.
network:
source: client # client (default) | socket | real_ip
ip:
deny: ["203.0.113.0/24"]
deny_check: all # check every hop in the request chain, not just the source
on_deny:
status: 403
body: { error: forbidden, message: "Request blocked by the Air Pipe network firewall." }
  • Inheritance is tighten-only. A route inherits the baseline and may make it stricter: deny lists union, allow lists intersect. A route can never weaken a config-level deny — except by explicitly opting out with network: { inherit: false } (used here for /health).
  • Evaluation order per level: ip.deny → geo.deny → asn.deny → *.allow → rate_limit. The first rule that rejects wins.
  • source picks which captured IP is authoritative: client (leftmost X-Forwarded-For, else X-Real-IP, else socket), socket (raw TCP peer), or real_ip (X-Real-IP).
  • trusted_proxies resolves the real client by walking the X-Forwarded-For chain from the right and skipping your own proxy CIDRs — so a caller-supplied (spoofed) leftmost entry can't impersonate an allowed IP.

Setup​

No managed variables and no database. Deploy the pack and call it.

Sample IPs use the reserved documentation ranges (RFC 5737):

RangeRole in this pack
192.0.2.0/24 (TEST-NET-1)"corporate egress" / admin allow list
198.51.100.0/24 (TEST-NET-2)legitimate external users
203.0.113.0/24 (TEST-NET-3)the "bad neighborhood" / threat deny list
10.0.0.0/8your trusted load balancer / private subnet

Walkthrough​

Replace $BASE with your deployment URL (e.g. http://localhost:8080).

1. See what the firewall sees

curl -s "$BASE/whoami" -H "X-Forwarded-For: 198.51.100.42"
# { "client":"198.51.100.42", "socket":"...", "forwarded_chain":["198.51.100.42"],
# "real_ip":null, "country":null, "asn":null, "message":"This is what the Air Pipe firewall sees." }

A request whose chain touches the threat range is blocked by the baseline (deny_check: all sees it even behind a proxy):

curl -s -o /dev/null -w "%{http_code}\n" "$BASE/whoami" \
-H "X-Forwarded-For: 198.51.100.42, 203.0.113.5"
# 403

2. IP allow list (admin)

curl -s -o /dev/null -w "%{http_code}\n" "$BASE/admin/dashboard" -H "X-Forwarded-For: 192.0.2.10"   # 200 (corporate egress)
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/admin/dashboard" -H "X-Forwarded-For: 198.51.100.5" # 403

3. Anti-spoofing with trusted proxies

The route trusts 10.0.0.0/8 and only allows real clients in 198.51.100.0/24.

# Legit: real client 198.51.100.7 sits behind your LB (10.0.0.1) → allowed
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/secure/origin" \
-H "X-Forwarded-For: 198.51.100.7, 10.0.0.1" # 200

# Spoof: caller forges an allowed IP on the left; the real client (203.0.113.50)
# is the first untrusted hop from the right → blocked
curl -s -o /dev/null -w "%{http_code}\n" "$BASE/secure/origin" \
-H "X-Forwarded-For: 198.51.100.7, 203.0.113.50, 10.0.0.1" # 403

4. Rate limiting (5 requests / 60s per IP)

for i in $(seq 1 6); do
curl -s -o /dev/null -w "request $i -> %{http_code}\n" \
-X POST "$BASE/api/echo" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 198.51.100.20" \
-d '{"hello":"world"}'
done
# request 1..5 -> 200
# request 6 -> 429 {"error":"rate_limited", ...} (with a `Retry-After: 60` header)

5. Geo / ASN restriction

curl -s "$BASE/restricted/region" -H "X-Forwarded-For: 198.51.100.31"
# Denies RU/KP/IR and ASN 13335 when a GeoIP/ASN provider is configured.
# With no provider it fails OPEN (returns 200); set require_resolution: true to fail closed.

6. Monitor mode (dry-run a policy)

curl -s -o /dev/null -w "%{http_code}\n" "$BASE/monitor/canary" -H "X-Forwarded-For: 203.0.113.9"
# 200 — the allow list would block this IP, but monitor mode only LOGS the would-be denial.
# Flip `mode: monitor` → `enforce` once the logs look right.

7. Public exception

curl -s -o /dev/null -w "%{http_code}\n" "$BASE/health" -H "X-Forwarded-For: 203.0.113.5"
# 200 — inherit: false exempts /health from the baseline so uptime monitors are never blocked.

Make it yours​

  • Lock down the whole API: put your office/VPN CIDRs in the baseline ip.allow; every route inherits it. Add per-route exceptions with inherit: false.
  • Block threats: add bad IPs/ranges to ip.deny. Use deny_check: all to catch them even behind proxies.
  • Trust your edge: set trusted_proxies to your load balancer / CDN egress CIDRs so client IPs can't be spoofed.
  • Throttle abuse: tune rate_limit.requests / rate_limit.window per route.
  • Geofence: with a GeoIP/ASN provider configured, use geo.allow / geo.deny (ISO country codes) and asn.deny; set require_resolution: true to fail closed.
  • Roll out safely: ship a new policy in mode: monitor first, read the logs, then switch to enforce.

Configuration​

config.yml​

name: NetworkFirewall
description: >
An API-layer network firewall: IP allow/deny lists (CIDR + IPv6), proxy-aware
client resolution to defeat X-Forwarded-For spoofing, per-IP rate limiting,
GeoIP/ASN blocking, monitor (log-only) mode, and custom deny responses — all
declared per route. No database required.

docs: true

# ─────────────────────────────────────────────────────────────────────────────
# This pack needs NO managed variables and NO database. Every route is a pure
# transform that echoes back the IP the firewall resolved, so you can see each
# policy decision in the response body. Drive the examples by setting the
# `X-Forwarded-For` / `X-Real-IP` headers (a real proxy/load balancer sets these
# for you). All sample IPs use the reserved documentation ranges (RFC 5737):
#
# 192.0.2.0/24 (TEST-NET-1) — "corporate egress" / admin allow list
# 198.51.100.0/24 (TEST-NET-2) — legitimate external users
# 203.0.113.0/24 (TEST-NET-3) — the "bad neighborhood" / threat deny list
# 10.0.0.0/8 — your trusted load balancer / private subnet
#
# Pattern note: each GET action starts from `input: a|ip|` — the captured request
# IP object `{ client, socket, forwarded_chain, real_ip, country, asn }` — and
# annotates it with `add_attribute`. The POST echo route starts from `input: a|body|`
# and tags on the caller's IP via `a|ip::client|`.
# ─────────────────────────────────────────────────────────────────────────────

# Config-wide baseline. Applies to EVERY interface below as a starting point.
# Interfaces inherit and *tighten* it (deny lists union, allow lists intersect)
# unless they opt out with `network: { inherit: false }`.
network:
# Which captured IP is authoritative for allow/deny matching.
# client (default) — leftmost X-Forwarded-For, else X-Real-IP, else socket
# socket — the raw TCP peer (reliable when no proxy is in front)
# real_ip — the X-Real-IP header
source: client
ip:
# Known bad actors. Anything in this range is blocked across the whole API.
deny:
- "203.0.113.0/24"
# `all` checks every hop in the request chain (client, socket, X-Real-IP and
# all X-Forwarded-For entries), so a banned IP can't hide behind a proxy.
deny_check: all
# The response a blocked request receives (instead of leaking a real route).
on_deny:
status: 403
body:
error: forbidden
message: "Request blocked by the Air Pipe network firewall."

interfaces:

# ── 1. IP introspection ─────────────────────────────────────────────────────
# GET /whoami
# Shows exactly what the firewall sees: best-guess client IP, raw socket peer,
# the full X-Forwarded-For chain, and the X-Real-IP header. Inherits the
# baseline, so a request whose chain touches 203.0.113.0/24 is blocked (403).
whoami:
output: http
summary: Show the resolved client IP
description: >
Echoes the IP information the firewall captured for this request:
`client` (best-guess real client), `socket` (raw TCP peer),
`forwarded_chain` (every X-Forwarded-For hop) and `real_ip`. Subject to
the baseline threat deny list.
tags: [network, introspection]
response_example:
client: "198.51.100.42"
socket: "127.0.0.1"
forwarded_chain: ["198.51.100.42"]
real_ip: null
country: null
asn: null
message: "This is what the Air Pipe firewall sees."
actions:
- name: Identify
input: a|ip|
post_transforms:
- add_attribute:
message: "This is what the Air Pipe firewall sees."

# ── 2. IP allow list (CIDR) ─────────────────────────────────────────────────
# GET /admin/dashboard
# Only traffic from the corporate egress range or the private network may reach
# admin tooling. `allow` lists intersect with the baseline, and the baseline
# deny still applies on top — defence in depth in two lines of YAML.
admin/dashboard:
output: http
summary: IP-restricted admin route
description: >
Locked to a CIDR allow list (corporate egress + private network). Requests
from any other IP get the baseline 403. IPv4 and IPv6 CIDRs are supported.
tags: [network, access-control]
response_example:
client: "192.0.2.10"
socket: "10.0.0.1"
forwarded_chain: ["192.0.2.10"]
real_ip: null
access: granted
role: admin
message: "Welcome to the admin dashboard."
actions:
- name: Authorize
input: a|ip|
post_transforms:
- add_attribute:
access: granted
role: admin
message: "Welcome to the admin dashboard."
network:
ip:
allow:
- "192.0.2.0/24" # corporate egress
- "10.0.0.0/8" # private / VPN
# - "2001:db8::/32" # IPv6 ranges work too

# ── 3. Proxy-aware client resolution (anti-spoofing) ────────────────────────
# GET /secure/origin
# Without trusted_proxies, `source: client` trusts the leftmost X-Forwarded-For
# entry — which a caller can forge. With trusted_proxies set, the real client is
# resolved by walking the chain from the right and skipping your own proxies, so
# a spoofed leftmost value is ignored. Only real clients in 198.51.100.0/24 pass.
secure/origin:
output: http
summary: Spoof-resistant client IP allow list
description: >
Resolves the true client IP behind a trusted load balancer (10.0.0.0/8) and
enforces an allow list against it. A forged leftmost X-Forwarded-For entry is
ignored. `inherit: false` isolates this route so the lesson is purely the
trusted-proxy resolution.
tags: [network, anti-spoofing]
response_example:
client: "198.51.100.7"
socket: "10.0.0.1"
forwarded_chain: ["198.51.100.7", "10.0.0.1"]
real_ip: null
message: "Resolved real client IP behind the proxy chain."
actions:
- name: ResolveOrigin
input: a|ip|
post_transforms:
- add_attribute:
message: "Resolved real client IP behind the proxy chain."
network:
inherit: false
source: client
trusted_proxies:
- "10.0.0.0/8"
ip:
allow:
- "198.51.100.0/24"

# ── 4. Rate limiting ────────────────────────────────────────────────────────
# POST /api/echo
# A per-IP sliding-window limit. The first 5 requests in any 60s window from a
# given source IP pass; the 6th is throttled with 429 + Retry-After. Swap the
# numbers to taste; `per: ip` buckets by the resolved source IP.
api/echo:
output: http
method: POST
summary: Rate-limited echo endpoint
description: >
Echoes the posted JSON body plus the caller's IP, behind a per-IP rate limit
of 5 requests / 60s. The 6th request inside the window returns 429 with the
custom rate_limited body and a Retry-After header. POST a JSON object.
tags: [network, rate-limit]
request_example:
hello: world
response_example:
hello: world
client_ip: "198.51.100.20"
echoed: true
actions:
- name: Echo
input: a|body|
post_transforms:
- add_attribute:
client_ip: a|ip::client|
echoed: true
network:
inherit: false
rate_limit:
per: ip
requests: 5
window: "60s"
on_deny:
status: 429
body:
error: rate_limited
message: "Too many requests. Slow down and try again shortly."
headers:
Retry-After: "60"

# ── 5. GeoIP + ASN blocking ─────────────────────────────────────────────────
# GET /restricted/region
# Block whole countries and networks (e.g. abusive hosting / cloud ASNs). These
# rules need a configured GeoIP/ASN provider; with require_resolution:false they
# fail OPEN when enrichment is unavailable, so the route still serves traffic in
# this no-provider demo. Set require_resolution:true to fail CLOSED instead.
restricted/region:
output: http
summary: Country + ASN restriction
description: >
Denies a set of countries (ISO 3166-1 alpha-2) and ASNs. Requires a GeoIP /
ASN provider in production; falls open here because none is configured.
Flip require_resolution to true to block when the country/ASN can't be
resolved.
tags: [network, geo, asn]
response_example:
client: "198.51.100.31"
socket: "10.0.0.1"
forwarded_chain: ["198.51.100.31"]
real_ip: null
country: null
asn: null
message: "Region check passed (no GeoIP provider configured — failing open)."
actions:
- name: RegionCheck
input: a|ip|
post_transforms:
- add_attribute:
message: "Region check passed (no GeoIP provider configured — failing open)."
network:
inherit: false
geo:
deny: ["RU", "KP", "IR"]
require_resolution: false
asn:
deny: [13335] # example: a hosting/CDN ASN
require_resolution: false

# ── 6. Monitor (log-only) mode ──────────────────────────────────────────────
# GET /monitor/canary
# Roll out a policy safely. In `monitor` mode the firewall evaluates every rule
# and LOGS what it *would* have blocked, but never actually blocks. Here the
# allow list would reject everything except 192.0.2.0/24 — yet all requests pass,
# with the would-be denials recorded. Flip to `enforce` once the logs look right.
monitor/canary:
output: http
summary: Policy dry-run (monitor mode)
description: >
The same allow list as a locked-down route, but in monitor mode: would-be
denials are logged, not enforced, so you can validate a policy against live
traffic before turning it on. Every request returns 200.
tags: [network, monitor]
response_example:
client: "203.0.113.9"
socket: "10.0.0.1"
forwarded_chain: ["203.0.113.9"]
real_ip: null
mode: monitor
message: "Allowed — monitor mode logs would-be denials without blocking."
actions:
- name: Canary
input: a|ip|
post_transforms:
- add_attribute:
mode: monitor
message: "Allowed — monitor mode logs would-be denials without blocking."
network:
inherit: false
mode: monitor
ip:
allow:
- "192.0.2.0/24"

# ── 7. Public exception (escape hatch) ──────────────────────────────────────
# GET /health
# A status endpoint that must answer for everyone, including IPs the baseline
# would block. `inherit: false` with no rules of its own opts the route out of
# the config-wide policy entirely.
health:
output: http
summary: Public health check
description: >
Always-200 status route. `inherit: false` bypasses the baseline deny list so
uptime monitors are never blocked.
tags: [network, health]
response_example:
client: "203.0.113.9"
socket: "10.0.0.1"
forwarded_chain: ["203.0.113.9"]
real_ip: null
status: ok
service: network-firewall
time: "2026-01-01T00:00:00Z"
actions:
- name: Health
input: a|ip|
post_transforms:
- add_attribute:
status: ok
service: network-firewall
time: a|timestamp:datetimeutctz|
network:
inherit: false