GoFlow2 on Ubuntu 24.04 on Azure User Guide
Overview
GoFlow2 is an open source network flow collector written in Go. Routers, switches, firewalls and hypervisors can export a summary of every conversation crossing them, but they each speak a different dialect: NetFlow v5, NetFlow v9, IPFIX or sFlow v5. GoFlow2 listens for all of them at once, decodes the templates and samples, and emits every record in one common schema so everything downstream sees the same fields no matter which vendor sent it. Records come out as newline delimited JSON or protocol buffers, written to a file for a log shipper to collect or published straight onto a Kafka topic.
GoFlow2 is deliberately the ingest stage and nothing more, and it ships with no web interface and no authentication of its own. The cloudimg image supplies the operational layer around it: the pinned GoFlow2 2.2.6 binary runs under systemd as a dedicated unprivileged goflow2 user, its HTTP server is bound to 127.0.0.1:8080 so the Prometheus metrics are never exposed directly, and an nginx reverse proxy on port 80 puts a per-VM HTTP Basic Auth gate in front of them. Decoded flows are written to a dedicated Azure data disk mounted at /var/lib/goflow2, because flow volume filling a root filesystem is the classic way a collector fails. Backed by 24/7 cloudimg support.
What is included:
- GoFlow2 2.2.6 installed as a single pinned Go binary running as the
goflow2systemd service - NetFlow v5, NetFlow v9 and IPFIX collection on
2055/udp, and sFlow v5 collection on6343/udp - Decoded flows written as newline delimited JSON to
/var/lib/goflow2/flows.json - A dedicated Azure data disk at
/var/lib/goflow2so flow volume never fills the OS disk - Per-VM HTTP Basic Auth (user
admin) protecting the Prometheus metrics, with a unique password generated on first boot - GoFlow2's HTTP server bound to
127.0.0.1:8080only, with nginx on port 80 as the sole gate - An unauthenticated
/healthzendpoint for Azure Load Balancer health probes goflow2-send-test-flow, a bundled synthetic NetFlow v5 generator for proving the collector decodes- logrotate keeping 14 compressed generations, rotating with a SIGHUP reopen so no record is lost
- Kernel UDP receive buffers tuned for bursty exporter traffic
goflow2.service+nginx.serviceas systemd units, enabled and active- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point for tens of thousands of flows per second; scale up the VM size and the data disk if you are collecting from a large estate. NSG inbound: allow 22/tcp from your management network, 80/tcp for the metrics endpoint, and 2055/udp plus 6343/udp from your flow exporters. NetFlow, IPFIX and sFlow carry no credentials by protocol design, so restricting those two UDP rules to your own exporter address ranges is the correct control, not an optional hardening step.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for GoFlow2 by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Review the dedicated data disk on the Disks tab, then Review + create -> Create. The UDP collector ports are added in Step 7.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name goflow2 \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name goflow2 --port 80 --priority 1010
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the collector is running
First boot generates this VM's metrics password, starts nginx and runs a self test, which takes a few seconds after the VM reports ready. Confirm both services are active, check the GoFlow2 version, and look at the three sockets the appliance opens.
systemctl is-active goflow2.service nginx.service
Both lines report active.
/usr/local/bin/goflow2 -v
GoFlow2 v2.2.6 (2025-12-27T05:02:22+0000)
Now the sockets. The two UDP collectors listen on all interfaces so your exporters can reach them, while the HTTP server that serves the metrics is bound to loopback only.
sudo ss -ulnp | grep -E ':(2055|6343) '
sudo ss -tlnp | grep ':8080 '
UNCONN 0 0 *:2055 *:* users:(("goflow2",pid=3089,fd=7))
UNCONN 0 0 *:6343 *:* users:(("goflow2",pid=3089,fd=8))
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("goflow2",pid=3089,fd=6))

