API Firewall
Overview
This guide covers deploying and configuring Wallarm API Firewall on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images.
API Firewall is a reverse proxy with a built-in OpenAPI 3.0 validator. You give it the specification your API already publishes, and it sits in front of that API and checks traffic in both directions. It checks every incoming request against the declared paths, parameters, types and body schemas. It checks every response your service sends back against the declared response schemas. It refuses anything that does not match the contract.
This is a positive security model, the opposite of how most API protection works. Instead of maintaining a list of attacks to recognize, you allow only the traffic that your specification describes and reject everything else by default. The firewall refuses a wrongly typed parameter, a body with an undeclared field, a call to an undocumented endpoint, and a response that carries a field it should not, without anyone having to anticipate them.
API Firewall is written in Go, runs as a single process with no database, and is built for near-zero added latency in front of production traffic. It has no accounts, no datastore and no interface of its own, so a bare install demonstrates nothing. This SecureAnyCloud image therefore ships a complete reference deployment: the firewall, a demo upstream API, the OpenAPI 3.0 specification that describes it, and a browser console. The console sends live conforming and non-conforming requests through the firewall, so you can see the model working. When you are ready, you replace two files and the firewall protects your own service.
Wallarm API Firewall is an independent open source project. SecureAnyCloud produces this image, and it is not affiliated with, endorsed by, or sponsored by Wallarm. The image ships the free and open source software, licensed under the Mozilla Public License 2.0, unmodified.
What you get:
- Wallarm API Firewall v0.9.6, the official upstream
linux/amd64release binary, pinned by sha256 content hash (566db1cc…d37b44), not by a moving tag - The upstream Mozilla Public License 2.0, shipped on the image at
/usr/share/api-firewall/LICENSE.upstream - A SecureAnyCloud appliance console that shows what the firewall is enforcing and runs six live validation scenarios against it
- A demo upstream API and the OpenAPI 3.0 specification that describes it, so the appliance demonstrably works on first boot
- nginx as the single hardened front door, with an HTTP Basic gate whose credential is generated per VM on first boot
- Ubuntu 24.04 LTS, fully patched at build time, with unattended security updates enabled
Key facts:
Item Value
Platform Ubuntu 24.04 LTS
Default SSH user azureuser
Console and protected API http://<vm-ip>/ (HTTP Basic)
Firewall listener 127.0.0.1:8282
Firewall health API 127.0.0.1:9667
Demo upstream API 127.0.0.1:8080
Active specification /etc/api-firewall/openapi.yaml
Firewall configuration /etc/api-firewall/api-firewall.env
Per instance credentials /root/api-firewall-credentials.txt
Prerequisites
- An active Azure subscription with permission to create virtual machines
- An SSH key pair for
azureuser
- A network security group allowing inbound TCP 22 (SSH) and 80 (console and protected API) from your own address ranges
- The Azure CLI, if you prefer to deploy from a terminal
Recommended VM size: Standard_B2s (2 vCPU, 4 GB). API Firewall is a single lightweight Go process and adds very little overhead. Scale up only if the API you place behind it is itself demanding.
Deploy from the Azure Portal
- In the Azure Portal, choose Create a resource and search the Marketplace for Wallarm API Firewall on Ubuntu 24.04 LTS by SecureAnyCloud
- Select the offer and choose Create
- Choose your subscription, resource group and region
- Set the VM size to Standard_B2s
- Leave the administrator username as azureuser and upload or generate your SSH public key
- Under Inbound port rules, allow SSH (22) and HTTP (80)
- On the Disks tab, choose Standard SSD for the OS disk
- Select Review and create, then Create
Under Networking, restrict ports 22 and 80 to your own address ranges. An HTTP Basic gate protects the console, but there is no reason to expose it to the whole internet.
Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:api-firewall-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name api-firewall-01 \
--image sac1647283583153:api-firewall-ubuntu-24-04:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# Then open the console port, scoped to the ranges you control
az vm open-port --resource-group my-resource-group --name api-firewall-01 --port 80 --priority 900Connect to your VM
# If you do not have the address to hand, ask Azure for it first:
az vm show --resource-group my-resource-group --name api-firewall-01 -d --query publicIps -o tsv ssh azureuser@<vm-ip>Confirm the services are running
Four units make up the appliance. All four should report active.
for U in api-firewall.service sac-demo-api.service nginx.service api-firewall-firstboot.service; do printf '%-34s %s\n' "$U" "$(systemctl is-active $U)" doneExpected output:
api-firewall.service active sac-demo-api.service active nginx.service active api-firewall-firstboot.service activeapi-firewall-firstboot.service is a one-shot unit that runs once, on the first boot of this VM. Afterwards it reports active (exited), which shows that it completed rather than never ran.
Ask the firewall itself what it is:
curl -s --max-time 5 http://127.0.0.1:9667/v1/livenessExpected output:
{"status":"up","build":"0.9.6","host":"api-firewall-01"}Only ports 22 and 80 are reachable from outside the VM. The firewall, its health API and the demo upstream are all bound to loopback, so the front door is the only way in:
ss -lntH | awk '{print $4}' | sortRead the per-instance credentials
Nothing is baked into the image. API Firewall ships no accounts. The one credential that this appliance uses is the HTTP Basic gate in front of the console and the protected API. The first boot generated it on this virtual machine, and it exists nowhere else.
sudo cat /root/api-firewall-credentials.txtThe file is 0600 root:root, so only root can read it. It contains these keys:
Key Meaning
APIFW_CONSOLE_USERNAME The HTTP Basic username, always apifw
APIFW_CONSOLE_PASSWORD A 32 character password generated on this VM
APIFW_CONSOLE_URL The appliance console
APIFW_LIVENESS_URL The firewall liveness probe, as JSON
APIFW_READINESS_URL The firewall readiness probe, as JSON
APIFW_SPEC_URL The specification currently being enforced
First boot requests every URL in that file, and each must answer 200 before the boot can complete. The appliance therefore never hands you a link that it does not serve.
Prove the appliance is closed by default
Before you open anything, confirm that the gate works. Every route refuses an anonymous caller:
for R in / /openapi.yaml /v1/products /firewall/liveness; do printf ' %-22s HTTP %s\n' "$R" \
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1$R)" doneExpected output:
/ HTTP 401 /openapi.yaml HTTP 401 /v1/products HTTP 401 /firewall/liveness HTTP 401Every credential that an attacker would try first is refused too:
for D in admin:admin admin:password apifw:apifw wallarm:wallarm root:root guest:guest; do printf ' %-22s HTTP %s\n' "$D" \
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 -u $D http://127.0.0.1/)" doneThis is by design. During first boot, the appliance starts a throwaway loopback-only listener that uses this VM's real credential file. It proves against that listener that an unauthenticated request is answered with exactly 401, and only then writes the marker that permits nginx to start. If the proof fails, port 80 is never opened.
There is exactly one deliberate exception: a two-byte liveness probe for a load balancer, which reveals nothing:
curl -s -o /dev/null -w 'nginx-health: HTTP %{http_code}\n' http://127.0.0.1/nginx-healthOpen the console
Browse to your VM over HTTP and sign in with the username and password from Step 5.
http://<vm-ip>/The console reads the status cards from the running process and the live configuration file, not from hard-coded values. They always describe what the firewall is enforcing right now.
Watch the firewall allow and block real traffic
Choose Run all scenarios. Each scenario sends a real HTTP request through the firewall on this VM and shows exactly what came back.
The firewall proxies a request that the specification describes to the upstream, and validates the response on the way back:
A request that the specification does not describe never reaches the upstream:
To do the same from the command line, use the credentials from Step 5:
U=$(sudo grep '^APIFW_CONSOLE_USERNAME=' /root/api-firewall-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^APIFW_CONSOLE_PASSWORD=' /root/api-firewall-credentials.txt | cut -d= -f2-)
echo "ALLOWED conforming GET : HTTP $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" 'http://127.0.0.1/v1/products?limit=3')"
echo "BLOCKED wrong type : HTTP $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" 'http://127.0.0.1/v1/products?limit=not-an-integer')"
echo "BLOCKED over the maximum : HTTP $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" 'http://127.0.0.1/v1/products?limit=9999')"
echo "BLOCKED shadow endpoint : HTTP $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" http://127.0.0.1/v1/leak)"
echo "BLOCKED bad response : HTTP $(curl -s -o /dev/null -w '%{http_code}' -u "$U:$P" http://127.0.0.1/v1/products/0)"Expected output:
ALLOWED conforming GET : HTTP 200 BLOCKED wrong type : HTTP 403 BLOCKED over the maximum : HTTP 403 BLOCKED shadow endpoint : HTTP 403 BLOCKED bad response : HTTP 403When the firewall refuses a request, it gives the reason in a response header:
U=$(sudo grep '^APIFW_CONSOLE_USERNAME=' /root/api-firewall-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^APIFW_CONSOLE_PASSWORD=' /root/api-firewall-credentials.txt | cut -d= -f2-)
curl -s -D - -o /dev/null -u "$U:$P" 'http://127.0.0.1/v1/products?limit=not-an-integer' \
| tr -d '\r' | grep -iE '^(HTTP/|Apifw-Validation-Status)'Expected output:
HTTP/1.1 403 Forbidden Apifw-Validation-Status: request-parameter:unknown:limitThe five things that got blocked, and why
Request Why it was refused
GET /v1/products?limit=not-an-integer The specification types limit as an integer
GET /v1/products?limit=9999 The specification caps limit at 50
POST /v1/orders with {"productId":"one","quantity":0} Wrong type, below the declared minimum, and a required field missing
GET /v1/leak A shadow endpoint: it exists on the upstream but is not in the specification
GET /v1/products/0 Response validation: the upstream answered 200 with a price of "not-a-number" where the schema requires a number
The last two deserve a closer look. /v1/leak does exist on the demo upstream and does return data. The firewall refuses it only because the specification does not declare it. For /v1/products/0, the upstream is the component that misbehaves, and the firewall stops the malformed payload before the client sees it.
See what the specification allows
The console renders the enforced contract directly from the file that the firewall loaded:
You can read the same contract from the shell:
U=$(sudo grep '^APIFW_CONSOLE_USERNAME=' /root/api-firewall-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^APIFW_CONSOLE_PASSWORD=' /root/api-firewall-credentials.txt | cut -d= -f2-)
curl -s -u "$U:$P" http://127.0.0.1/openapi.json | jq -r '.paths | keys[]'Expected output:
/v1/orders /v1/products /v1/products/{productId} /v1/statusAnything not in that list is a shadow endpoint, and the firewall rejects it before it reaches the upstream.
Protect your own API
This is the step that the appliance exists for. Two things change: the specification and the upstream.
Copy your own OpenAPI 3.0 document into place, point the firewall at your service, and restart. Replace <your-domain> with the host that your API listens on.
# 1. Your own specification becomes the contract the firewall enforces
sudo cp /path/to/my-openapi.yaml /etc/api-firewall/openapi.yaml
sudo chown root:apifw /etc/api-firewall/openapi.yaml
sudo chmod 0640 /etc/api-firewall/openapi.yaml
# 2. Point the firewall at your service
sudo sed -i 's|^APIFW_SERVER_URL=.*|APIFW_SERVER_URL=http://<your-domain>:3000|' \
/etc/api-firewall/api-firewall.env
# 3. Restart, and republish the console's view so it describes the new contract sudo systemctl restart api-firewall.service sudo /usr/local/sbin/api-firewall-publish-config.sh # 4. Once your own API is behind the firewall, retire the demo upstream sudo systemctl disable --now sac-demo-api.serviceThe firewall refuses to start on an invalid configuration. A missing specification, an unparseable specification, an empty upstream or an invalid validation mode fails the preflight check. systemd then reports the start as failed instead of bringing up an appliance that would silently allow everything through. Check the outcome:
systemctl is-active api-firewall.serviceA note on your specification. The quality of the protection is the quality of your OpenAPI document. A field typed as a bare string with no constraints allows any string. Declare types, formats, minimums, maximums, required arrays and additionalProperties: false where you can. Describe your response schemas as well as your requests, because response schemas turn the response validator on.
Onboard an API safely with LOG_ONLY
If you switch straight to BLOCK in front of a live API whose specification may be incomplete, the firewall rejects real traffic. API Firewall has a monitoring mode for this situation.
sudo sed -i 's|^APIFW_REQUEST_VALIDATION=.*|APIFW_REQUEST_VALIDATION=LOG_ONLY|' /etc/api-firewall/api-firewall.env
sudo sed -i 's|^APIFW_RESPONSE_VALIDATION=.*|APIFW_RESPONSE_VALIDATION=LOG_ONLY|' /etc/api-firewall/api-firewall.env
sudo systemctl restart api-firewall.service for i in $(seq 1 20); do curl -sf -o /dev/null --max-time 3 http://127.0.0.1:9667/v1/liveness && break; sleep 1; done grep -E '^APIFW_(REQUEST|RESPONSE)_VALIDATION=' /etc/api-firewall/api-firewall.envExpected output:
APIFW_REQUEST_VALIDATION=LOG_ONLY APIFW_RESPONSE_VALIDATION=LOG_ONLYWhen you are satisfied that your specification is complete, switch both settings back:
sudo sed -i 's|^APIFW_REQUEST_VALIDATION=.*|APIFW_REQUEST_VALIDATION=BLOCK|' /etc/api-firewall/api-firewall.env
sudo sed -i 's|^APIFW_RESPONSE_VALIDATION=.*|APIFW_RESPONSE_VALIDATION=BLOCK|' /etc/api-firewall/api-firewall.env
sudo systemctl restart api-firewall.service for i in $(seq 1 20); do curl -sf -o /dev/null --max-time 3 http://127.0.0.1:9667/v1/liveness && break; sleep 1; done grep -E '^APIFW_(REQUEST|RESPONSE)_VALIDATION=' /etc/api-firewall/api-firewall.envIn LOG_ONLY, the firewall refuses nothing. It writes every request that would have been blocked to the log with the rule it broke, and reports undocumented endpoints that answer requests as shadow APIs. Run production traffic through the firewall, read the log and fix the gaps in your specification. Then switch both settings back to BLOCK.
The three modes are:
Mode Behaviour
BLOCK Refuse anything that does not match, with the configured status code. The shipped default
LOG_ONLY Allow everything, record what would have been refused
DISABLE Turn that direction of validation off entirely
Monitor the firewall
In proxy mode, the firewall's observability is its structured JSON log. The log records every block with a request id, the path, the method and the exact rule that rejected the request.
sudo journalctl -u api-firewall.service --no-pager -n 400 -o cat 2>/dev/null \
| grep 'request blocked' | tail -5 \
| jq -r '"blocked \(.method) \(.path) request_id=\(.request_id)"' || trueExpected output:
blocked GET /v1/products request_id=a614d8e5-c915-48d6-8680-a952e07823f4 blocked POST /v1/orders request_id=1c0f77a2-7bd2-4a05-9a1e-2c0a0b7a3f11Each of those is one line of JSON in the journal. In full, a single block looks like this, and it names the exact schema rule that rejected the request:
{"level":"error","error":"parameter \"limit\" in query has an error: value not-an-integer: an invalid integer: invalid syntax","request_id":"a614d8e5-...","host":"127.0.0.1", "path":"/v1/products","method":"GET","message":"Request validation error: request blocked"}To follow the log live while you test, run this command in a second terminal and leave it running:
sudo journalctl -u api-firewall.service -f -o catThe liveness and readiness probes return JSON and suit a load balancer or an uptime monitor. Readiness reflects whether the connection pool to your upstream is healthy, so it becomes unhealthy when your backend does:
U=$(sudo grep '^APIFW_CONSOLE_USERNAME=' /root/api-firewall-credentials.txt | cut -d= -f2-)
P=$(sudo grep '^APIFW_CONSOLE_PASSWORD=' /root/api-firewall-credentials.txt | cut -d= -f2-)
curl -s -u "$U:$P" http://127.0.0.1/firewall/readinessOptional: expose the firewall port directly
By default, the firewall listens on 127.0.0.1:8282 and nginx on port 80 is the only public entrance. This gives you one place to terminate TLS, one place to gate access and one access log. If you want clients to connect to the firewall directly, change its listener and open the port.
# After this, clients speak to http://<vm-ip>:8282/ instead of going through nginx
sudo sed -i 's|^APIFW_URL=.*|APIFW_URL=http://0.0.0.0:8282/|' /etc/api-firewall/api-firewall.env
sudo systemctl restart api-firewall.serviceThen allow inbound TCP 8282 in your network security group, scoped to the clients that need it. The firewall performs no authentication. It validates shape, not identity, so if you expose it directly, authentication is your API's responsibility.
Server components
Component Version Purpose
Wallarm API Firewall 0.9.6 OpenAPI 3.0 request and response validation proxy
nginx Ubuntu 24.04 stock The single public entrance and the HTTP Basic gate
SecureAnyCloud demo upstream API 1.0.0 The reference service the firewall protects out of the box
Ubuntu Server 24.04 LTS Operating system
Filesystem layout
Path Purpose
/usr/local/bin/api-firewall The upstream release binary
/etc/api-firewall/openapi.yaml The specification being enforced
/etc/api-firewall/api-firewall.env The firewall configuration
/usr/share/api-firewall/ Shipped templates, licence, artifact hash and build evidence
/var/www/api-firewall-console/ The appliance console and the published specification
/etc/nginx/api-firewall.htpasswd The per VM HTTP Basic credential
/etc/nginx/sites-available/api-firewall.conf The front door configuration
/root/api-firewall-credentials.txt This VM's credentials, mode 0600
/var/lib/sac/data/ First boot markers and the sentinel
Scripts and log files
Path Purpose
/usr/local/sbin/api-firewall-firstboot.sh Generates this VM's credential, proves the gate, starts the appliance
/usr/local/sbin/api-firewall-preflight.sh Fail closed guard that runs before the firewall and before nginx
/usr/local/sbin/api-firewall-publish-config.sh Republishes the console's view of the live configuration
/var/log/nginx/api-firewall.access.log Front door access log
/var/log/nginx/api-firewall.error.log Front door error log
journalctl -u api-firewall The firewall's own structured JSON log
On startup
The first time a VM created from this image boots, api-firewall-firstboot.service runs once. It generates this machine's console password, writes the HTTP Basic file, proves on a throwaway loopback listener that an unauthenticated request is refused with 401, creates the readiness markers and starts the three appliance services. It then confirms end to end that a conforming request is proxied and a non-conforming one is blocked, requests every URL it is about to advertise, and finally writes /var/lib/sac/data/api-firewall-firstboot.done.
Until those markers exist, api-firewall.service, sac-demo-api.service and nginx.service are all skipped by systemd rather than started, because each carries a ConditionPathExists on them. This makes the image closed by default. The services do not merely start late. They cannot start until the appliance is configured and the gate is proved on that specific machine.
On every later boot the sentinel already exists, so systemd skips the first boot unit and leaves your credential alone.
Day-to-day operation
systemctl status api-firewall.service --no-pager -n 0Restart the firewall after any configuration or specification change:
sudo systemctl restart api-firewall.serviceChange the console password at any time:
sudo htpasswd -bB /etc/nginx/api-firewall.htpasswd apifw '<new-password>'Confirm the specification that the firewall has loaded, which is the one it enforces:
sudo grep '^APIFW_API_SPECS=' /etc/api-firewall/api-firewall.envSecurity hardening
- Restrict the network security group. Allow ports 22 and 80 only from address ranges you control.
- Put TLS in front of it. nginx is already the single entrance, so add a certificate there and redirect port 80 to 443. The appliance ships with plain HTTP because it cannot know your hostname.
- Change the console password to one your team manages, with
htpasswd -Bas above.
- Know what the firewall does and does not do. It validates the shape of traffic against your contract. It is not an authentication layer or a rate limiter, so keep your API's own authentication in place behind it.
- Retire the demo upstream once your own service is behind the firewall, with
sudo systemctl disable --now sac-demo-api.service.
- Keep unattended security updates enabled. They are on by default in this image.
Troubleshooting
The console returns 401 and you are sure the password is right. Confirm that you are using the username apifw and the password from /root/api-firewall-credentials.txt on this VM. Every VM has a different password, so a password from another instance never works.
A request you expected to succeed is blocked with 403. The firewall is telling you that your specification does not describe the request. Read the reason:
sudo journalctl -u api-firewall.service --no-pager -n 400 -o cat 2>/dev/null \
| grep 'request blocked' | tail -3 \
| jq -r '"\(.method) \(.path) request_id=\(.request_id)"' || trueThe Apifw-Validation-Status response header carries the same verdict.
api-firewall.service will not start after a configuration change. The preflight guard refused to start it. The guard prints the exact reason:
sudo journalctl -u api-firewall.service --no-pager -n 40 \
| grep -i 'REFUSING TO START' || echo '(no preflight refusal recorded, the firewall configuration is valid)'Common causes are a specification path that does not exist, a document that is not valid OpenAPI 3, an empty APIFW_SERVER_URL, or a validation mode that is not BLOCK, LOG_ONLY or DISABLE.
nginx will not start. nginx depends on the first boot markers and on the credential file. If any of them is missing, systemd skips nginx instead of starting it:
ls -l /var/lib/sac/data/ /etc/nginx/api-firewall.htpasswdEverything is being blocked after you swapped in your own specification. Check that the paths in your document match the paths that your clients call, including any prefix. Switch to LOG_ONLY as described in Step 11, run real traffic through the firewall and read the log before you switch back to BLOCK.
The readiness probe reports not ready. The firewall cannot reach the upstream named in APIFW_SERVER_URL. Confirm your backend is listening and reachable from the VM.
Support
SecureAnyCloud packages this image and provides 24/7 support for it. Wallarm API Firewall is open source software under the Mozilla Public License 2.0. The license that ships with the binary is on the image at /usr/share/api-firewall/LICENSE.upstream.
For questions about this image or its deployment, contact SecureAnyCloud support. For questions about API Firewall itself, see the upstream documentation at https://wallarm.github.io/api-firewall/.
Deploy on Azure
Launch Wallarm API Firewall on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com