Files
agent-credential-broker/agent_credential_broker/adapters/ssh.py
T
me be87900794 agent-credential-broker: fork of vaultwarden-agent-broker + OpenBao backend
Mechanical rename (package agent_credential_broker; entry points
agent-broker{,-mcp,-plugin}; env AGENT_BROKER_*; paths agent-broker;
agent-browser credential provider 'broker'; MCP server name agent-broker).

New backend 'openbao': stdlib-only HTTP client for OpenBao/Vault KV v2
(approle login or token file/env; metadata-only ref index; lazy re-login
on 401/403; token renewal). bitwarden-cli backend kept as transition.
2026-10-14 13:27:33 -06:00

101 lines
3.5 KiB
Python

"""SSH adapter: broker-managed ssh authentication.
Two credential modes:
* agent.ssh_key_path set -> uses that existing local private key file
(path metadata only -- key bytes never move through the broker)
* otherwise -> password auth via SSH_ASKPASS, where the
password is a 0600 file under the runtime tmpfs that the generated
askpass script cats at prompt time. Never on argv.
The remote command is passed as a single argv element to ssh -- no local
shell interpolation is possible.
"""
from __future__ import annotations
import os
import re
import shutil
from ..config import state_dir
from ..errors import invalid_argument, unsafe_tool_integration
from ..policy import require_host_allowed
from ..items import item_secrets
from .common import (clean_env, run_subprocess, secret_dir, write_askpass,
write_secret_file)
def _known_hosts():
"""Broker-managed known_hosts (TOFU trust stays in the broker's own
state dir instead of silently writing to the user's ~/.ssh)."""
d = state_dir() / "ssh"
d.mkdir(mode=0o700, parents=True, exist_ok=True)
p = d / "known_hosts"
if not p.exists():
p.touch(mode=0o600)
return p
_CMD_MAX = 2000
_DANGEROUS = re.compile(r"[;&|`$<>]|^\s*(sudo\s+)?(rm|dd|mkfs|shutdown|reboot|poweroff)\b")
def validate(item, policy, params, config):
host = params.get("host") or ""
command = params.get("command") or ""
if not command or len(command) > _CMD_MAX:
raise invalid_argument("missing or oversized command")
if _DANGEROUS.search(command):
raise invalid_argument("command rejected by safety policy")
require_host_allowed(policy, host)
def run(item, policy, params, config):
host = params.get("host") or ""
command = params.get("command") or ""
port = int(params.get("port") or 22)
if not command or len(command) > _CMD_MAX:
raise invalid_argument("missing or oversized command")
if _DANGEROUS.search(command):
raise invalid_argument("command rejected by safety policy")
require_host_allowed(policy, host)
ssh = shutil.which("ssh") or "/usr/bin/ssh"
user = policy.ssh_user or item.get("username") or "root"
target = f"{user}@{host}"
argv = [
ssh,
"-o", "BatchMode=no",
"-o", "ConnectTimeout=15",
"-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={_known_hosts()}",
"-o", "NumberOfPasswordPrompts=1",
"-o", "IdentitiesOnly=yes",
"-p", str(port),
target,
command,
]
secrets = item_secrets(item)
key_path = policy.ssh_key_path
if key_path:
if not os.path.isfile(key_path):
raise unsafe_tool_integration("ssh: agent.ssh_key_path does not exist")
argv = argv[:-2] + ["-i", key_path] + argv[-2:]
result = run_subprocess(argv, env=clean_env(), timeout=30, secrets=secrets)
else:
password = item.get("password")
if not password:
raise unsafe_tool_integration("ssh: no key path and no password on item")
with secret_dir() as d:
pwfile = d / "pw"
write_secret_file(pwfile, password)
askpass = write_askpass(d, None, pwfile)
env = clean_env({
"SSH_ASKPASS": str(askpass),
"SSH_ASKPASS_REQUIRE": "force",
"DISPLAY": "broker:0",
})
result = run_subprocess(argv, env=env, timeout=30, secrets=secrets)
result["host"] = host
result["auth"] = "key" if key_path else "password-askpass"
return result