fix(pipelock): exempt declared ssh hosts from SSRF blocks
Pipelock's default SSRF blocklist includes 100.64.0.0/10 (RFC 6598 CGNAT, where Tailscale IPs live) plus all RFC 1918 / link-local ranges, so a CONNECT to a bottle.ssh[] target on Tailscale was rejected with `scanner: ssrf, reason: SSRF blocked: <ip> resolves to internal IP` even after the host appeared in api_allowlist. Fix: while emitting the YAML, classify each bottle.ssh[].Hostname: - IPv4 literal -> ssrf.ip_allowlist as <ip>/32 (canonical CIDR). - Hostname -> trusted_domains (hostname-based SSRF exemption). Both blocks are emitted only when entries exist, so bottles with no ssh / no private-IP targets still produce a minimal config. Assisted-by: Claude Code
This commit is contained in:
@@ -165,6 +165,50 @@ pipelock_bottle_ssh_hostnames() {
|
||||
' "$manifest_file"
|
||||
}
|
||||
|
||||
# _pipelock_is_ipv4_literal <s> — exit 0 if <s> looks like an IPv4
|
||||
# literal (four dot-separated octets). Pipelock's SSRF check fires on
|
||||
# the resolved IP, so a Hostname that's already an IP literal needs
|
||||
# `ssrf.ip_allowlist`, while a hostname needs `trusted_domains`.
|
||||
_pipelock_is_ipv4_literal() {
|
||||
local s="${1:?}"
|
||||
[[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
}
|
||||
|
||||
# pipelock_bottle_ssh_trusted_domains <manifest> <bottle>
|
||||
#
|
||||
# Hostname-shaped ssh[].Hostname entries that should bypass pipelock's
|
||||
# SSRF check (so a name resolving to a private IP — e.g. internal API
|
||||
# behind a VPN — is reachable). IP-literal entries are excluded;
|
||||
# trusted_domains is hostname-based per pipelock's docs.
|
||||
pipelock_bottle_ssh_trusted_domains() {
|
||||
local manifest_file="${1:?}"
|
||||
local bottle_name="${2:?}"
|
||||
local h
|
||||
while IFS= read -r h; do
|
||||
[ -z "$h" ] && continue
|
||||
_pipelock_is_ipv4_literal "$h" && continue
|
||||
printf '%s\n' "$h"
|
||||
done < <(pipelock_bottle_ssh_hostnames "$manifest_file" "$bottle_name")
|
||||
}
|
||||
|
||||
# pipelock_bottle_ssh_ip_cidrs <manifest> <bottle>
|
||||
#
|
||||
# Emits one canonical /32 CIDR per IPv4-literal ssh[].Hostname so they
|
||||
# pass pipelock's SSRF IP-range check (which blocks RFC 1918, RFC 6598
|
||||
# CGNAT, link-local, loopback, etc. by default). Hostnames are skipped
|
||||
# — they go through trusted_domains instead.
|
||||
pipelock_bottle_ssh_ip_cidrs() {
|
||||
local manifest_file="${1:?}"
|
||||
local bottle_name="${2:?}"
|
||||
local h
|
||||
while IFS= read -r h; do
|
||||
[ -z "$h" ] && continue
|
||||
if _pipelock_is_ipv4_literal "$h"; then
|
||||
printf '%s/32\n' "$h"
|
||||
fi
|
||||
done < <(pipelock_bottle_ssh_hostnames "$manifest_file" "$bottle_name")
|
||||
}
|
||||
|
||||
# pipelock_effective_allowlist <manifest_file> <bottle_name>
|
||||
#
|
||||
# Prints the deduplicated union of: the baked-in default allowlist, the
|
||||
@@ -285,6 +329,32 @@ pipelock_write_yaml() {
|
||||
printf 'forward_proxy:\n'
|
||||
printf ' enabled: true\n'
|
||||
printf '\n'
|
||||
# SSRF exemptions for declared SSH hosts. Pipelock blocks the CGNAT
|
||||
# range (100.64.0.0/10, where Tailscale IPs live) and the rest of
|
||||
# RFC 1918 / link-local by default. Hostname entries go to
|
||||
# trusted_domains; IP-literal entries to ssrf.ip_allowlist as /32.
|
||||
local trusted_count=0 ssrf_count=0
|
||||
local td
|
||||
while IFS= read -r td; do
|
||||
[ -z "$td" ] && continue
|
||||
if [ "$trusted_count" -eq 0 ]; then
|
||||
printf 'trusted_domains:\n'
|
||||
fi
|
||||
printf ' - "%s"\n' "$td"
|
||||
trusted_count=$((trusted_count + 1))
|
||||
done < <(pipelock_bottle_ssh_trusted_domains "$manifest_file" "$bottle_name")
|
||||
[ "$trusted_count" -gt 0 ] && printf '\n'
|
||||
local cidr
|
||||
while IFS= read -r cidr; do
|
||||
[ -z "$cidr" ] && continue
|
||||
if [ "$ssrf_count" -eq 0 ]; then
|
||||
printf 'ssrf:\n'
|
||||
printf ' ip_allowlist:\n'
|
||||
fi
|
||||
printf ' - "%s"\n' "$cidr"
|
||||
ssrf_count=$((ssrf_count + 1))
|
||||
done < <(pipelock_bottle_ssh_ip_cidrs "$manifest_file" "$bottle_name")
|
||||
[ "$ssrf_count" -gt 0 ] && printf '\n'
|
||||
printf 'dlp:\n'
|
||||
printf ' include_defaults: true\n'
|
||||
printf ' scan_env: true\n'
|
||||
|
||||
Reference in New Issue
Block a user