Deployment guide

Batfish

Overview

Batfish builds a vendor-independent model of your network from the configuration files your devices already have, then answers questions about it before you deploy anything. Give it a snapshot of router, switch and firewall configurations. It tells you which BGP sessions will actually establish, which flows can reach which destinations, what an ACL really permits, which structures are defined but never used, and what would change if you pushed a candidate configuration.

Batfish reads Cisco IOS, IOS XE, IOS XR, NX OS, Arista EOS, Juniper Junos, Palo Alto PAN OS, Cumulus, SONiC, F5 BIG IP, Fortinet, Check Point and more, so a mixed estate is modelled as one network rather than a pile of syntaxes.

This SecureAnyCloud image runs the official Batfish service build 2025.07.07.2423 as a plain JVM systemd unit on OpenJDK 17, bound to loopback 127.0.0.1:19996, behind nginx on port 9996.

There is no web interface. Batfish is a service you query from a client, not a dashboard you log into. The supported client is pybatfish, the official Python SDK, and every example below is a real pybatfish session against the appliance. Port 9996 is the port pybatfish talks to by default, so the appliance is a drop-in for a stock client with no configuration beyond an address and a key.

What you get:

  • Batfish 2025.07.07.2423, pinned by registry digest and tied to upstream commit 7a0efda8257058576ae3787925e8c720dc918820
  • batfish.service on OpenJDK 17, bound to 127.0.0.1:19996, with the JVM heap bounded per VM
  • nginx on port 9996 with a per-VM gate, ready for TLS
  • Secure by default, no default credential. Batfish ships with no authentication anyone should rely on. Its API key header defaults to the published constant 00000000000000000000000000000000 and, under its default authorizer, is not checked at all. An exposed Batfish is an open relay that accepts arbitrary snapshot uploads and burns your CPU on anyone's analysis. This image never exposes it. The JVM binds loopback only, nginx is the sole ingress and is default-deny at server scope, and a batfish-firstboot.service oneshot generates a unique per-VM API key and HTTP Basic password on each VM's first boot.
  • A first-boot proof of the gate. Before it allows nginx to start at all, first boot proves against a throwaway listener that an anonymous request, the published default key, and a one-character mutation of your own key are each answered with exactly 401
  • Batfish's own file authorizer enabled with a per-VM key, as defence in depth. It runs against a users file holding only this VM's key, so even a caller who reached the loopback port directly is refused
  • pybatfish 2025.7.7.2423 preinstalled in a virtualenv at /opt/batfish/pybatfish-venv, version-matched to the server
  • A bundled 15-device example network and a batfish-demo command that analyses it end to end
  • 75 upstream question templates
  • A full dependency licence inventory at /usr/share/batfish/dependency-licences.txt and upstream licences at /usr/share/batfish/THIRD-PARTY-NOTICES
  • An unauthenticated /nginx-health endpoint for load balancer probes, answered by nginx itself
  • Ubuntu 24.04 LTS base, patched at build time, with unattended security updates left enabled

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet.

Batfish is a JVM service and is memory-hungry on real networks. The recommended VM size is Standard_B2ms (2 vCPU, 8 GB RAM). It gives roughly a 4.3 GB heap and comfortably handles the bundled example plus small to medium snapshots of a few dozen devices.

For hundreds of devices, or heavy dataplane and reachability work, use a Standard_D4s_v5 or Standard_E4s_v5 or larger. First boot sizes the heap from the machine's real memory, so moving to a bigger VM size is all that is required. There is nothing to tune by hand.

This appliance deliberately ships no swap, because Azure image certification rejects swap on the captured OS disk. The heap bound is what keeps a large analysis from being killed.

1

Deploy from the Azure Portal

Search the Marketplace for Batfish on Ubuntu 24.04 and choose your VM size.

Attach a network security group (NSG) that allows TCP 22 (SSH) from your management network and TCP 9996 (the Batfish API) from the workstations, CI runners and automation that will query it.

