NetGoat agent: a self-hosted reverse proxy with WAF rules and traffic policy
A Cloudflare alternative for local and cloud use, can be used ontop of cloudflare for cloudflares paid features, but for free!
At a glance
- What is it?
- The NetGoat agent is a Go reverse proxy that runs from a local YAML configuration or from snapshots pushed by a companion control plane, and ships with WAF rules, rate limits, TLS termination and a client fingerprinting stack class. It suits a homelab or self-hosting operator who wants Cloudflare-shaped edge features without a paid tier.
- Who is it for?
- The NetGoat agent is a good fit for an operator running a homelab or a small service who wants WAF rules, rate limiting, TLS termination and a reverse proxy in one Go binary they can read, and who is comfortable with alpha software and reading the source before exposing it.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 48 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A reverse proxy that keeps serving during a control plane outage
The architecture in one sentence is that NetGoat is a self-hosted reverse proxy and traffic-policy agent written in Go, which can run from a local YAML configuration, consume snapshots from the companion control plane, and continue serving the last known-good configuration during an outage. That third clause is the design decision that separates this from a thin client. If the agent is a view onto a central control plane, then the control plane is a single point of failure for every request the proxy handles, which is an awkward dependency for a piece of software whose job is to sit in front of other services. Keeping the last known-good configuration locally means an outage upstream degrades the management surface rather than the traffic path. The project also separates the two things explicitly. The dashboard shown by the wider NetGoat project belongs to the control plane, and this agent exposes metrics APIs but does not embed that dashboard, so the split between agent and control plane is a deliberate architectural boundary rather than an unfinished feature. The repository layout reflects that split, with a main.go entry point, an internal directory, an ai directory, and separate runtime files for the traffic path, for Cloudflare integration and for the developer plugin runtime.
The default configuration routes nothing, on purpose
The most important operational fact in the documentation is in the warning box at the top and in the feature status section. NetGoat is active alpha software at v0.1.0-alpha.1, git tags going forward are v*, and the shipped configuration contains no routes. The documentation says the shipped config.yml has routes: {}, and that a fresh default deployment returns 404 for every Host instead of proxying to a local service. That is a design decision worth respecting rather than working around. An edge proxy that proxies somewhere by default is a footgun in a way that a library is not, because a misconfigured deployment becomes an open relay or an unintended exposure of a private service. Here the failure mode of no configuration is a clean 404, which is a safe failure. The warning also gives the operator's checklist in three items: review every upstream, use strong bootstrap credentials, and place administrative services behind authenticated TLS before exposing a deployment to the internet. The repository makes the same point structurally, with a config.yml at the root and a config_sample_test.go alongside startup_config_test.go, so the default configuration is covered by tests.
What the agent actually ships today
The feature table is explicit that the Available column is what the agent ships and the Planned column is designed work with open specs, not live behaviour. The available set is long but each entry is specific. Domain and path routing covers exact, wildcard, regex and longest-prefix path routes, with local routes overridable by streamed routes. Load balancing and failover gives round-robin pools, bounded concurrent health checks and safe-method retry. The WAF rules are precompiled expression rules with priorities, BLOCK and ALLOW actions, and request context covering host, method, path, query, header and the stack_class. Traffic controls cover global rate limiting, request queueing, bandwidth throttling, honeypot handling and session-bound proof of work. The shared response cache is a bounded LRU and TTL cache for explicitly public responses with HTTP freshness and revalidation safeguards. Local authentication supports cookie or Basic authentication, per-user zero-trust challenge flags and explicit secure bootstrap users. TLS termination offers static fallback, streamed per-domain and wildcard certificates and atomic SNI selection, and termination is required for the fingerprint and for the session-bound proof of work. WebSocket upgrade connections are preserved by the Go reverse proxy. Metrics are exposed as JSON and Prometheus endpoints for traffic, cache, block, latency and proxy-error counters. The optional AI request classifiers, local GoatAI, Koda-WAF and Koda-2 workers, need model files and Python dependencies only when they are enabled.
The fingerprint is a stack class, and the documentation says so bluntly
Stack fingerprint v1 is the feature most likely to be misunderstood, so it is worth reading the honesty section closely. The fingerprint combines JA4 with an Akamai-style HTTP/2 fingerprint built from SETTINGS, WINDOW_UPDATE and PRIORITY frames, pseudo-header order and ALPN order, and it works only when this agent terminates TLS. The output is an opaque stack_class for bot clustering, and the documentation says explicitly that it is not a user, a hardware ID or a canvas hash, and that there is no JA4H. The honesty section then states the limitation in plain words: a fingerprint is a client TLS and HTTP stack class, Chrome on a million laptops will collide, and you should not persist it as identity. That is a correct and unusual thing for a vendor to write about their own feature. The other half of the honesty section is about what the agent refuses to claim. If Cloudflare or another terminator sits in front, this agent must not fingerprint that ClientHello and call it the browser, and Cloudflare in front of the agent is explicitly not a JA4 source. Turnstile and reCAPTCHA are not NetGoat features, and HTTP/3 and QUIC clients will not appear in the fingerprint at all, which the documentation lists as a documented hole.
Proof of work, bot signing and the empty skip lane
The challenge system is the other place where the documentation is unusually candid. The challenge is JSON rather than text, click or slider, and the difficulty is derived from load plus a stack_class mismatch, wired from terminate-only JA4, HTTP/2 and ALPN data, rather than from the User-Agent header, which is a design choice that refuses to trust a self-declared string. The verify step is bound to the terminated TLS session rather than to the IP address, so a challenge cannot be solved once and replayed from elsewhere. The Web Bot Auth skip is pinned, and the documentation states that the pin list may be empty, in which case the skip lane stays empty until operators seed pinned https directories, and that unsigned agents take the proof of work lane. The gaps list spells out the consequence: unsigned LLM browsers, python, go-http-client and curl take the proof of work lane and not a magic skip. The proof of work and the pinned skip are live when the agent terminates TLS, so a deployment that sits behind another reverse proxy gets neither. There is a session-bound proof of work with pinned Web Bot Auth skip, and the entire feature set collapses to no fingerprint and no proof of work when there is no termination, whether plaintext, pass-through or Cloudflare in front.
Certificates, dynamic rules and Cloudflare management, all opt-in
Several capabilities that would normally be on by default are explicitly opt-in, and the reasoning is consistent. Automatic certificate issuance and renewal comes with an explicit ACME allow-list, an HTTP-01 handler, an encrypted persistent cache and last-known-good certificate retention. The JavaScript and TypeScript dynamic rules run isolated, bounded request decisions with atomic last-known-good reload and fail-closed evaluation, and the fail-closed direction is the right way round for a security control. The Cloudflare Access, DNS and tunnel management is opt-in too, with fail-closed Access JWT and JWKS verification and bounded, dry-run-by-default startup reconciliation using an environment-only token, and the dry-run default is what makes it safe to turn on against a production account. The developer plugin catalog and middleware SDK is available but restricted: restart-only selections for exact compiled descriptors, v1 capability grants, lifecycle isolation and no remote code or artifact loading. That last constraint is the important one, since a plugin system that could load code from a remote source would be a remote code execution surface sitting in front of your traffic.
Go 1.25, SQLite for state, and a MongoDB that is not required
The build requirements are narrow. The quick start asks for Go 1.25 or newer and one or more reachable HTTP upstreams, and the module is netgoat.xyz/agent with go 1.25.0 in the go.mod file. The dependency list is short and tells you what the agent actually uses: goja for the JavaScript rule engine, esbuild for the TypeScript side of those rules, expr-lang for the WAF rule expressions, zerolog for logging, yaml.v3 for the configuration, and modernc.org/sqlite for state. The use of a pure Go SQLite implementation rather than a cgo binding is a portability decision that removes a build dependency. The docker-compose.yml in the repository root is about the companion stream server, not the agent, and it says so in a comment: the optional development MongoDB is for the companion stream-server and the Go agent itself stores runtime state in SQLite and does not require this service. The compose file binds MongoDB to 127.0.0.1 only, sets a healthcheck and keeps the data in a named volume, which is the shape you want for a development-only dependency. The repository also has a quickstart-overlay.sh at the root, a scripts directory, a public directory, and a large set of test files with names that describe the behaviour under test, including waf_scope_test.go, zero_trust_test.go, tls_config_test.go, snapshot_apply_test.go and stream_poll_test.go.
Editorial conclusion
The NetGoat agent is a good fit for an operator running a homelab or a small service who wants WAF rules, rate limiting, TLS termination and a reverse proxy in one Go binary they can read, and who is comfortable with alpha software and reading the source before exposing it. It is the wrong tool for a production edge that needs HTTP/3 fingerprinting, for a deployment where an operator cannot be trusted with the upstream list, and for anyone who needs the project to leave alpha soon, since the release gate is written down in docs/release.md and the current tag is v0.1.0-alpha.1. Before deploying it, read the warning at the top of the documentation, because the shipped config contains no routes and returns 404 for every Host, and put administrative services behind authenticated TLS before exposing a deployment to the internet. The licence field on the repository says AGPL-3.0, and the last push was on 2026-09-04.
Frequently asked questions
What is the NetGoat agent and what does it do?
It is a self-hosted reverse proxy and traffic-policy agent written in Go. It runs from a local YAML configuration or consumes snapshots from a companion control plane, and it continues serving the last known-good configuration during an outage.
What happens if I deploy NetGoat without configuring any routes?
The shipped config.yml has routes set to an empty map, and a fresh default deployment returns 404 for every Host instead of proxying to a local service. The documentation warns that the shipped configuration contains no routes and tells operators to review every upstream before exposing a deployment.
What does the NetGoat stack fingerprint actually measure?
It combines JA4 with an Akamai-style HTTP/2 fingerprint built from SETTINGS, WINDOW_UPDATE and PRIORITY frames, pseudo-header order and ALPN order, and only when the agent terminates TLS. The result is an opaque stack_class for bot clustering, not a user, hardware ID or canvas hash, and Chrome clients will collide.
Which WAF and traffic features are available in the NetGoat agent?
Available features include exact, wildcard, regex and longest-prefix routing, round-robin pools with health checks and failover, precompiled WAF expression rules with BLOCK and ALLOW actions, global rate limiting, request queueing, bandwidth throttling, honeypot handling, a shared LRU and TTL response cache, local cookie or Basic authentication, TLS termination with per-domain and wildcard certificates, WebSocket proxying, and JSON and Prometheus metrics.
Is NetGoat ready for production use?
The project describes itself as active alpha software at v0.1.0-alpha.1, with git tags going forward following the v pattern, and the hardening section states that alpha is left only when the written gate in docs/release.md is checked off. The last push was on 2026-09-04, and the licence field on the repository says AGPL-3.0.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/netgoat-xyz-netgoat)