BFE
Overview
BFE (originally Baidu Front End) is an open-source layer 7 load balancer and reverse proxy, and a Cloud Native Computing Foundation sandbox project. It sits at the edge of your estate, terminates client traffic and forwards it to the right backend.
Routing decisions are made on the content of the request rather than just the port it arrived on. Host, path, header, cookie, query string and client address are all first-class matching conditions, expressed in a small condition language. Matched traffic is balanced across named clusters and sub-clusters with health checking, weighting and configurable retries, so an unhealthy backend drops out of rotation without you touching anything.
BFE speaks HTTP, HTTPS, HTTP/2, WebSocket, gRPC and FastCGI. It carries a modular plugin framework for rewriting, redirecting, blocking, tagging and rate limiting traffic, and exposes rich built-in counters so you can see exactly what your edge is doing.
This guide covers deploying and configuring the SecureAnyCloud BFE image on Ubuntu 24.04 on Azure. The image ships the official upstream BFE 1.8.4 release binary, fetched from the project's own GitHub release and pinned by SHA-256. The exact artifact, its digest and its provenance are recorded on the image at /opt/bfe/VERSION. BFE runs under systemd as bfe.service, as an unprivileged bfe account.
A load balancer that load balances the moment it boots. A proxy with no backends is an empty daemon, and on a fresh VM you have not configured any backends yet. So the image ships one: a tiny demo backend bound to 127.0.0.1:8081, to which BFE's default configuration routes every request.
On first boot, a real request travels through BFE end to end and comes back with the backend's page. That backend is reachable only over loopback, so a response carrying its marker can only have arrived by being proxied. BFE also stamps its own X-Proxied-By: bfe header onto the response on the way out, so both ends of the journey are visible in a single curl -i. You then repoint BFE at your own servers and remove the demo backend.
A per-instance TLS certificate, and no key in the image. The HTTPS listener needs a certificate. Baking one in would put the same private key on every customer VM, so the image ships no certificate and no key at all. Both the certificate and the TLS session ticket key are generated on your VM at first boot. bfe.service is gated so that it cannot start before that bootstrap has completed.
The metrics endpoint is not on the public interface. BFE's monitor port is more than metrics. Alongside the counters, it serves an unauthenticated /reload (live configuration reload) and a /debug surface, and upstream offers no bind address setting for it. The image therefore keeps port 8421, and the demo backend on 8081, reachable only through a loopback address, enforced in the kernel with an nftables rule. The proxy listeners on 8080 and 8443 stay open, because those are the product.
No default login of any kind. BFE has no credential to leak. It is a stateless traffic router with no console, no account and no password. The upstream release does ship sample credentials: two seeded HTTP basic accounts whose passwords are written in the file, a JWT signing key, a published TLS session ticket key and four private keys. Every one of them is removed at build time, and the build fails closed if any survives.
What you get:
- The official BFE 1.8.4 release binary, pinned by SHA-256 and redistributed unmodified, run under systemd as
bfe.service - A working routing configuration out of the box: a default product, a route rule and a health-checked cluster pointing at a bundled loopback demo backend
- HTTP on 8080 and HTTPS on 8443 (HTTP/2 negotiated via ALPN), with a per-instance self-signed certificate generated at first boot
- A hardened
bfe.conf: RC4 and 3DES cipher suites removed, SSLv2 ClientHello compatibility disabled, and the loaded module set trimmed to nine, leaving the WebAssembly plugin and AI gateway modules unloaded - An nftables rule that keeps the monitor and demo backend ports on loopback only
bfe-selftest, a shipped command that proves in one line that a real request traversed BFE, that the metrics port is not exposed, and that BFE's own counters moved- The Apache-2.0 licence text, retained on the image at
/opt/bfe/LICENSE - An Ubuntu 24.04 LTS base with the latest security patches applied at build time
- The Azure Linux Agent for cloud integration and SSH key injection
- 24/7 SecureAnyCloud support with a guaranteed 24 hour response SLA
SecureAnyCloud is not affiliated with or endorsed by the BFE project, the CNCF or Baidu.
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
- A subscription to the BFE listing on the Azure Marketplace
- The backend servers you intend to put behind BFE, reachable from the VM. You can deploy first and add them later, because the bundled demo backend lets the appliance work standalone.
Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM) is a sensible starting size, and BFE idles at around 12 MB of memory. Load balancers are bound by connection and request rate rather than by storage. For busy edges, move to a compute-optimised size such as Standard_F4s_v2 and give BFE more cores via MaxCpus in bfe.conf.
Deploy from the Azure Portal
Search the Azure Marketplace for BFE, select the SecureAnyCloud publisher, and choose Create.
Add network security group (NSG) rules for TCP 8080 (the HTTP proxy listener), TCP 8443 (the HTTPS proxy listener) and TCP 22 (admin SSH), each from the networks you serve or connect from.
Do not open 8421 or 8081. They are the monitor endpoint and the demo backend, and the image deliberately keeps them private.
Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:bfe:default:latest
az vm create \
--resource-group my-bfe-rg \
--name my-bfe-vm \
--image sac1647283583153:bfe:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# Then open the two proxy listeners to the networks that need them
az vm open-port --resource-group my-bfe-rg --name my-bfe-vm --port 8080 --priority 1010
az vm open-port --resource-group my-bfe-rg --name my-bfe-vm --port 8443 --priority 1020Connect and confirm BFE is running
Log in over SSH as azureuser. First boot generates this VM's TLS material and starts the load balancer. This takes a few seconds after the VM reports ready.
sudo systemctl is-active bfe.service
sudo systemctl is-active nginx.service /opt/bfe/bin/bfe -v 2>&1 | grep -o 'bfe version: .*'Expected output:
active active bfe version: 1.8.4The nginx.service above is not a second web server for your traffic. It is the bundled demo backend, bound to 127.0.0.1:8081, which gives BFE something real to route to before you have added your own servers.
Confirm which process owns which port. BFE holds the two public listeners plus the loopback-restricted monitor. The demo backend is bound to the loopback address only:
sudo ss -lntp | grep -E 'bfe|nginx'The image records exactly which upstream artifact it shipped:
cat /opt/bfe/VERSIONProve a real request travels through BFE
Run this check first, because it exercises the whole path rather than just a listening socket.
curl -si http://127.0.0.1:8080/ | head -12Expected output:
HTTP/1.1 200 OK Accept-Ranges: bytes Connection: keep-alive Content-Length: 1245 Content-Type: text/html Date: Fri, 07 Aug 2026 17:26:53 GMT Etag: "6a7614f1-4dd" Last-Modified: Fri, 07 Aug 2026 17:25:05 GMT Server: nginx X-Sac-Backend: bfe-demo-backend X-Proxied-By: bfeTwo things in that response together prove the traversal:
X-Sac-Backend: bfe-demo-backendand the page body come from the demo backend, which listens only on127.0.0.1:8081and is firewalled off every other address. Something really did reach it.X-Proxied-By: bfewas added on the way out by BFE's ownmod_header. Only BFE's module chain can produce that header.
Neither header alone would be enough. A listening socket or an HTTP 200 would not be enough either, which is why the shipped self-test asserts on both.
The same journey works over TLS, where ALPN negotiates HTTP/2:
curl -sik https://127.0.0.1:8443/ | head -5Expected output:
HTTP/2 200 accept-ranges: bytes content-type: text/html date: Fri, 07 Aug 2026 17:27:03 GMT etag: "6a7614f1-4dd"-k is expected here, because first boot generates a self-signed certificate for this VM. Replace it with a certificate for your own domain in Step 7.
Run the shipped self-test for a single line that covers all of it:
sudo /usr/local/sbin/bfe-selftestExpected output:
bfe-selftest OK: a real request traversed BFE on :8080 and :8443 and returned the loopback-only backend's response (marker present, X-Proxied-By: bfe present); monitor :8421 and backend :8081 answer on loopback and are refused off-box; per-VM certificate in use; request counter 9 -> 11; no seeded credentials in /etc/bfeFrom your own workstation, the same request against the VM's public address returns the same page. Substitute your VM's IP address:
curl -i http://<your-vm-public-ip>:8080/Read this VM's endpoint summary
First boot writes a summary of this VM's endpoints and file locations. The file is root-only, mode 0600, and holds no password because BFE has none.
sudo cat /root/bfe-endpoints.txt | head -14Expected output (your address will differ):
# BFE — layer-7 load balancer. Generated on first boot. Unique to THIS VM.
# bfe.host=20.25.114.98 bfe.url=http://20.25.114.98:8080/ bfe.url_tls=https://20.25.114.98:8443/ bfe.http_port=8080 bfe.https_port=8443 bfe.monitor=http://127.0.0.1:8421/monitor/proxy_state bfe.config=/etc/bfe/bfe.conf bfe.backends=/etc/bfe/cluster_conf/cluster_table.data bfe.routes=/etc/bfe/server_data_conf/route_rule.data bfe.tls_cert=/etc/bfe/tls_conf/certs/sac.crt bfe.selftest=/usr/local/sbin/bfe-selftestPoint BFE at your own backends
BFE separates what the request is from where it goes across a few small JSON files under /etc/bfe. You edit three of them:
File Decides
server_data_conf/host_rule.data which product a hostname belongs to
server_data_conf/route_rule.data which cluster a request in that product goes to
cluster_conf/cluster_table.data which backend instances make up that cluster
The shipped configuration is deliberately the smallest thing that works. Every request falls to a default product, which routes to one cluster, which contains the single loopback demo backend. Inspect it:
cat /etc/bfe/cluster_conf/cluster_table.dataExpected output:
{ "Version": "sac-1", "Config": { "cluster_demo": { "demo.local": [ { "Addr": "127.0.0.1", "Name": "sac_demo_backend", "Port": 8081, "Weight": 10 } ] } } }To use your own servers, replace that instance list. Each entry is one backend, and Weight sets its share of traffic within the sub-cluster:
{ "Version": "sac-2", "Config": { "cluster_demo": { "demo.local": [ { "Addr": "10.0.1.10", "Name": "app-1", "Port": 8080, "Weight": 50 }, { "Addr": "10.0.1.11", "Name": "app-2", "Port": 8080, "Weight": 50 } ] } } }The cluster's health check lives in server_data_conf/cluster_conf.data. It defaults to GET /healthz expecting a 200, which is what the demo backend serves. Point it at whatever path your own application exposes:
grep -A6 '"CheckConf"' /etc/bfe/server_data_conf/cluster_conf.dataTo route by hostname, add your domains to host_rule.data and give each product its own rule in route_rule.data. Conditions are written in BFE's condition language, with functions such as req_host_in(), req_path_prefix_in(), req_header_value_in() and req_cookie_value_in(). They are evaluated in order, so put specific rules above default_t():
{ "Version": "sac-2", "ProductRule": { "sac_demo": [ { "Cond": "req_path_prefix_in(\"/api\", false)", "ClusterName": "cluster_api" }, { "Cond": "default_t()", "ClusterName": "cluster_demo" } ] } }Any new cluster name you introduce also needs an entry in cluster_conf.data (its timeouts and health check) and in cluster_conf/gslb.data (its traffic split across sub-clusters).
Apply configuration changes with a reload, which is graceful and does not drop connections:
sudo systemctl reload bfeOnce your own backends are serving, remove the demo backend. It exists only so that the appliance can prove itself on a bare VM:
sudo systemctl disable --now nginx
sudo rm -f /etc/nginx/sites-enabled/sac-bfe-demo.confInstall your own TLS certificate
First boot generated a self-signed certificate for this VM. To serve your own domain, replace the certificate and key pair and reload. The file names are already referenced by tls_conf/server_cert_conf.data, so nothing else needs editing:
sudo install -o root -g bfe -m 0644 fullchain.pem /etc/bfe/tls_conf/certs/sac.crt
sudo install -o root -g bfe -m 0640 privkey.pem /etc/bfe/tls_conf/certs/sac.key
sudo systemctl reload bfeConfirm which certificate the listener is serving:
echo | openssl s_client -connect 127.0.0.1:8443 2>/dev/null | openssl x509 -noout -subject -datesTo match a certificate to a specific hostname instead of serving one default, add the name to SniConf in /etc/bfe/tls_conf/tls_rule_conf.data and add a second entry under CertConf in server_cert_conf.data.
Watch what your edge is doing
BFE keeps detailed counters and exposes them on the monitor port. Request the totals:
curl -s "http://127.0.0.1:8421/monitor/proxy_state?format=json" | jq '{HTTP_CLIENT_REQ_SERVED:.CounterData.HTTP_CLIENT_REQ_SERVED, CLIENT_REQ_SERVED:.CounterData.CLIENT_REQ_SERVED, CLIENT_REQ_FAIL:.CounterData.CLIENT_REQ_FAIL}'Expected output (your numbers will differ):
{ "HTTP_CLIENT_REQ_SERVED": 11, "CLIENT_REQ_SERVED": 13, "CLIENT_REQ_FAIL": 0 }Check that your backends are registered and in rotation:
curl -s "http://127.0.0.1:8421/monitor/bal_table_status?format=json" | jq .Expected output:
{ "Balancers": { "cluster_demo": { "SubClusters": { "GSLB_BLACKHOLE": { "BackendNum": 0 }, "demo.local": { "BackendNum": 1 } }, "BackendNum": 1 } }, "BackendNum": 1 }Dozens of other endpoints are available, including proxy_delay, http_state, bal_state, proxy_mem_stat, per-module counters and a _diff variant of most of them for interval sampling. List them:
curl -s http://127.0.0.1:8421/monitor | tr '<' '\n' | grep -oE 'href="/monitor/[a-z_.0-9]*"' | head -12Per-request logging is on by default and lands in /var/log/bfe, rotated hourly:
sudo grep -o 'host: [^ ]* product: [^ ]* .*status: [0-9]*' /var/log/bfe/access.log | tail -2Expected output:
host: 127.0.0.1:8080 product: sac_demo user_agent: curl/8.5.0 status: 200 host: 127.0.0.1:8443 product: sac_demo user_agent: curl/8.5.0 status: 200Why the monitor port is not open to the internet
The monitor port is not a read-only metrics endpoint. Alongside the counters, BFE serves an unauthenticated /reload that reloads configuration, and a /debug surface. Upstream provides no bind address option to restrict them, so exposing 8421 exposes a control plane.
The image handles this in the kernel rather than by convention. An nftables table accepts these ports only when the destination is a loopback address and drops them otherwise:
sudo nft list table inet sac_bfeExpected output:
table inet sac_bfe { chain input { type filter hook input priority filter; policy accept; ip daddr 127.0.0.0/8 tcp dport { 8081, 8421 } accept ip6 daddr ::1 tcp dport { 8081, 8421 } accept tcp dport { 8081, 8421 } drop } }Matching the destination address rather than the input interface is deliberate. Linux routes traffic addressed to the VM's own routable address over the loopback interface, so an interface-based rule would have left the port open on the VM's real addresses while still appearing to pass a 127.0.0.1 test.
You can prove the restriction from the VM itself. The metrics answer on loopback and time out on this VM's own private address, while the proxy listener answers on both:
PRIV=$(hostname -I | awk '{print $1}')
echo "monitor via loopback : $(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8421/monitor/proxy_state)"
echo "monitor via ${PRIV} : $(curl -s -o /dev/null -w '%{http_code}' -m 5 http://${PRIV}:8421/monitor/proxy_state)"
echo "proxy via ${PRIV} : $(curl -s -o /dev/null -w '%{http_code}' -m 8 http://${PRIV}:8080/)"Expected output: 000 is curl reporting that the connection never completed, which is the point.
monitor via loopback : 200 monitor via 10.0.0.17 : 000 proxy via 10.0.0.17 : 200To let a monitoring system scrape the counters, add an accept rule for the addresses you trust above the drop in /etc/nftables.d/sac-bfe.nft, then run sudo systemctl reload nftables. Open the port to the internet only if you have put your own authentication in front of it.
Enable additional modules
Nine modules are loaded by default: mod_trust_clientip, mod_block, mod_header, mod_rewrite, mod_redirect, mod_logid, mod_tag, mod_access and mod_prison. Their rule files ship empty, ready for your rules.
Many more are compiled in but not loaded. See everything available:
curl -s "http://127.0.0.1:8421/monitor/module_status?format=json" | jq -r '.available[]' | head -20The image ships configuration only for the modules it loads. This is deliberate. The upstream sample files of several unloaded modules hard-code third-party addresses (the mod_tcp_keepalive sample names a public Baidu address, and the mod_key_log sample names 10.0.0.x hosts), which have no place on your VM.
mod_wasm (WebAssembly plugins) and the AI gateway modules are likewise not loaded. The prebuilt binary plugin that upstream ships with mod_wasm is not on the image at all.
To enable another module, add a Modules = <name> line to /etc/bfe/bfe.conf. Create /etc/bfe/<name>/ with that module's configuration, taking the template from the upstream release tarball or the module documentation. Then reload.
BFE validates module configuration at startup and refuses to run if it is wrong. It exits with a success status when it does so, so systemctl reports the unit as deactivated cleanly rather than failed. Always confirm that a reload or restart took effect by checking BFE's own warning log, not the exit code:
sudo cat /var/log/bfe/bfe.wf.logAn empty file means no errors. If BFE refused to start, the reason is on the last line there.
Troubleshooting
- BFE says it started but nothing is listening. BFE exits with status
0even on a fatal configuration error, so systemd reports "Deactivated successfully". Read/var/log/bfe/bfe.wf.log. The[EROR]line names the module and the field that was rejected. - The proxy returns 500 or 503. BFE has no healthy backend. Check the balancer with
/monitor/bal_table_statusas in Step 8. ABackendNumof0means your instances are not incluster_table.data. Instances that are listed but fail the health check are taken out of rotation, so verify that theCheckConfpath incluster_conf.datareturns the expected status code from your servers. - A new hostname is not routing.
host_rule.datamaps hostnames to products, and a request whoseHostheader matches nothing falls through toDefaultProduct. Add the hostname to a tag underHosts, and make sure the tag is listed against your product underHostTags. - The service will not start after a reboot.
bfe.servicedoes not start until/var/lib/sac/data/bfe-bootstrap-readyexists, which first boot creates after generating this VM's TLS material. Checksudo systemctl status bfe-firstboot.serviceandsudo cat /var/log/sac/sac-firstboot.log. - Requests are logged with the wrong client IP. If BFE sits behind another proxy or an Azure Load Balancer, configure
mod_trust_clientipwith your trusted ranges so that the real client address is taken from the forwarded header.
Security Notes
- No default credentials of any kind. BFE is a stateless traffic router with no console, no account and no password, so there is nothing to rotate. The sample credentials that the upstream release ships (two seeded HTTP basic accounts, a JWT signing key, a published TLS session ticket key and four private keys) are all removed at build time, and the build fails closed if any survives.
- No key material in the image. The TLS certificate, its private key and the TLS session ticket key are generated on your VM at first boot.
bfe.serviceis gated on a marker that first boot creates only after that has happened, so the load balancer cannot serve traffic before it has its own key material. The private key isroot:bfemode0640. - The monitor port is a control plane. It serves an unauthenticated
/reloadand a/debugsurface as well as metrics, and the kernel restricts it to loopback addresses. Treat any decision to expose it as you would exposing an admin API. - Hardened TLS. RC4 and 3DES cipher suites are removed from the upstream default list, and SSLv2 ClientHello compatibility is disabled.
- Reduced module surface. The WebAssembly plugin module and the AI gateway modules are not loaded, and the prebuilt sample WebAssembly plugin that the upstream release ships is not present on the image.
- Restrict the proxy listeners to the networks you actually serve using NSG rules, and keep 8421 and 8081 closed.
- Keep the OS patched. Unattended security upgrades are enabled. To apply BFE updates, replace
/opt/bfe/bin/bfewith a newer upstream release and restart the service.
Support
SecureAnyCloud provides 24/7 support with a guaranteed 24 hour response SLA for all Azure Marketplace images. Contact sales@secureanycloud.com.
Upstream project documentation is at bfe-networks.net and the source is at github.com/bfenetworks/bfe.
Deploy on Azure
Launch BFE 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