Batfish holds your device configurations, so treat 9996 as an administrative port and do not open it to the internet. Front it with TLS in production (see Step 10).

2

Deploy from the Azure CLI

az vm image terms accept --urn sac1647283583153:batfish-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-rg \
  --name batfish-01 \
  --image sac1647283583153:batfish-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard
az network nsg rule create \
  --resource-group my-rg \
  --nsg-name batfish-01NSG \
  --name allow-batfish \
  --priority 1010 \
  --protocol Tcp --destination-port-ranges 9996 \
  --source-address-prefixes <your-mgmt-cidr> \
  --access Allow
3

Connect via SSH

SSH in as azureuser:

ssh azureuser@<vm-ip>
4

Verify the services and the whole port surface

First boot generates this VM's secrets, proves the gate, starts Batfish and runs the bundled analysis. It completes in well under a minute.

Check that everything is up, and confirm that the only externally reachable ports are SSH and the gate:

systemctl is-active batfish.service nginx.service batfish-firstboot.service ss -lntp grep BATFISH_HEAP_OPTS /etc/default/batfish curl -s http://127.0.0.1:9996/nginx-health

All three services report active. The socket list shows exactly one routable Batfish-related listener, nginx on 0.0.0.0:9996, with the JVM itself on 127.0.0.1:19996 and nothing else. The heap line shows the bound that first boot computed for this machine.

5

Read your per-VM API key

Every secret on this appliance is generated on this VM, on its first boot. Nothing is baked into the image, and no two VMs share a credential. Read the credentials file:

sudo ls -l /root/batfish-credentials.txt
sudo cat /root/batfish-credentials.txt

BATFISH_API_KEY is what a pybatfish client presents. BATFISH_USERNAME and BATFISH_PASSWORD are an HTTP Basic credential for curl and any generic HTTP client.

Now prove that the appliance refuses everything else:

BF_URL=http://127.0.0.1:9996/v2/networks
KEY=$(sudo grep -m1 '^BATFISH_API_KEY=' /root/batfish-credentials.txt | cut -d= -f2-) DEFAULT_KEY=00000000000000000000000000000000
ANON=$(curl -s -o /dev/null -w '%{http_code}' "$BF_URL")
DEF=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Batfish-Apikey: $DEFAULT_KEY" "$BF_URL")
MINE=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Batfish-Apikey: $KEY" "$BF_URL") printf 'anonymous -> HTTP %s\n' "$ANON" printf 'published default API key -> HTTP %s\n' "$DEF" printf 'your own per VM API key -> HTTP %s\n' "$MINE" test "$ANON" = 401 && test "$DEF" = 401 && test "$MINE" = 200 && echo GATE_PROOF_OK

The first two return 401, and only the third returns 200. The published default key is refused deliberately. A pybatfish client that has not been given a key sends exactly that value, so an unconfigured client cannot reach your network data by accident.

6

Run the bundled example analysis

The appliance ships upstream's own example network: 13 Cisco IOS devices and 2 hosts across three autonomous systems, with external BGP announcements. A batfish-demo command uploads it and analyses it through the gate with a stock pybatfish client.

First boot has already run this once. Run it again whenever you want to confirm that the service is healthy end to end.

sudo batfish-demo

The command parses the snapshot, reports any parse or conversion issues, lists the devices Batfish modelled, and evaluates every BGP session in the network. UNIQUE_MATCH means the two ends agree and the session will establish. Anything else is a finding you want to know about before deployment.

The log of the first-boot run is kept at /var/log/batfish-firstboot-demo.log:

sudo tail -20 /var/log/batfish-firstboot-demo.log
7

Query the appliance from your own workstation

Install the client, and pin it to the same version as the server so that the client and service question sets match:

pip install pybatfish==2025.7.7.2423

Then point a Session at the appliance. This is the whole client-side configuration: an address, the port, and the API key you read in Step 5:

from pybatfish.client.session import Session bf = Session( host="<vm-ip>", port=9996, api_key="<paste BATFISH_API_KEY from Step 5>", ) bf.set_network("my-network") bf.init_snapshot("/path/to/my/configs", name="candidate", overwrite=True) print(bf.q.initIssues().answer().frame()) print(bf.q.bgpSessionCompatibility().answer().frame())

pybatfish always sends its key in the X-Batfish-Apikey header, which is exactly what the gate checks, so no client-side changes, wrappers or proxies are needed.

For other HTTP clients, the same key is also accepted as Authorization: Bearer <key> or in X-Api-Key. The HTTP Basic credential from Step 5 works too. Run the following on the appliance, where it reads its own credentials file:

BF_USER=$(sudo grep -m1 '^BATFISH_USERNAME=' /root/batfish-credentials.txt | cut -d= -f2-)
BF_PASS=$(sudo grep -m1 '^BATFISH_PASSWORD=' /root/batfish-credentials.txt | cut -d= -f2-)
curl -sf -u "${BF_USER}:${BF_PASS}" http://127.0.0.1:9996/v2/networks && echo BASIC_AUTH_OK
8

Trace every path across the network

Reachability is where Batfish earns its keep. Ask it where a packet would actually go, and it returns every equal-cost path, hop by hop, with a disposition for each:

from pybatfish.datamodel.flow import HeaderConstraints answer = bf.q.traceroute( startLocation="@enter(as2dept1[GigabitEthernet1/0])", headers=HeaderConstraints(dstIps="1.0.2.1", srcIps="2.34.201.10"), ).answer().frame() for trace in answer.iloc[0]["Traces"]: print(trace.disposition, " -> ".join(hop.node for hop in trace.hops))

Run that against the bundled example on the appliance itself:

sudo /opt/batfish/pybatfish-venv/bin/python - "$(sudo grep -m1 '^BATFISH_API_KEY=' /root/batfish-credentials.txt | cut -d= -f2-)" <<'PYEOF' import sys, logging logging.getLogger("pybatfish").setLevel(logging.ERROR) from pybatfish.client.session import Session from pybatfish.datamodel.flow import HeaderConstraints bf = Session(host="127.0.0.1", port=9996, api_key=sys.argv[1]) bf.set_network("sac-example") bf.set_snapshot("example") answer = bf.q.traceroute( startLocation="@enter(as2dept1[GigabitEthernet1/0])", headers=HeaderConstraints(dstIps="1.0.2.1", srcIps="2.34.201.10"), ).answer().frame() n = 0 for _, row in answer.iterrows(): for trace in row["Traces"]: n += 1 print(f"path {n}: {trace.disposition}, {len(trace.hops)} hops") print(" " + " -> ".join(hop.node for hop in trace.hops)) print(f"{n} equal-cost paths, AS2 access switch to AS1 edge") print("ipOwners:", len(bf.q.ipOwners().answer().frame()), "addresses inventoried") print("undefinedReferences:", len(bf.q.undefinedReferences().answer().frame()), "defects found") PYEOF

Batfish computes the dataplane on demand and returns all four equal-cost paths from the AS2 access switch out to the AS1 edge, seven hops each.

ipOwners inventories every address in the snapshot. undefinedReferences finds structures that a configuration refers to but never defines. This is a class of latent defect that static analysis catches and a ping never will.

9

Analyse your own network

Batfish reads a snapshot, a directory of the configuration files your devices already produce. The minimum layout is a configs/ subdirectory containing one file per device, named however you like:

my-snapshot/ configs/ core-sw-01.cfg edge-rtr-01.cfg fw-01.conf

Collect those with whatever you already use: RANCID, Oxidized, NAPALM, your vendor's own archive, or a git repository of golden configurations. Then pass the directory to init_snapshot().

