Default-block Azure WireServer's public address (closes #245) #334
@@ -157,6 +157,11 @@ private and reserved ranges — RFC 1918, loopback, CGNAT, link-local and
|
|||||||
the rest — are refused, which stops a target from being used to make
|
the rest — are refused, which stops a target from being used to make
|
||||||
webhooker probe the network it sits in.
|
webhooker probe the network it sits in.
|
||||||
|
|
||||||
|
Besides the private and reserved ranges, the default blocklist refuses
|
||||||
|
public cloud metadata addresses: currently only `168.63.129.16`, Azure's
|
||||||
|
WireServer, which serves an Azure VM its credentials. Because it is a
|
||||||
|
public address, listing it in `ALLOWED_EGRESS_CIDRS` reopens it.
|
||||||
|
|
||||||
That default is also inconvenient for the thing webhooker is mostly
|
That default is also inconvenient for the thing webhooker is mostly
|
||||||
for: taking a public webhook and forwarding it to something on your own
|
for: taking a public webhook and forwarding it to something on your own
|
||||||
network. A container on the same Docker network, a box on `10.x`, a
|
network. A container on the same Docker network, a box on `10.x`, a
|
||||||
@@ -242,7 +247,8 @@ Two things this setting cannot do:
|
|||||||
encodings, which the default blocklist does not match. A publicly
|
encodings, which the default blocklist does not match. A publicly
|
||||||
routable metadata address is not listed here, because nothing on this
|
routable metadata address is not listed here, because nothing on this
|
||||||
list can be reopened and blocking one that way would leave you no
|
list can be reopened and blocking one that way would leave you no
|
||||||
escape hatch at all.
|
escape hatch at all; Azure's `168.63.129.16` is refused by the default
|
||||||
|
blocklist instead, as described above.
|
||||||
|
|
||||||
This list is not exhaustive of every cloud's metadata address — if
|
This list is not exhaustive of every cloud's metadata address — if
|
||||||
yours is not here, do not allowlist the block that contains it.
|
yours is not here, do not allowlist the block that contains it.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var (
|
|||||||
"hostname resolved to no IP addresses",
|
"hostname resolved to no IP addresses",
|
||||||
)
|
)
|
||||||
errBlockedIP = errors.New(
|
errBlockedIP = errors.New(
|
||||||
"blocked private/reserved IP range",
|
"blocked private, reserved or cloud metadata address",
|
||||||
)
|
)
|
||||||
errBlockedMetadata = errors.New(
|
errBlockedMetadata = errors.New(
|
||||||
"blocked link-local or cloud instance metadata " +
|
"blocked link-local or cloud instance metadata " +
|
||||||
@@ -37,9 +37,10 @@ var (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// blockedNetworks contains all private/reserved IP ranges
|
// blockedNetworks is the default blocklist: the private and
|
||||||
// that should be blocked to prevent SSRF attacks. An operator
|
// reserved IP ranges, plus the public cloud metadata addresses,
|
||||||
// can permit specific blocks out of this set with
|
// that are blocked to prevent SSRF attacks. An operator can
|
||||||
|
// permit specific blocks out of this set with
|
||||||
// ALLOWED_EGRESS_CIDRS; see Guard.
|
// ALLOWED_EGRESS_CIDRS; see Guard.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // package-level network list is appropriate here
|
//nolint:gochecknoglobals // package-level network list is appropriate here
|
||||||
@@ -122,6 +123,8 @@ func init() {
|
|||||||
"::1/128",
|
"::1/128",
|
||||||
"fc00::/7",
|
"fc00::/7",
|
||||||
"fe80::/10",
|
"fe80::/10",
|
||||||
|
// Azure WireServer, a public address that serves VM credentials.
|
||||||
|
"168.63.129.16/32",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Every entry is named. The set must not grow or shrink
|
// Every entry is named. The set must not grow or shrink
|
||||||
@@ -216,8 +219,8 @@ func matchesAny(networks []*net.IPNet, ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isBlockedIP checks whether an IP address falls within
|
// isBlockedIP checks whether an IP address falls within
|
||||||
// any blocked private/reserved network range, before any
|
// the default blocklist, before any operator allowlist is
|
||||||
// operator allowlist is considered.
|
// considered.
|
||||||
func isBlockedIP(ip net.IP) bool {
|
func isBlockedIP(ip net.IP) bool {
|
||||||
return matchesAny(blockedNetworks, ip)
|
return matchesAny(blockedNetworks, ip)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -390,6 +390,41 @@ func TestGuardAllowlist_PublicUnaffected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGuardAllowlist_AzureWireServerReopenable covers Azure's
|
||||||
|
// WireServer, a public address that serves VM credentials. The
|
||||||
|
// default guard refuses it, but because it is public it sits in
|
||||||
|
// the default blocklist rather than the unconditional set, so an
|
||||||
|
// operator who lists it can reach it.
|
||||||
|
func TestGuardAllowlist_AzureWireServerReopenable(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const wireServerIP = "168.63.129.16"
|
||||||
|
|
||||||
|
target := "http://" + wireServerIP + "/?comp=versions"
|
||||||
|
|
||||||
|
defaultGuard := delivery.NewTestGuard()
|
||||||
|
|
||||||
|
err := defaultGuard.ValidateTargetURL(context.Background(), target)
|
||||||
|
require.Error(t, err,
|
||||||
|
"WireServer must be refused with no allowlist set",
|
||||||
|
)
|
||||||
|
assert.NotContains(t, err.Error(), metadataRefusalClause,
|
||||||
|
"WireServer must be refused by the default blocklist, "+
|
||||||
|
"which an allowlist can override",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertDialRefused(t, defaultGuard, target)
|
||||||
|
|
||||||
|
listed := delivery.NewTestGuard(
|
||||||
|
netip.MustParsePrefix(wireServerIP + "/32"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t,
|
||||||
|
listed.ValidateTargetURL(context.Background(), target),
|
||||||
|
"an operator who lists WireServer must be able to reach it",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TestGuardCheckIP_BothPathsShareOneDecision asserts that the
|
// TestGuardCheckIP_BothPathsShareOneDecision asserts that the
|
||||||
// validator and the dialer are not two policies that happen to
|
// validator and the dialer are not two policies that happen to
|
||||||
// agree: both are defined in terms of checkIP, so the exported
|
// agree: both are defined in terms of checkIP, so the exported
|
||||||
|
|||||||
Reference in New Issue
Block a user