generated from coulomb/repo-seed
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
13
tests/test_ssh.py
Normal 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"
|
||||
Reference in New Issue
Block a user