Network Firewall & Access Control
Category: Security & Compliance · ⭐ Featured
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
| # | Capability | Where |
|---|---|---|
| 1 | IP introspection — what the firewall actually sees | GET /whoami |
| 2 | IP allow list (CIDR, IPv4/IPv6) | GET /admin/dashboard |
| 3 | Spoof-resistant client IP via trusted_proxies | GET /secure/origin |
| 4 | Per-IP rate limiting (sliding window) | POST /api/echo |
| 5 | GeoIP + ASN country/network blocking | GET /restricted/region |
| 6 | Monitor (log-only) mode for safe rollout | GET /monitor/canary |
| 7 | Public exception via inherit: false | GET /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
| Method | Route | Policy | Description |
|---|---|---|---|
| GET | /whoami | inherits baseline | Echoes the resolved client / socket / forwarded_chain / real_ip. |
| GET | /admin/dashboard | ip.allow (CIDR) | Only the corporate egress + private ranges may enter. |
| GET | /secure/origin | trusted_proxies + ip.allow | Resolves the true client behind your proxy; a forged leftmost XFF is ignored. |
| POST | /api/echo | rate_limit 5 / 60s per IP | Echoes the body; the 6th request in the window is throttled (429 + Retry-After). |
| GET | /restricted/region | geo.deny + asn.deny | Blocks listed countries/ASNs (needs a GeoIP/ASN provider; fails open without one). |
| GET | /monitor/canary | mode: monitor + ip.allow | Same lockdown rules, but log-only — nothing is blocked. |
| GET | /health | inherit: false | Always-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:
denylists union,allowlists intersect. A route can never weaken a config-level deny — except by explicitly opting out withnetwork: { 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. sourcepicks which captured IP is authoritative:client(leftmostX-Forwarded-For, elseX-Real-IP, else socket),socket(raw TCP peer), orreal_ip(X-Real-IP).trusted_proxiesresolves the real client by walking theX-Forwarded-Forchain 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):
| Range | Role 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/8 | your 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 withinherit: false. - Block threats: add bad IPs/ranges to
ip.deny. Usedeny_check: allto catch them even behind proxies. - Trust your edge: set
trusted_proxiesto your load balancer / CDN egress CIDRs so client IPs can't be spoofed. - Throttle abuse: tune
rate_limit.requests/rate_limit.windowper route. - Geofence: with a GeoIP/ASN provider configured, use
geo.allow/geo.deny(ISO country codes) andasn.deny; setrequire_resolution: trueto fail closed. - Roll out safely: ship a new policy in
mode: monitorfirst, read the logs, then switch toenforce.
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