PRD 0001: Per-agent egress proxy via pipelock #1
@@ -650,7 +650,9 @@ cmd_start() {
|
|||||||
|
|
||||||
# Set up SSH keys and config.
|
# Set up SSH keys and config.
|
||||||
if [ "${#SSH_ENTRIES[@]}" -gt 0 ]; then
|
if [ "${#SSH_ENTRIES[@]}" -gt 0 ]; then
|
||||||
ssh_setup "$CONTAINER" "$STAGE_DIR" "${SSH_ENTRIES[@]}"
|
local PIPELOCK_PROXY_HOST_PORT
|
||||||
|
PIPELOCK_PROXY_HOST_PORT="$(pipelock_proxy_host_port "$SLUG")"
|
||||||
|
ssh_setup "$CONTAINER" "$STAGE_DIR" "$PIPELOCK_PROXY_HOST_PORT" "${SSH_ENTRIES[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# When --cwd is on, ship the host repo's .git directory in via `docker cp`
|
# When --cwd is on, ship the host repo's .git directory in via `docker cp`
|
||||||
|
|||||||
+33
-4
@@ -108,6 +108,17 @@ pipelock_proxy_url() {
|
|||||||
printf 'http://%s:%s' "$name" "$CLAUDE_BOTTLE_PIPELOCK_PORT"
|
printf 'http://%s:%s' "$name" "$CLAUDE_BOTTLE_PIPELOCK_PORT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pipelock_proxy_host_port <slug> — prints <sidecar>:<port> (no scheme),
|
||||||
|
# suitable for socat's PROXY: directive in an SSH ProxyCommand. The
|
||||||
|
# agent's --internal network has no default route, so SSH (and any other
|
||||||
|
# raw TCP) must tunnel via pipelock's HTTP CONNECT.
|
||||||
|
pipelock_proxy_host_port() {
|
||||||
|
local slug="${1:?pipelock_proxy_host_port: missing slug}"
|
||||||
|
local name
|
||||||
|
name="$(pipelock_container_name "$slug")"
|
||||||
|
printf '%s:%s' "$name" "$CLAUDE_BOTTLE_PIPELOCK_PORT"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Allowlist resolution --------------------------------------------------
|
# --- Allowlist resolution --------------------------------------------------
|
||||||
|
|
||||||
# pipelock_bottle_allowlist <manifest_file> <bottle_name>
|
# pipelock_bottle_allowlist <manifest_file> <bottle_name>
|
||||||
@@ -139,12 +150,29 @@ pipelock_bottle_allowlist() {
|
|||||||
' "$manifest_file"
|
' "$manifest_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pipelock_bottle_ssh_hostnames <manifest_file> <bottle_name>
|
||||||
|
#
|
||||||
|
# Prints one hostname per line for each entry in bottles[<name>].ssh[].Hostname.
|
||||||
|
# These need to reach pipelock's allowlist so the agent can tunnel SSH
|
||||||
|
# through pipelock via HTTP CONNECT (see ssh_setup's ProxyCommand
|
||||||
|
# wiring). Empty output if the bottle has no ssh entries.
|
||||||
|
pipelock_bottle_ssh_hostnames() {
|
||||||
|
local manifest_file="${1:?pipelock_bottle_ssh_hostnames: missing manifest file}"
|
||||||
|
local bottle_name="${2:?pipelock_bottle_ssh_hostnames: missing bottle name}"
|
||||||
|
|
||||||
|
jq -r --arg b "$bottle_name" '
|
||||||
|
.bottles[$b].ssh // [] | .[] | .Hostname // empty
|
||||||
|
' "$manifest_file"
|
||||||
|
}
|
||||||
|
|
||||||
# pipelock_effective_allowlist <manifest_file> <bottle_name>
|
# pipelock_effective_allowlist <manifest_file> <bottle_name>
|
||||||
#
|
#
|
||||||
# Prints the deduplicated union of the baked-in default allowlist and
|
# Prints the deduplicated union of: the baked-in default allowlist, the
|
||||||
# the bottle's declared allowlist, one hostname per line, sorted for
|
# bottle's declared egress.allowlist, and any bottle.ssh[].Hostname
|
||||||
# stability. This is the single source of truth callers should use for
|
# entries (so SSH tunneling through pipelock is permitted by the same
|
||||||
# both YAML generation and the preflight summary.
|
# allowlist check that gates HTTP CONNECT). One hostname per line,
|
||||||
|
# sorted for stability. This is the single source of truth callers
|
||||||
|
# should use for both YAML generation and the preflight summary.
|
||||||
pipelock_effective_allowlist() {
|
pipelock_effective_allowlist() {
|
||||||
local manifest_file="${1:?pipelock_effective_allowlist: missing manifest file}"
|
local manifest_file="${1:?pipelock_effective_allowlist: missing manifest file}"
|
||||||
local bottle_name="${2:?pipelock_effective_allowlist: missing bottle name}"
|
local bottle_name="${2:?pipelock_effective_allowlist: missing bottle name}"
|
||||||
@@ -152,6 +180,7 @@ pipelock_effective_allowlist() {
|
|||||||
{
|
{
|
||||||
printf '%s\n' "$CLAUDE_BOTTLE_PIPELOCK_DEFAULT_ALLOWLIST"
|
printf '%s\n' "$CLAUDE_BOTTLE_PIPELOCK_DEFAULT_ALLOWLIST"
|
||||||
pipelock_bottle_allowlist "$manifest_file" "$bottle_name"
|
pipelock_bottle_allowlist "$manifest_file" "$bottle_name"
|
||||||
|
pipelock_bottle_ssh_hostnames "$manifest_file" "$bottle_name"
|
||||||
} | awk 'NF && !seen[$0]++' | LC_ALL=C sort
|
} | awk 'NF && !seen[$0]++' | LC_ALL=C sort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -89,7 +89,12 @@ ssh_validate_entries() {
|
|||||||
ssh_setup() {
|
ssh_setup() {
|
||||||
local container="${1:?ssh_setup: missing container}"
|
local container="${1:?ssh_setup: missing container}"
|
||||||
local stage_dir="${2:?ssh_setup: missing stage dir}"
|
local stage_dir="${2:?ssh_setup: missing stage dir}"
|
||||||
shift 2
|
# proxy_host_port is the pipelock sidecar as <host>:<port> (no scheme).
|
||||||
|
# Used as socat's PROXY: argument so the agent can reach SSH hosts
|
||||||
|
# over the agent's --internal network — the only egress route is the
|
||||||
|
# pipelock CONNECT proxy. Required.
|
||||||
|
local proxy_host_port="${3:?ssh_setup: missing proxy_host_port}"
|
||||||
|
shift 3
|
||||||
|
|
||||||
local container_home="${CLAUDE_BOTTLE_CONTAINER_HOME:-/home/node}"
|
local container_home="${CLAUDE_BOTTLE_CONTAINER_HOME:-/home/node}"
|
||||||
local container_ssh="${container_home}/.ssh"
|
local container_ssh="${container_home}/.ssh"
|
||||||
@@ -140,8 +145,18 @@ ssh_setup() {
|
|||||||
# No IdentityFile — IdentityAgent points SSH at the public (forwarded)
|
# No IdentityFile — IdentityAgent points SSH at the public (forwarded)
|
||||||
# socket. Pointing at the real agent socket directly would be rejected
|
# socket. Pointing at the real agent socket directly would be rejected
|
||||||
# by ssh-agent's UID-match check (see file header).
|
# by ssh-agent's UID-match check (see file header).
|
||||||
printf 'Host %s\n HostName %s\n User %s\n Port %s\n IdentityAgent %s\n\n' \
|
#
|
||||||
"$name" "$hostname" "$user" "$port" "$public_socket" >> "$config_file"
|
# ProxyCommand tunnels the SSH connection through pipelock via HTTP
|
||||||
|
# CONNECT. The agent container has no default route (--internal
|
||||||
|
# network); pipelock is the only path to anywhere. socat's PROXY:
|
||||||
|
# mode does CONNECT host:port to the proxy. %h / %p expand to this
|
||||||
|
# block's HostName / Port. The SSH host must also appear in
|
||||||
|
# pipelock's allowlist — pipelock_effective_allowlist auto-includes
|
||||||
|
# bottle.ssh[].Hostname entries so this just works for declared
|
||||||
|
# hosts.
|
||||||
|
printf 'Host %s\n HostName %s\n User %s\n Port %s\n IdentityAgent %s\n ProxyCommand socat - PROXY:%s:%%h:%%p,proxyport=%s\n\n' \
|
||||||
|
"$name" "$hostname" "$user" "$port" "$public_socket" \
|
||||||
|
"${proxy_host_port%:*}" "${proxy_host_port##*:}" >> "$config_file"
|
||||||
|
|
||||||
if [ -n "$known_host_key" ]; then
|
if [ -n "$known_host_key" ]; then
|
||||||
# Write under both the Host alias and the Hostname so SSH finds the key
|
# Write under both the Host alias and the Hostname so SSH finds the key
|
||||||
|
|||||||
Reference in New Issue
Block a user