Deployment guide

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/amd64 release 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.

1

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.

2

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 900
3

Connect 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>
4

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)" done

Expected output:

api-firewall.service active sac-demo-api.service active nginx.service active api-firewall-firstboot.service active

api-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/liveness

Expected 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}' | sort
5

Read 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.txt

The 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.

6

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)" done

Expected output:

/ HTTP 401 /openapi.yaml HTTP 401 /v1/products HTTP 401 /firewall/liveness HTTP 401

Every 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/)" done

This 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-health
7

Open 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.

8

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 403

When 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:limit

The 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.

9

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/status

Anything not in that list is a shadow endpoint, and the firewall rejects it before it reaches the upstream.

10

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.service

The 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.service

A 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.

11

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.env

Expected output:

APIFW_REQUEST_VALIDATION=LOG_ONLY APIFW_RESPONSE_VALIDATION=LOG_ONLY

When 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.env

In 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

12

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)"' || true

Expected output:

blocked GET /v1/products request_id=a614d8e5-c915-48d6-8680-a952e07823f4 blocked POST /v1/orders request_id=1c0f77a2-7bd2-4a05-9a1e-2c0a0b7a3f11

Each 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 cat

The 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/readiness
13

Optional: 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.service

Then 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 0

Restart the firewall after any configuration or specification change:

sudo systemctl restart api-firewall.service

Change 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.env

Security 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 -B as 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)"' || true

The 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.htpasswd

Everything 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

Deploy on Azure