Skip to content
Network egress controls

Network egress controls

Connectors dial addresses that a person typed into a form, from inside your cluster. A fetcher with no address policy can be turned into a request-forgery tool: pointed at the cloud metadata service, the cluster’s internal services or FACE itself. FACE’s connectors therefore share one hardened dialer, grpc/internal/extractors/connector_net.go, and one address policy.

The connector address policy

The check runs on the address actually dialled

The policy runs in the dialer’s Control hook. That hook receives the address being connected to after DNS resolution, so a hostname that resolves inward cannot slip past it, whether through DNS rebinding, a record pointing at loopback, or split-horizon DNS. The check runs on every dial, which covers every hop of a redirect chain and raw TCP protocols (Modbus, LLRP, NMEA, MQTT) as well as HTTP. An address that cannot be parsed is not dialled.

Always refused, with no opt-in

On every connector that uses the policy, these are refused regardless of configuration:

AddressRefusal
Cloud instance-metadata service: 169.254.169.254, and its IPv6 form fd00:ec2::254refusing to dial …: that is the cloud instance-metadata service, not a data source. This refusal has no opt-in
Multicastrefusing to dial multicast address …
The unspecified addressrefusing to dial the unspecified address

IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) are checked as the IPv4 address they carry.

Internal ranges: a per-connection decision

Loopback, RFC 1918 private space, carrier-grade NAT (100.64.0.0/10), link-local and IPv6 unique-local addresses are governed by one setting per connection, allow_internal_network. Each connector has a default that matches what its sources really are:

  • Refused by default: the web page connector. A web page is almost always on the public internet, so an internal address there is either a mistake or a probe.
  • Allowed by default: device and plant connectors, such as GPS, RFID readers, sensor gateways, MQTT brokers and logistics APIs. A dock-door reader or a cold-room gateway is internal by nature.

Either default can be overridden on each connection, in either direction. The decision is stored with the connection, not in an environment variable that would reopen every fetch on the instance. A value that is neither true nor false falls back to the default, so a typo never becomes an exposure.

A refused internal address is reported in words an operator can act on: refusing to reach …: it resolves to a private address, which is inside this deployment's own network rather than on the public internet.

NAT64

On an IPv6-only cluster, an IPv4-only destination is reached through NAT64. The socket then connects to a synthesised IPv6 address, not the IPv4 one. The policy unwraps addresses in the RFC 6052 well-known prefix 64:ff9b::/96 and judges the embedded IPv4 address. The metadata-service and private-range refusals therefore still apply when a destination is reached through NAT64.

Proxies, redirects and response size

  • No ambient proxy. Connector HTTP clients set Proxy: nil. An environment-configured proxy would move the check onto the proxy’s address and away from the real destination.
  • Redirects stay on the named host. Connector HTTP clients follow at most 5 redirects, and never to a different host: refusing to follow a redirect from … to …. Go drops an Authorization header when a redirect changes host, but it keeps custom API-key headers. The host lock keeps a tenant’s credential from travelling to a host the connection never named.
  • Bounded timeouts and bodies. Every connector client has a whole-request timeout, and bounded dial and TLS-handshake times. Response bodies are read up to a cap. A body over the cap is refused outright, never reported as a short but complete answer.

Which connectors use it

The hardened dialer is used by the web page, GPS, RFID, sensor-gateway, MQTT, logistics-API, Kafka, camera (RTSP, ONVIF and HTTP), SAP, Salesforce, ServiceNow, Guidewire and HubSpot connectors and the file downloader. The Snowflake and Databricks drivers are handed the same transport through their driver-level transport hooks, so the address policy applies to every address those drivers dial.

Read-only SQL

Every executable SQL path goes through ValidateSQL (grpc/internal/extractors/sql_validator.go). That covers the generic SQL service, which also serves the agent loop’s run_sql tool, and the Postgres, MySQL, Snowflake and Databricks connectors. The check:

  • strips comments and string literals, and refuses an unclosed comment or string;
  • splits the query into statements and requires each to start with SELECT, WITH, SHOW, DESCRIBE, DESC or EXPLAIN. Anything else is refused with only read-only queries (SELECT, WITH, SHOW, DESCRIBE, EXPLAIN) are allowed;
  • refuses restricted keywords inside an otherwise allowed statement.

The MySQL driver runs with MultiStatements disabled, so stacked queries are refused. Warehouse connectors pass query parameters as real bound parameters, never by string interpolation.

IPv6-first, IPv4 fully supported

FACE is deployed IPv6-first, while many warehouses and cloud APIs are reachable only over IPv4. Both have to work, so:

  • No code pins an address family. Every dial uses Go’s "tcp" network with no negative FallbackDelay, which gives RFC 8305 Happy Eyeballs behaviour. TestNothingPinsAnAddressFamily fails on any tcp4, tcp6, ip4 or ip6 literal, or on a negative fallback delay.
  • Host:port strings are built and parsed properly. Code uses net.JoinHostPort to build addresses and net.SplitHostPort to parse them, never string concatenation or a colon search. Those break every IPv6 literal. internal/extractors/ipv6_hostport_guard_test.go parses the backend source and fails on the old patterns. Mutation tests in the same file prove the guard can fail.

What these controls do not establish

  • An address policy only sees addresses. A public address that fronts an internal service, such as a load balancer or a third party’s open redirect, is allowed by design.
  • Allowing internal addresses narrows the policy. Where a connection allows internal addresses (the default for device and plant connectors), the policy refuses only the metadata service, multicast and unspecified addresses. On those connections the remaining control is that the operator who configured the connection is authenticated and audited.
  • Read-only is a check on statement shape, not authorisation. ValidateSQL guarantees statements that read. It does not limit which tables a query reads. That limit is the database account’s own permissions, so connect FACE with a least-privilege, read-only database user.
  • Parsing an address is not reaching it. Correct dialling code gives an IPv6-only pod no route to an IPv4-only provider. That route is NAT64 or egress configuration in the deployment.