Your software in your customers' networks
Your product has a piece that runs at the customer's site: a sensor, a collector, a gateway or an agent. It has to reach your cloud, and sometimes your cloud or your support engineers have to reach it. This page shows what that usually takes, the same fleet on nfltr, measured, and where nfltr does not help.
The problem
Vendors with a component inside customer networks tend to fall into a few shapes. We read the public deployment, firewall and network-requirements guides of a dozen such vendors; these are the shapes they describe:
- An OT or industrial security sensor vendor. A sensor sits in a plant's network and sends what it sees to the vendor's cloud console. The plant lets it out only through a proxy in its DMZ. Operators also need the sensor's web UI or a shell, and the vendor needs a remote-support path into the plant.
- A network or infrastructure monitoring vendor. A collector runs as a VM at each customer and keeps one TLS connection to the vendor's cloud. The customer's firewall team is handed a list of wildcard cloud domains or IP ranges that change, and an exemption from TLS inspection.
- An industrial IoT gateway maker. Gateways sit behind the customer's NAT or on a cellular link, push telemetry, and the vendor's engineers need remote access to the machines behind them, usually through a VPN server or a cloud service the vendor runs for that alone.
Across these shapes, the data path to the cloud is already outbound only; nobody asks a customer to open an inbound port for telemetry. What each vendor builds and runs on top of it is:
- The client side, in every language and platform it ships: TLS with its own CA or pinned certificates, HTTP proxy support with authentication, keepalives that survive NATs, reconnect with backoff, certificate rotation.
- The cloud side: public endpoints, load balancers and DNS names per region, a CA and its renewals, published IP ranges kept stable for customers' allow-lists.
- The way back in: reaching one component by name for a command, a support session or the device's UI. This is often a second product with its own tunnels or VPN servers.
- Every customer's firewall ticket: the allow-list, the proxy settings, the TLS-inspection exemption, and the back-and-forth whenever an endpoint changes.
The demo: one fleet, before and after
The fleet is a control plane and three edge agents in .NET 8 with gRPC, written twice. It is an example in nfltr's own repository, which is not public, and an end-to-end test runs both versions in containers and checks every claim below by exit codes.
- Before, as such fleets are usually built: the control plane listens on a public TLS port under a DNS name, with a certificate from the fleet's private CA. Agents trust that CA, check the name, keep the connection alive through NATs, and keep one session open: commands go down it, progress and results come up.
- After, on nfltr's connect sidecar: every program listens on loopback only. One
nfltr upbeside each program connects out to the relay. The agent's session goes to a local socket, and the control plane can also call one chosen agent by name (edge-1/execute), which it could not do before: nothing reaches an agent behind NAT. - One agent is at a customer site whose only way out is the site's HTTP proxy, in both versions. The proxy allows
CONNECTto a short list of addresses and refuses everything else. That is the most common setup in the guides we read.
What the fleet's machines need, read from the test's runs:
| what the fleet's machines need | before | after |
|---|---|---|
| listeners reachable from the network (each one an inbound firewall, NAT or ingress rule and a public DNS name) | 1 (the control plane's public TLS port) | 0 |
| a private CA to run, and the certificates it issues and renews | 1 CA, 1 certificate | none |
| self-signed certificates whose SHA-256 the peers pin (end-to-end encryption, per service) | 0 | 3 |
| an account key file on every machine (mode 0600) | no | yes |
| processes on the 4 machines | 4 programs | 4 programs + 4 sidecars |
what the proxied site must allow: CONNECT to | the vendor's own endpoint (controlplane.fleet.example:8443) | the relay's two addresses (in the test relay:8443, relay:8444; hosted: grpc.nfltr.xyz:443, nfltr.xyz:443) |
| proxy code in the agent | a hand-written CONNECT (the gRPC library used neither HTTPS_PROXY nor its handler's proxy setting) | none: the sidecar reads HTTPS_PROXY |
| the control plane calls one agent by name | no (refused, checked) | yes |
Evidence: what each machine runs and listens on
- Setup
- The example's end-to-end test, both versions in containers on one Mac (Apple Silicon) with Docker, against a private relay; two runs on November 6, 2026. Listeners were read from each fleet machine's
/proc/net/tcpand/proc/net/tcp6while it ran, sidecars included; the proxy's list and log from the proxy itself. - Numbers
- Listeners reachable from the network 1 → 0; private CAs 1 → 0; pinned self-signed certificates 0 → 3; processes 4 → 4 + 4 sidecars. The proxied agent has no direct route out (checked), its proxy refuses a
CONNECTto any other address (403, checked), and its traffic went throughCONNECTs to the listed addresses only. - Reproduce
- (a) With the released binaries: the files at the end on two machines, the agent's machine with
HTTPS_PROXYset to your proxy; list listening sockets withss -ltn. (b) Our check: the example's end-to-end test in nfltr's repository, which is not public (Docker and openssl). - Limits
- The relay is not counted: hosted, it is not yours to run; your own relay is one more public endpoint, shared by every fleet of the account. An account key file now sits on every machine. Three agents, not three hundred.
What the customer's network team is asked
Before, each vendor hands each customer its own list: its endpoints, regions and ports, often wildcard domains or IP ranges that change. On nfltr, the list is the same for every customer and every vendor using it: allow outbound CONNECT (or plain outbound TCP) to grpc.nfltr.xyz:443 and nfltr.xyz:443. No inbound rule, at the customer or at the vendor.
- Through the site's proxy.
nfltr upfinds the proxy inHTTPS_PROXY(withNO_PROXY), as most tools do, and sends its credentials when the proxy URL carries them (http://user:password@proxy:3128). Both the agent's connection and the sessions it opens go through the proxy. - TLS inspection. A proxy that intercepts TLS breaks any pinned connection, nfltr's included, unless the two names are exempt, which is the same exemption such guides already ask for. With end-to-end encryption on, an intercepting proxy sees only the inner TLS records either way.
Faults and restarts
The application keeps its own deadlines, cancellation and retries; the sidecar does not hide failures. A session the sidecar loses is closed on the local socket, so gRPC sees UNAVAILABLE at once and the application's retry runs.
| fault | failure the application saw after | first successful call started after |
|---|---|---|
| before: the proxied agent killed mid-command, its session over TLS | 132 to 184 ms | 910 to 1227 ms |
| after: the proxied agent killed mid-command (its sidecar kept running) | 118 to 252 ms | 642 to 1036 ms |
| after: an agent and the sidecar it owns killed, called by name | 122 to 139 ms | 779 to 896 ms |
Times are from the kill, with Docker restarting the container. A 1 s deadline on a 5 s command was cancelled on every agent, before and after, on both shapes. Progress updates arrived as the agent sent them: the first 102 to 123 ms after the call for 100 ms of work, before and after.
Evidence: restarts, deadlines and network drops
- Setup
- The fleet's test as above, two runs (restarts: an agent killed mid-command, the control plane retrying every 250 ms; the Mac was shared with other work). Network drops: our separate comparison of a conventional HTTPS port forward against the sidecar, one Go client and server, the home uplink dropped without a reset for 5, 60 and 180 s.
- Numbers
- In the table above. After every network drop both versions answered and streamed again within 3 s; neither returned a wrong, duplicated or missing answer or stream item.
- Reproduce
- (a) With the released binaries: kill the agent's program or its
nfltr upwhile a call runs, and time your own retry. (b) Our checks: the example's end-to-end test and the port-forward comparison, both in nfltr's repository. - Limits
- One host, no real network distance. A single relay in both tests, so a relay restart is an outage until it is back.
Migration effort
The protocol and the business logic did not change. A channel's target became a local endpoint of the sidecar, the listeners moved to loopback, and the TLS client code went:
| files | code lines added / removed | |
|---|---|---|
| application code, the migration | 4 | +52 / −73 |
| application code, new and optional: call an agent by name, an agent that owns its sidecar | 4 | +158 / −0 |
| configuration | 4 | +27 / −19 |
Code lines exclude comments and blank lines. The removed lines are the TLS client: trust of the private CA, the server-name check, keepalives, the connect timeout and the proxy's CONNECT. The protocol and the business logic are untouched; the test applies the patch to the before tree, builds the after fleet from the result and checks that every command's outcome, progress and output are the same.
Evidence: the size of the migration
- Setup
- The example's
migration.patch, counted by its report script; two runs of the test on November 6, 2026 applied it and built the after fleet from the result. - Numbers
- As in the table. Unchanged: the protocol definition, the business logic and both project files.
- Reproduce
- (a) Not from outside: the example is not published, so the counts are our record. (b) Our check: the example's end-to-end test in nfltr's repository.
- Limits
- One small fleet in one language, written for this comparison. A real codebase with TLS and proxy options in several places will have a larger diff.
Rollout can be per agent: both paths run side by side, configuration picks one, and setting the old value back rolls an agent back. A call on an open session cost 0.06 to 0.21 ms more at the median on one host (two sidecars and the relay, end-to-end encryption on, against the direct TLS connection); on a real network each hop adds its round trip to the relay.
The relay never sees your traffic
With e2ee: true on a service, the two sidecars run TLS 1.3 inside the session, and the client accepts only the certificate whose SHA-256 it was given (the pin). There is no CA: provisioning makes a self-signed certificate per machine and records its pin in your inventory. The test checks that a wrong pin fails closed (nfltr dial exits 7, nothing delivered) and that a plain open of an encrypted service is refused (exit 4).
The relay still sees which agent talks to which service, when, and how many bytes, and it can refuse or cut a session. It cannot read or change what is inside.
Where nfltr is not a fit
- Your component only sends data out, and that works. If nothing ever has to reach the component, and your telemetry already flows out through the customer's proxy, nfltr replaces a working path and adds a hop. A latency-sensitive data plane, such as DNS answered on site, should not go through a relay at all.
- Per-customer isolation is enforced by the relay only per account. The relay authenticates the account key, not the component: a key on one customer's appliance reaches every service of the account. Keep your application's own per-component credential and check it, or use one account per customer.
- One name is one agent.
edge-1/executereaches exactly one machine. A service balanced across several machines needs a balancer behind one agent. - TCP only. The sidecar carries byte streams; UDP protocols need their own path.
- The relay is on the path. Its availability is now part of yours; with your own relay it is yours to run.
- Air-gapped sites. Without any outbound path, no relay helps.
What it costs
A comparison of both sides at the same workload (agents connected around the clock, bytes per command and per session) will be here once prices are set. What nfltr will meter is on the pricing page: connected time per agent and bytes per kind of traffic. On the build side, the lines to count are the ones in the problem: engineering for the client and the way back in, the public endpoints and their certificates, and each customer's firewall work.
Is it for you?
Fits if your component sits behind customers' NATs and proxies, and your cloud or your support engineers need to reach one component by name, or you are about to build that path and the client-side connection code in more than one language.
Not for a component that only sends data out over a path that already works, a latency-critical data plane, or sites with no outbound connection.
Try it
On the vendor's side, the control plane publishes its service and maps each agent's:
# controlplane.yaml: nfltr up -c controlplane.yaml --api-key-file <key file>
agent: controlplane
services:
fleet: { tcp: 5000, e2ee: true }
connect:
edge-1: { target: edge-1/execute, listen: "127.0.0.1:7001", pin: "<edge-1's pin>" }
At the customer's site, beside your component:
# edge-1.yaml: HTTPS_PROXY=http://proxy.customer.example:3128 nfltr up -c edge-1.yaml --api-key-file <key file>
agent: edge-1
services:
execute: { tcp: 50051, e2ee: true }
connect:
fleet: { target: controlplane/fleet, listen: "unix:/run/nfltr/fleet.sock", pin: "<the control plane's pin>" }
Your component then talks to unix:/run/nfltr/fleet.sock instead of a host on the network, and your control plane calls 127.0.0.1:7001 to reach edge-1. Services and remote clients and the migration story have the details: pins, status, metrics, who runs the sidecar, and a gradual rollout.