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:
| Address | Refusal |
|---|---|
Cloud instance-metadata service: 169.254.169.254, and its IPv6 form fd00:ec2::254 | refusing to dial …: that is the cloud instance-metadata service, not a data source. This refusal has no opt-in |
| Multicast | refusing to dial multicast address … |
| The unspecified address | refusing 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 anAuthorizationheader 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,DESCorEXPLAIN. Anything else is refused withonly 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 negativeFallbackDelay, which gives RFC 8305 Happy Eyeballs behaviour.TestNothingPinsAnAddressFamilyfails on anytcp4,tcp6,ip4orip6literal, or on a negative fallback delay. - Host:port strings are built and parsed properly. Code uses
net.JoinHostPortto build addresses andnet.SplitHostPortto parse them, never string concatenation or a colon search. Those break every IPv6 literal.internal/extractors/ipv6_hostport_guard_test.goparses 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.
ValidateSQLguarantees 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.