The egress policy: which URLs Ankusa may send a hook to (CWE-918).
An HTTP sink forwards the raw webhook body to a URL somebody configured — an
operator in YAML, or a tenant through the admin API. Without a policy that URL
may be http://127.0.0.1:4003/... (Ankusa's own admin listener), the cloud
metadata service (http://169.254.169.254/...), or any service on the
private network. Ankusa.Sink.Http therefore calls validate_target/3 (via
pin/3) on every attempt, so a name that resolves somewhere else on the
next retry is checked again, and a deny is a permanent failure.
The policy
Ankusa.Config's egress section (YAML egress:):
:allow_insecure_http—false: onlyhttpsURLs.truealso allowshttp, which carries the hook in the clear:deny_cidrs— an address inside any of these is refused. Default: loopback, RFC 1918, carrier-grade NAT, link-local (the metadata service), unique-local and link-local IPv6, and the unspecified addresses:allow_cidrs— a deny exception: an address inside one of these passes even when a deny CIDR also holds it (["10.20.0.0/16"]for the one internal network sinks live on):allow_hosts— hostnames exempt from the CIDR check, for sinks reached by service name (["worker"]). The name is still resolved and pinned:deny_hosts— hostnames refused before any lookup; wins over:allow_hosts. Default:["metadata.google.internal"]
A name is resolved for both address families with the OTP resolver
(:inet.getaddrs), and the
target is refused when any of its addresses is denied and not allowed: a
name with one public and one private record is a rebinding setup, not a
multi-homed service. IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) are
matched as the IPv4 address they stand for.
Pinning
pin/3 returns what to send instead of the URL it was given: the URL names the
checked IP address and a host header carries the name, so the connection
cannot go anywhere the check did not look and nothing is left to rebind. For
https the connection options also carry hostname: name, which Mint uses for
SNI and for the certificate check, so the peer must still present a certificate
valid for the name (peer verification cannot be switched off without
allow_insecure_tls, see Ankusa.HttpClient).
Every connection a pool opens to the pinned URL goes to that address:
Ankusa.HttpClient.Pools keeps one pool per pinned address and hostname, and
pin/3 runs again, resolving the name afresh, before every request. A name that
later resolves to a denied address is refused on that request, before any pool
is touched; one that resolves to another public address gets a pool of its own,
and the old one is swept once idle.
Reasons in {:error, {:egress_denied, reason}} carry the host and the
offending address, never the URL: it may hold credentials.
Summary
Types
What to send: url, extra request headers, and connect_options for the client.
Functions
The default policy: https only, every private, loopback and link-local range denied.
validate_target/3, returning what to send: see "Pinning".
Raise ArgumentError, naming the key, unless config.egress is a well-formed
policy (Ankusa.Config calls it, and so does the instance at boot), and unless
every Ankusa.Sink.Http the config names — lifecycle sinks and the static
sources — passes Ankusa.Sink.Http.validate_opts!/1.
Raise ArgumentError unless policy is a well-formed policy map. See validate_config!/1.
Check url against policy: :ok, or {:error, {:egress_denied, reason}}.
Types
Functions
@spec default_policy() :: policy()
The default policy: https only, every private, loopback and link-local range denied.
@spec pin(String.t(), policy(), keyword()) :: {:ok, pinned()} | {:error, {:egress_denied, reason()}}
validate_target/3, returning what to send: see "Pinning".
Takes validate_target/3's options.
@spec validate_config!(Ankusa.Config.t()) :: :ok
Raise ArgumentError, naming the key, unless config.egress is a well-formed
policy (Ankusa.Config calls it, and so does the instance at boot), and unless
every Ankusa.Sink.Http the config names — lifecycle sinks and the static
sources — passes Ankusa.Sink.Http.validate_opts!/1.
@spec validate_policy!(term()) :: :ok
Raise ArgumentError unless policy is a well-formed policy map. See validate_config!/1.
@spec validate_target(String.t(), policy(), keyword()) :: :ok | {:error, {:egress_denied, reason()}}
Check url against policy: :ok, or {:error, {:egress_denied, reason}}.
Options: :resolver (a (charlist, family -> {:ok, [ip]} | {:error, reason}
function, default :inet.getaddrs with the timeout) and :timeout (the lookup deadline in
milliseconds, default 5000).