Fix SSH auth: respect ~/.ssh/config instead of defaulting to root

CoulombCore (92.205.130.254) authenticates as tegwick via id_ops in
ssh config, not root. Omit SANDBOXER_SSH_USER to let OpenSSH apply config;
set SANDBOXER_SSH_USER only to override.
This commit is contained in:
2026-06-23 14:16:16 +02:00
parent 8a39eaba34
commit 939c4e1aff
5 changed files with 36 additions and 12 deletions

View File

@@ -16,9 +16,10 @@ Provision a compose-based e2e sandbox via `ext.compose-ssh` (e2e-framework linea
- Sufficient disk for images
```bash
export SANDBOXER_HOST=<ip-or-hostname> # e.g. coulombcore IP
export SANDBOXER_SSH_USER=root # optional
export SANDBOXER_SSH_KEY=~/.ssh/id_rsa # optional
export SANDBOXER_HOST=coulombcore # or 92.205.130.254
# Omit SANDBOXER_SSH_USER to use ~/.ssh/config (CoulombCore: tegwick + id_ops)
# export SANDBOXER_SSH_USER=tegwick # only if not in ssh config
# export SANDBOXER_SSH_KEY=~/.ssh/id_ops # only to override config
```
## Create

View File

@@ -12,5 +12,5 @@ capabilities:
pricing_model: self-hosted
config:
base_dir: /tmp/sandboxer
ssh_user: root
# ssh_user omitted — uses SANDBOXER_SSH_USER or ~/.ssh/config (e.g. tegwick@coulombcore)
compose_timeout_s: 180

View File

@@ -18,7 +18,7 @@ class ComposeSSHExtension:
def __init__(self, config: dict[str, Any] | None = None) -> None:
cfg = config or {}
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer")
self.ssh_user: str = cfg.get("ssh_user", "root")
self.ssh_user: str | None = cfg.get("ssh_user")
self.compose_timeout_s: int = int(cfg.get("compose_timeout_s", 180))
def provision(
@@ -33,7 +33,7 @@ class ComposeSSHExtension:
sandbox_id = inputs.get("sandbox_id") or str(uuid.uuid4())[:8]
remote_dir = f"{self.base_dir}/{sandbox_id}"
ssh = SSHConfig.from_env(host, user=self.ssh_user)
ssh = SSHConfig.from_env(host, user=self.ssh_user or None)
rc, out = ssh.run(f"mkdir -p {remote_dir}")
if rc != 0:
@@ -64,7 +64,8 @@ class ComposeSSHExtension:
def wait_ready(self, handle: dict[str, str]) -> dict[str, str]:
"""Confirm compose services are running (no HTTP health polling)."""
ssh = SSHConfig.from_env(handle["host"], user=handle.get("ssh_user", self.ssh_user))
ssh_user = handle.get("ssh_user") or self.ssh_user or None
ssh = SSHConfig.from_env(handle["host"], user=ssh_user)
project = handle["compose_project"]
remote_dir = handle["remote_dir"]
compose_file = handle["compose_file"]
@@ -83,7 +84,8 @@ class ComposeSSHExtension:
}
def teardown(self, handle: dict[str, str]) -> dict[str, str]:
ssh = SSHConfig.from_env(handle["host"], user=handle.get("ssh_user", self.ssh_user))
ssh_user = handle.get("ssh_user") or self.ssh_user or None
ssh = SSHConfig.from_env(handle["host"], user=ssh_user)
project = handle.get("compose_project")
remote_dir = handle.get("remote_dir")
compose_file = handle.get("compose_file")

View File

@@ -11,19 +11,27 @@ from pathlib import Path
@dataclass
class SSHConfig:
host: str
user: str = "root"
user: str | None = None
key: str | None = None
connect_timeout: int = 15
@property
def destination(self) -> str:
"""SSH destination; omit user when unset so ~/.ssh/config applies."""
if self.user:
return f"{self.user}@{self.host}"
return self.host
@property
def target(self) -> str:
return f"{self.user}@{self.host}"
return self.destination
@classmethod
def from_env(cls, host: str, *, user: str | None = None, key: str | None = None) -> SSHConfig:
env_user = os.environ.get("SANDBOXER_SSH_USER")
return cls(
host=host,
user=user or os.environ.get("SANDBOXER_SSH_USER", "root"),
user=user if user is not None else (env_user or None),
key=key or os.environ.get("SANDBOXER_SSH_KEY"),
)
@@ -39,7 +47,7 @@ class SSHConfig:
]
if self.key:
args += ["-i", self.key]
args.append(self.target)
args.append(self.destination)
return args
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:

13
tests/test_ssh.py Normal file
View File

@@ -0,0 +1,13 @@
"""SSH config behavior."""
from sandboxer.extensions.ssh import SSHConfig
def test_destination_uses_ssh_config_when_user_unset() -> None:
ssh = SSHConfig(host="92.205.130.254")
assert ssh.destination == "92.205.130.254"
def test_destination_includes_explicit_user() -> None:
ssh = SSHConfig(host="92.205.130.254", user="tegwick")
assert ssh.destination == "tegwick@92.205.130.254"