feat(ssh-gate): retarget ssh provisioner at the new gate
PRD 0007: stop tunneling ssh through pipelock. Each Host block in the agent's ~/.ssh/config now points at the gate container + the per-entry listen port; HostKeyAlias preserves host-key validation against the real upstream name, and CheckHostIP=no skips the resolved-IP path (which would otherwise hit the gate's IP). known_hosts collapses to a single entry per upstream keyed on the alias. The pipelock_proxy_host_port import is gone from this module; the function itself becomes dead code and gets removed alongside the broader pipelock SSH carve-outs in the next commit.
This commit is contained in:
@@ -17,11 +17,11 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from ....log import info
|
from ....log import die, info
|
||||||
from ....util import expand_tilde
|
from ....util import expand_tilde
|
||||||
from .. import util as docker_mod
|
from .. import util as docker_mod
|
||||||
from ..bottle_plan import DockerBottlePlan
|
from ..bottle_plan import DockerBottlePlan
|
||||||
from ..pipelock import pipelock_proxy_host_port
|
from ..ssh_gate import ssh_gate_host
|
||||||
|
|
||||||
|
|
||||||
def provision_ssh(plan: DockerBottlePlan, target: str) -> None:
|
def provision_ssh(plan: DockerBottlePlan, target: str) -> None:
|
||||||
@@ -61,13 +61,23 @@ def provision_ssh(plan: DockerBottlePlan, target: str) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
container = target
|
container = target
|
||||||
proxy_host_port = pipelock_proxy_host_port(plan.slug)
|
gate_target = ssh_gate_host(plan.slug)
|
||||||
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
|
container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node")
|
||||||
container_ssh = f"{container_home}/.ssh"
|
container_ssh = f"{container_home}/.ssh"
|
||||||
agent_socket = "/run/claude-bottle-agent.sock"
|
agent_socket = "/run/claude-bottle-agent.sock"
|
||||||
public_socket = "/run/claude-bottle-agent-public.sock"
|
public_socket = "/run/claude-bottle-agent-public.sock"
|
||||||
keys_dir = "/root/.claude-bottle-keys"
|
keys_dir = "/root/.claude-bottle-keys"
|
||||||
|
|
||||||
|
# Per-entry listen ports come off the gate plan (PRD 0007).
|
||||||
|
# Indexed by the bottle.ssh entry's Host alias so each ssh_config
|
||||||
|
# block knows which port its forwarder lives on.
|
||||||
|
upstreams_by_alias = {u.bottle_host_alias: u for u in plan.gate_plan.upstreams}
|
||||||
|
if set(upstreams_by_alias) != {e.Host for e in bottle.ssh}:
|
||||||
|
die(
|
||||||
|
"ssh-gate upstream table is out of sync with bottle.ssh; "
|
||||||
|
"this is an internal bug"
|
||||||
|
)
|
||||||
|
|
||||||
# ~/.ssh for node (700, owned by node).
|
# ~/.ssh for node (700, owned by node).
|
||||||
docker_mod.docker_exec_root(container, ["mkdir", "-p", container_ssh])
|
docker_mod.docker_exec_root(container, ["mkdir", "-p", container_ssh])
|
||||||
docker_mod.docker_exec_root(container, ["chown", "node:node", container_ssh])
|
docker_mod.docker_exec_root(container, ["chown", "node:node", container_ssh])
|
||||||
@@ -85,16 +95,15 @@ def provision_ssh(plan: DockerBottlePlan, target: str) -> None:
|
|||||||
known_hosts_file.write_text("")
|
known_hosts_file.write_text("")
|
||||||
known_hosts_file.chmod(0o600)
|
known_hosts_file.chmod(0o600)
|
||||||
|
|
||||||
proxy_host, _, proxy_port = proxy_host_port.partition(":")
|
|
||||||
|
|
||||||
container_key_paths: list[str] = []
|
container_key_paths: list[str] = []
|
||||||
for entry in bottle.ssh:
|
for entry in bottle.ssh:
|
||||||
name = entry.Host
|
name = entry.Host
|
||||||
key = expand_tilde(entry.IdentityFile)
|
key = expand_tilde(entry.IdentityFile)
|
||||||
hostname = entry.Hostname
|
hostname = entry.Hostname
|
||||||
user = entry.User
|
user = entry.User
|
||||||
port = entry.Port
|
|
||||||
known_host_key = entry.KnownHostKey
|
known_host_key = entry.KnownHostKey
|
||||||
|
upstream = upstreams_by_alias[name]
|
||||||
|
listen_port = upstream.listen_port
|
||||||
|
|
||||||
key_basename = os.path.basename(key)
|
key_basename = os.path.basename(key)
|
||||||
container_key_path = f"{keys_dir}/{key_basename}"
|
container_key_path = f"{keys_dir}/{key_basename}"
|
||||||
@@ -110,35 +119,32 @@ def provision_ssh(plan: DockerBottlePlan, target: str) -> None:
|
|||||||
|
|
||||||
container_key_paths.append(container_key_path)
|
container_key_paths.append(container_key_path)
|
||||||
|
|
||||||
# ProxyCommand tunnels SSH through pipelock via HTTP
|
# Each Host block points at the gate container + its
|
||||||
# CONNECT. %h / %p expand to this block's HostName /
|
# per-entry listen port. HostKeyAlias makes ssh validate
|
||||||
# Port. socat's PROXY: mode does CONNECT host:port to
|
# the host key against `hostname` (the real upstream
|
||||||
# the proxy.
|
# name) instead of the gate container; CheckHostIP=no
|
||||||
|
# skips the resolved-IP lookup, which would also point at
|
||||||
|
# the gate.
|
||||||
block = (
|
block = (
|
||||||
f"Host {name}\n"
|
f"Host {name}\n"
|
||||||
f" HostName {hostname}\n"
|
f" HostName {gate_target}\n"
|
||||||
f" User {user}\n"
|
f" User {user}\n"
|
||||||
f" Port {port}\n"
|
f" Port {listen_port}\n"
|
||||||
f" IdentityAgent {public_socket}\n"
|
f" IdentityAgent {public_socket}\n"
|
||||||
f" ProxyCommand socat - PROXY:{proxy_host}:%h:%p,proxyport={proxy_port}\n"
|
f" HostKeyAlias {hostname}\n"
|
||||||
|
f" CheckHostIP no\n"
|
||||||
f"\n"
|
f"\n"
|
||||||
)
|
)
|
||||||
with config_file.open("a") as f:
|
with config_file.open("a") as f:
|
||||||
f.write(block)
|
f.write(block)
|
||||||
|
|
||||||
if known_host_key:
|
if known_host_key:
|
||||||
entries_to_write: list[str] = []
|
# HostKeyAlias makes ssh look up known_hosts under
|
||||||
if port == "22":
|
# `hostname` (the upstream's real name / IP literal),
|
||||||
entries_to_write.append(f"{name} {known_host_key}\n")
|
# not the gate container. One unambiguous entry per
|
||||||
if hostname != name:
|
# ssh entry.
|
||||||
entries_to_write.append(f"{hostname} {known_host_key}\n")
|
|
||||||
else:
|
|
||||||
entries_to_write.append(f"[{name}]:{port} {known_host_key}\n")
|
|
||||||
if hostname != name:
|
|
||||||
entries_to_write.append(f"[{hostname}]:{port} {known_host_key}\n")
|
|
||||||
with known_hosts_file.open("a") as f:
|
with known_hosts_file.open("a") as f:
|
||||||
for e in entries_to_write:
|
f.write(f"{hostname} {known_host_key}\n")
|
||||||
f.write(e)
|
|
||||||
|
|
||||||
# Boot the agent, load each key, delete the key files, then
|
# Boot the agent, load each key, delete the key files, then
|
||||||
# start the root-owned socat forwarder. One docker exec so the
|
# start the root-owned socat forwarder. One docker exec so the
|
||||||
|
|||||||
Reference in New Issue
Block a user