Network Fetch Guard
Wallet-service endpoints fetch URLs that are effectively caller-supplied: issuer metadata, request_uri,
credential_offer_uri, and token/credential endpoints all come from an offer or presentation request, not from a
trusted admin. Without a guard, a malicious offer could point one of those fetches at an internal address —
another tenant's service, an admin endpoint, or a cloud provider's metadata service — and have the Enterprise API
make that request from inside your network. network-fetch-guard.conf controls what those fetches are allowed to
reach. It is a base feature, so it is always loaded, not something you switch on in _features.conf.
Some categories are never reachable, regardless of configuration: link-local addresses (which includes
169.254.169.254, the cloud-metadata address used by AWS/GCP/Azure), multicast, and wildcard addresses. There is
no setting that opens these up.
Example File
This is the shipped, most-restrictive default:
allowLoopback = false
allowPrivateNetworks = false
additionalTrustedHostSuffixes = []
Fields
allowLoopback
Boolean — defaults to false. Set true only if this deployment's issuer, verifier, and wallet services run as
siblings on the same host (for example, local development, or a single all-in-one container) and the wallet
legitimately needs to reach an issuer at 127.0.0.1 or localhost.
allowPrivateNetworks
Boolean — defaults to false. Set true only if those services legitimately reach each other over a private
network (RFC 1918 IPv4, or an IPv6 unique-local address) — for example, sibling containers on the same Docker
network, or pods in the same Kubernetes cluster.
Both flags are process-wide: turning one on to fix one tenant's topology reopens that whole address range to
every tenant's caller-supplied URLs on this process — including the internal targets this guard exists to block.
On a shared multi-tenant deployment, prefer additionalTrustedHostSuffixes below instead.
additionalTrustedHostSuffixes
Array of strings, optional — defaults to an empty list. Hostnames listed here (and any subdomain of them) are
allowed to resolve to a loopback or private address, without opening up allowLoopback / allowPrivateNetworks
for anyone else.
This exists for one specific situation: DNS hairpinning. Every organization is hosted at
{organizationId}.{baseDomain} (baseDomain is set in enterprise.conf), and that hostname is
always trusted automatically — you don't need to list it here. But some DNS resolvers optimize a request for a
name that points back at the resolver's own network by handing back a loopback or private address instead of the
real public one, purely as a routing shortcut. That means a wallet fetching its own organization's issuer, at
its own public hostname, can sometimes see that hostname resolve to 127.0.0.1 or a private IP — which looks
identical, at the IP level, to an attacker trying to reach something internal. The guard can't tell those two
cases apart from the IP address alone, so it trusts the hostname itself instead, for names you control.
Use additionalTrustedHostSuffixes only if you need to extend that same trust to a hostname beyond your own
baseDomain — most deployments should leave it empty.
Trusting a hostname this way never overrides the always-blocked categories above. Even a trusted hostname cannot be used to reach the cloud-metadata address, for example.
