feat: fix SSH key mounting with multi-key support and unique filenames
SSH key mounting was broken because: 1. Each selected key was mounted to a separate source dir but all targeted the same ~/.ssh path in the container, causing Docker Compose's last-mount-wins behavior 2. All keys were named id_ed25519, so they'd overwrite each other Changes: - ssh_keys.py: add key_filename param to prepare_ssh_key_files for unique key names; add write_ssh_config for combined multi-key config - tool_instances.py: collect all selected keys into a single ~/.ssh mount with sanitized unique filenames (id_ed25519_<name>); generate combined SSH config with all IdentityFile entries - tests: add os.makedirs mock for SSH permission tests Quality gates: pytest (19 passed, 1 skipped)
This commit is contained in:
@@ -1611,38 +1611,14 @@ async def start_instance(
|
||||
|
||||
# Mount selected SSH keys into container home dir
|
||||
if instance.ssh_key_ids:
|
||||
from src.services.ssh_keys import write_ssh_config, _sanitize_filename
|
||||
|
||||
# Collect all valid keys first
|
||||
ssh_keys_to_mount = []
|
||||
for key_id in instance.ssh_key_ids:
|
||||
ssh_key = await session.get(SSHKey, uuid.UUID(key_id))
|
||||
if ssh_key and ssh_key.user_id == user_id:
|
||||
try:
|
||||
ssh_dir = prepare_ssh_key_files(
|
||||
instance_dir,
|
||||
ssh_key,
|
||||
subdir=f"mounts/ssh/{key_id}/.ssh",
|
||||
uid=container_uid,
|
||||
gid=container_gid,
|
||||
)
|
||||
ssh_target = os.path.join(home_dir, ".ssh")
|
||||
extra_volumes.append(
|
||||
{
|
||||
"source": ssh_dir,
|
||||
"target": ssh_target,
|
||||
"type": "bind",
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
"Mounted SSH key %s for instance %s to %s",
|
||||
ssh_key.name,
|
||||
instance.id,
|
||||
ssh_target,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to prepare SSH key %s for instance %s: %s",
|
||||
key_id,
|
||||
instance.id,
|
||||
exc,
|
||||
)
|
||||
ssh_keys_to_mount.append(ssh_key)
|
||||
else:
|
||||
logger.warning(
|
||||
"SSH key %s not found or not authorized for user %s",
|
||||
@@ -1650,6 +1626,79 @@ async def start_instance(
|
||||
user_id,
|
||||
)
|
||||
|
||||
if ssh_keys_to_mount:
|
||||
# Use a single shared .ssh directory so all keys are visible
|
||||
ssh_dir = os.path.join(instance_dir, "mounts", "ssh", ".ssh")
|
||||
os.makedirs(ssh_dir, exist_ok=True)
|
||||
|
||||
key_filenames = []
|
||||
for ssh_key in ssh_keys_to_mount:
|
||||
# Use sanitized key name as filename prefix to avoid collisions
|
||||
key_name = _sanitize_filename(ssh_key.name)
|
||||
# If multiple keys have the same name, append a short hash
|
||||
base_filename = f"id_ed25519_{key_name}"
|
||||
filename = base_filename
|
||||
counter = 1
|
||||
while filename in key_filenames:
|
||||
filename = f"{base_filename}_{counter}"
|
||||
counter += 1
|
||||
key_filenames.append(filename)
|
||||
|
||||
try:
|
||||
prepare_ssh_key_files(
|
||||
instance_dir,
|
||||
ssh_key,
|
||||
subdir="mounts/ssh/.ssh",
|
||||
uid=container_uid,
|
||||
gid=container_gid,
|
||||
key_filename=filename,
|
||||
write_config=False,
|
||||
)
|
||||
logger.debug(
|
||||
"Prepared SSH key %s as %s for instance %s",
|
||||
ssh_key.name,
|
||||
filename,
|
||||
instance.id,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to prepare SSH key %s for instance %s: %s",
|
||||
ssh_key.id,
|
||||
instance.id,
|
||||
exc,
|
||||
)
|
||||
|
||||
# Write combined SSH config with all keys
|
||||
try:
|
||||
write_ssh_config(
|
||||
ssh_dir,
|
||||
key_filenames,
|
||||
uid=container_uid,
|
||||
gid=container_gid,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to write SSH config for instance %s: %s",
|
||||
instance.id,
|
||||
exc,
|
||||
)
|
||||
|
||||
# Mount the single .ssh directory into container home
|
||||
ssh_target = os.path.join(home_dir, ".ssh")
|
||||
extra_volumes.append(
|
||||
{
|
||||
"source": ssh_dir,
|
||||
"target": ssh_target,
|
||||
"type": "bind",
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
"Mounted %d SSH key(s) for instance %s to %s",
|
||||
len(ssh_keys_to_mount),
|
||||
instance.id,
|
||||
ssh_target,
|
||||
)
|
||||
|
||||
# ── MANIFEST-BASED FLOW ──────────────────────────────────────
|
||||
resolved_manifest = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user