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.
This commit is contained in:
commit
be87900794
35 files changed
+5003
No files matched your search
@@ -0,0 +1,100 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user