Optional subdirectories let you model more: hosts/ for host JSON, iptables/ for host firewalls, batfish/isp_config.json to model upstream ISPs, and an external_bgp_announcements.json to inject routes your peers advertise. The bundled example shows all of these:

find /usr/share/batfish/example-snapshot -type f | sort

List the questions available to you:

ls /usr/share/batfish/questions/stable/ | head -40

Batfish never touches your live devices. It reads text and reasons about it, so running it against a candidate configuration change is safe.

10

Enable HTTPS

The gate speaks plain HTTP, so the appliance works immediately on a VM address. For production, terminate TLS in the same nginx server block. First back up the site file and test the configuration:

sudo cp /etc/nginx/sites-available/batfish.conf /root/batfish.conf.bak
sudo nginx -t

Add your certificate and key, change listen 9996; to listen 9996 ssl;, and reload nginx. Leave the auth_basic and auth_basic_user_file directives and the proxy_set_header X-Batfish-Apikey line exactly as they are. They are the gate, and nginx refuses to start without them.

Then point clients at https:// and set ssl=True on the Session. An Azure Application Gateway or Front Door in front of the VM works equally well.

Ports, the security model, and how clients authenticate

This appliance opens only these ports:

  • 22, all interfaces: SSH administration
  • 9996, all interfaces: the Batfish API, behind the nginx gate
  • 19996, 127.0.0.1 only: the Batfish JVM, not reachable from the network

nginx is default-deny at server scope, so every route inherits the gate, and no path that reaches the service is exempt. The single unauthenticated route is /nginx-health. nginx answers it itself from a static literal and proxies nothing, so a load balancer probe reveals nothing about your network data.

A request is admitted if, and only if, it presents one of this VM's own generated secrets, compared as an exact string:

  • this VM's API key in X-Batfish-Apikey, the pybatfish idiom
  • this VM's API key in Authorization: Bearer <key>
  • this VM's API key in X-Api-Key
  • HTTP Basic with this VM's username and password

Anything else, including a one-character mutation, is answered 401. Batfish's own authorizer reads only X-Batfish-Apikey, so the gate normalises the credential and forwards this VM's key to the service on every request that got past it. All four idioms therefore work end to end. That variable is empty in the shipped image, so the image cannot authenticate to itself.

Both nginx.service and batfish.service carry an ExecStartPre guard. It refuses to start if the gate map, the credential file, the authorizer users file or the heap bound are missing, empty or still in their shipped form. In addition, nginx.service is not even considered for start until first boot has written a marker proving that the gate denies an unauthenticated request. If you take the credential file away, nginx stays down rather than coming up open.

Persistence, first boot and updates

Networks and snapshots you upload live under /var/lib/batfish/containers, which is on the OS disk and survives reboots. Back that directory up if the analyses matter to you. It is safe to delete a network through the API to reclaim space.

First boot runs once. It is gated on /var/lib/sac/data/batfish-firstboot.done, so a reboot does not rotate your key. To rotate credentials, remove the sentinel and the per-VM state, and restart the unit. Check the unit's status with:

sudo systemctl status batfish-firstboot.service --no-pager

The OS keeps patching itself through unattended-upgrades, which includes the OpenJDK the service runs on. That is why the JRE comes from the Ubuntu archive rather than from inside a container image.

To move to a newer Batfish build, deploy a fresh SecureAnyCloud image version rather than patching in place, so that the build stays pinned and verified.

The full dependency licence inventory for everything in the image, with each dependency's resolved licence, is at:

head -20 /usr/share/batfish/dependency-licences.txt

Support

Batfish is licensed Apache 2.0 and this image carries no additional licence restrictions. Upstream licences, including the MIT grant covering the bundled JavaBDD library, are reproduced in full at /usr/share/batfish/THIRD-PARTY-NOTICES.

SecureAnyCloud provides 24/7 support for this image with a 24-hour response SLA. Email sales@secureanycloud.com.

Deploy on Azure

Launch Batfish 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