security: 机构独立 SSH key + host key 校验(accept-new 防 MITM)
- ensure_org_key: 每机构独立 ed25519 key pair(~/.ssh/org_keys/<org_id>/),防单点泄露 - org 级远程模式自动用机构 key,不允许指定任意 key - StrictHostKeyChecking=no → accept-new,首次记录后严格校验 host key 防 MITM
This commit is contained in:
parent
d35800a100
commit
939fd252ec
@ -50,6 +50,8 @@ from .work_env import (
|
||||
run_remote_sandbox,
|
||||
run_in_work_env,
|
||||
ensure_remote_bwrap,
|
||||
ensure_org_key,
|
||||
get_org_pubkey,
|
||||
)
|
||||
|
||||
__version__ = "3.5.0"
|
||||
|
||||
@ -19,6 +19,7 @@ import shlex
|
||||
import shutil
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger("pipeline.work_env")
|
||||
|
||||
@ -70,8 +71,12 @@ def _ssh_target(env: dict) -> str:
|
||||
|
||||
|
||||
def _ssh_common_args(env: dict) -> list:
|
||||
"""ssh/scp/rsync 共用的 SSH 参数。"""
|
||||
args = ["-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10"]
|
||||
"""ssh/scp/rsync 共用的 SSH 参数。
|
||||
|
||||
StrictHostKeyChecking=accept-new:首次连接记录 host key,之后严格校验,
|
||||
防 MITM(host key 变化会拒绝),替代原来的 =no(完全不校验)。
|
||||
"""
|
||||
args = ["-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"]
|
||||
key = env.get("remote_key_path", "")
|
||||
if key:
|
||||
args += ["-i", key]
|
||||
@ -80,6 +85,48 @@ def _ssh_common_args(env: dict) -> list:
|
||||
return args
|
||||
|
||||
|
||||
# ── 机构独立 SSH key(每机构一把,防单点泄露) ────────────────
|
||||
|
||||
SSH_KEYS_DIR = os.path.expanduser("~/.ssh/org_keys")
|
||||
|
||||
|
||||
def _org_key_dir(org_id: str) -> str:
|
||||
"""机构 key 目录(org_id sanitize 防路径穿越)。"""
|
||||
safe = re.sub(r"[^a-zA-Z0-9_-]", "_", org_id or "")
|
||||
if not safe:
|
||||
safe = "_"
|
||||
return os.path.join(SSH_KEYS_DIR, safe)
|
||||
|
||||
|
||||
def ensure_org_key(org_id: str) -> dict:
|
||||
"""确保机构有独立 SSH key pair(ed25519),没有则生成。
|
||||
|
||||
Returns: {"key_path", "pubkey_path", "pubkey"}
|
||||
"""
|
||||
d = _org_key_dir(org_id)
|
||||
os.makedirs(d, mode=0o700, exist_ok=True)
|
||||
priv = os.path.join(d, "id_ed25519")
|
||||
pub = priv + ".pub"
|
||||
if not (os.path.exists(priv) and os.path.exists(pub)):
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", priv,
|
||||
"-C", "pipeline-org-" + (re.sub(r"[^a-zA-Z0-9_-]", "_", org_id) or "_")],
|
||||
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
os.chmod(priv, 0o600)
|
||||
pubkey = ""
|
||||
if os.path.exists(pub):
|
||||
with open(pub, "r", encoding="utf-8") as f:
|
||||
pubkey = f.read().strip()
|
||||
return {"key_path": priv, "pubkey_path": pub, "pubkey": pubkey}
|
||||
|
||||
|
||||
def get_org_pubkey(org_id: str) -> dict:
|
||||
"""获取机构 public key(供 admin 复制到远程主机 authorized_keys)。"""
|
||||
info = ensure_org_key(org_id)
|
||||
return {"ok": True, "org_id": org_id, "pubkey": info["pubkey"],
|
||||
"key_path": info["key_path"]}
|
||||
|
||||
|
||||
# ── 远程配置安全校验 ───────────────────────────────────────
|
||||
|
||||
_HOST_RE = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$")
|
||||
@ -232,6 +279,11 @@ async def set_work_env(sor, owner_type: str, owner_id: str, mode: str,
|
||||
return {"ok": False, "error": "mode 必须是 local 或 remote"}
|
||||
|
||||
remote_config = remote_config or {}
|
||||
if owner_type == "org" and mode == "remote":
|
||||
# 机构级远程:自动使用机构独立 key(每机构一把,防单点泄露),
|
||||
# 不允许 admin 指定任意 key
|
||||
_key_info = ensure_org_key(owner_id)
|
||||
remote_config["remote_key_path"] = _key_info["key_path"]
|
||||
if mode == "remote":
|
||||
err = _validate_remote(remote_config)
|
||||
if err:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user