Step 5 - Retrieve this VM's metrics password
GoFlow2 has no accounts of its own, so the gate in front of its metrics is nginx HTTP Basic Auth with user admin and a password generated uniquely on this VM's first boot. It is never baked into the image.
sudo cat /root/goflow2-credentials.txt
The file records the metrics URL, the username, the password and the addresses your exporters should send to. Store the password in your password manager. Only the bcrypt hash is kept on the VM, in /etc/nginx/.goflow2.htpasswd.
Step 6 - Prove the collector decodes flows
Before you point production hardware at a new collector, prove it is actually decoding. The image ships goflow2-send-test-flow, which builds a well formed NetFlow v5 datagram using RFC 5737 documentation addresses and sends it to the collector. Every run uses a fresh random source port so you can pick your own test record out of the file.
sudo /usr/local/bin/goflow2-send-test-flow
Sent 1 synthetic NetFlow v5 flow to 127.0.0.1:2055
198.51.100.7:30909 -> 203.0.113.9:443 proto=TCP packets=33 bytes=42768
marker (unique source port): 30909
look for it with: sudo grep 30909 /var/lib/goflow2/flows.json
Now read the decoded record back out of the flow file. Every conversation GoFlow2 decodes becomes one JSON object on one line.
sleep 3
sudo tail -1 /var/lib/goflow2/flows.json | python3 -m json.tool | head -20
{
"type": "NETFLOW_V5",
"time_received_ns": 1785142429141646182,
"sequence_num": 1,
"sampling_rate": 0,
"sampler_address": "127.0.0.1",
"time_flow_start_ns": 1785142428000000000,
"time_flow_end_ns": 1785142429000000000,
"bytes": 42768,
"packets": 33,
"src_addr": "198.51.100.7",
"dst_addr": "203.0.113.9",
"etype": "IPv4",
"proto": "TCP",
"src_port": 30909,
"dst_port": 443,
"in_if": 1,
"out_if": 2,
"src_as": 64500,
"dst_as": 64501,
The record carries the five tuple, the byte and packet counts, the interface indexes, the autonomous system numbers and the derived source and destination networks. A NetFlow v9, IPFIX or sFlow sample from a real router produces a record with the same field names, which is the whole point of the collector.

Step 7 - Point your exporters at the collector
Open the two collector ports to your exporters only. Replace the source prefix with the address range your routers, switches or hypervisors send from.
az network nsg rule create --resource-group <your-rg> --nsg-name <your-nsg> \
--name allow-netflow --priority 1020 --protocol Udp --destination-port-ranges 2055 \
--source-address-prefixes <your-exporter-range> --access Allow
az network nsg rule create --resource-group <your-rg> --nsg-name <your-nsg> \
--name allow-sflow --priority 1021 --protocol Udp --destination-port-ranges 6343 \
--source-address-prefixes <your-exporter-range> --access Allow
Then configure the exporter itself. On Cisco IOS, exporting NetFlow v9 to this VM looks like this:
flow exporter CLOUDIMG-GOFLOW2
destination <vm-private-ip>
transport udp 2055
export-protocol netflow-v9
On a Juniper device, or any sFlow capable switch, point the sFlow collector at <vm-private-ip> port 6343. On Linux hosts, softflowd -i eth0 -n <vm-private-ip>:2055 exports NetFlow from a live interface. Use the private IP when the exporter is inside the same VNet; use the public IP only if you must, and keep the NSG source range tight.
NetFlow v9 and IPFIX are template based, so the first samples from a new exporter cannot be decoded until its option and data templates arrive, which typically takes a minute or two. You can see which exporters have sent templates:
PW=$(sudo grep '^GOFLOW2_PASSWORD=' /root/goflow2-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" http://127.0.0.1/templates
Once a real exporter is sending, watch records arrive:
sudo tail -f /var/lib/goflow2/flows.json
Press Ctrl+C to stop following.
Step 8 - Scrape the metrics from Prometheus
GoFlow2 exposes counters for packets received, samples decoded, decoding errors and drops. Those counters are how you know the collector is keeping up. The endpoint is behind Basic Auth; an unauthenticated request is rejected, and /healthz is deliberately left open so an Azure Load Balancer can probe it.
curl -s -o /dev/null -w 'healthz unauthenticated: HTTP %{http_code}\n' http://127.0.0.1/healthz
curl -s -o /dev/null -w 'metrics unauthenticated: HTTP %{http_code}\n' http://127.0.0.1/metrics
healthz unauthenticated: HTTP 200
metrics unauthenticated: HTTP 401
With the per-VM password, the metrics come back:
PW=$(sudo grep '^GOFLOW2_PASSWORD=' /root/goflow2-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" http://127.0.0.1/metrics | grep -E '^goflow2_flow_(traffic_packets_total|traffic_bytes_total|process_nf_total)'
goflow2_flow_process_nf_total{router="127.0.0.1",version="5"} 5
goflow2_flow_traffic_bytes_total{local_ip="::",local_port="2055",remote_ip="127.0.0.1",type="netflow"} 360
goflow2_flow_traffic_packets_total{local_ip="::",local_port="2055",remote_ip="127.0.0.1",type="netflow"} 5
GoFlow2's own readiness endpoint reports OK once it is collecting:
PW=$(sudo grep '^GOFLOW2_PASSWORD=' /root/goflow2-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$PW" http://127.0.0.1/__health

From your Prometheus server, scrape it over the network with the same credentials:
scrape_configs:
- job_name: goflow2
static_configs:
- targets: ['<vm-private-ip>:80']
basic_auth:
username: admin
password: <GOFLOW2_PASSWORD>
The counters worth alerting on are goflow2_flow_dropped_packets_total, which rises when the kernel receive buffer overflows, and goflow2_flow_decoder_error_total, which rises when an exporter sends something GoFlow2 cannot parse.
Step 9 - Where the flow data lives
Decoded flows are written to a dedicated Azure data disk, not the OS disk, and logrotate keeps 14 compressed generations.
df -h /var/lib/goflow2 | tail -1
sudo wc -l /var/lib/goflow2/flows.json
/dev/sda 20G 32K 19G 1% /var/lib/goflow2
5 /var/lib/goflow2/flows.json
Rotation uses a rename plus a SIGHUP, because GoFlow2's file transport reopens its output on that signal. That is why no record is lost or duplicated at the rotation boundary, which a copy and truncate strategy cannot guarantee.
cat /etc/logrotate.d/goflow2

Step 10 - Working with the flow records
Because every record is one JSON object on one line, jq is the natural tool. Top talkers by bytes:
sudo tail -n 10000 /var/lib/goflow2/flows.json | jq -r '.src_addr' | sort | uniq -c | sort -rn | head -10
Total bytes seen per destination port:
sudo tail -n 10000 /var/lib/goflow2/flows.json | jq -r '"\(.dst_port) \(.bytes)"' | awk '{b[$1]+=$2} END {for (p in b) print b[p], p}' | sort -rn | head -10
To ship the records onward, point any log shipper at the file. Promtail, Fluent Bit, Vector, Filebeat and the Azure Monitor agent all tail newline delimited JSON natively, and because the file is reopened on SIGHUP rather than truncated, a shipper following it will not lose its position at rotation.
Configuration
Every runtime option is in one environment file, so you never edit the systemd unit.
sudo cat /etc/goflow2/goflow2.env
The GOFLOW2_ARGS line is passed verbatim to the binary. The defaults are:
GOFLOW2_ARGS=-listen netflow://:2055,sflow://:6343 -format json -transport file \
-transport.file /var/lib/goflow2/flows.json -addr 127.0.0.1:8080 \
-templates.path /templates -loglevel info
To run four parallel sFlow sockets on a busy collector, change the listen URI to sflow://:6343?count=4,netflow://:2055. To emit protocol buffers instead of JSON, set -format bin. To publish onto Kafka rather than a file, replace the transport arguments:
GOFLOW2_ARGS=-listen netflow://:2055,sflow://:6343 -format bin -transport kafka \
-transport.kafka.brokers <broker-host>:9092 -transport.kafka.topic flows \
-addr 127.0.0.1:8080 -loglevel info
Apply any change with a restart, then confirm the service came back:
sudo systemctl restart goflow2.service
sleep 3
systemctl is-active goflow2.service
If your exporters use vendor specific IPFIX fields that GoFlow2 does not map out of the box, write a mapping file and add -mapping /etc/goflow2/mapping.yaml to the arguments. The upstream mapping documentation covers the field identifiers.
Maintenance
Rotate the metrics password. The Basic Auth credential is an nginx htpasswd entry, so replacing it is a one liner. Record the new password somewhere safe first, because only the bcrypt hash is stored.
sudo htpasswd -B /etc/nginx/.goflow2.htpasswd admin
sudo systemctl reload nginx
Add TLS. The metrics endpoint serves plain HTTP on port 80. For anything crossing an untrusted network, put a certificate in front of it with your own domain, then restrict port 80 to your Prometheus server.
Sizing the data disk. A single flow record is roughly 700 bytes of JSON. At 1,000 flows per second that is around 60 GB per day before compression, so size the data disk for your retention window, or ship records off the VM with a log shipper or Kafka and keep only a short local buffer.
Watch for drops. If goflow2_flow_dropped_packets_total is climbing, the kernel receive buffer is overflowing. The image already raises net.core.rmem_max in /etc/sysctl.d/60-goflow2.conf; beyond that, add parallel sockets with ?count=N on the listen URI or move to a larger VM size.
Upgrades. GoFlow2 is a single static binary at /usr/local/bin/goflow2. To move to a newer upstream release, download the new goflow2-<version>-linux-amd64 asset, verify its checksum, replace the binary and restart the service.
Backups. Snapshot the data disk on your normal schedule. The collector itself is stateless: /etc/goflow2/goflow2.env is the only configuration worth keeping.
Troubleshooting
No records appear in flows.json. Confirm the exporter can reach the VM at all, then confirm the packets arrive. sudo tcpdump -ni any udp port 2055 or udp port 6343 shows inbound datagrams; if nothing arrives, the NSG rule or the exporter's destination address is wrong. If datagrams arrive but no records are written, the exporter is probably sending NetFlow v9 or IPFIX and its templates have not been received yet; wait a minute and check /templates.
goflow2_flow_decoder_error_total is rising. An exporter is sending something GoFlow2 cannot parse. sudo journalctl -u goflow2.service -n 50 names the source address, which identifies the device.
The metrics endpoint returns 401. The password is per VM and lives in /root/goflow2-credentials.txt. If that file still says first boot has not run, check sudo systemctl status goflow2-firstboot.service.
The metrics endpoint returns 502. nginx is up but GoFlow2 is not answering on loopback. Check systemctl is-active goflow2.service and sudo journalctl -u goflow2.service -n 50.
Check the service logs at any time:
sudo journalctl -u goflow2.service -n 20 --no-pager
Support
This image is published and supported by cloudimg with 24/7 support. Raise an issue through the Azure Marketplace listing or contact cloudimg support directly.