Skip to content

SSH:配置双端 SSH 与受限跳板 ​

本篇以SSH 双端身份与受限跳板为例;前置是客户端配置、两端主机公钥与用户授权。源码与输入与下文配置共用同一份文件,示例未执行。

前置、依赖与输入 ​

SSH完整示例是离线证据处理器,示例未执行。需要Python3.11以上标准库,第三方依赖为零,不需要服务、管理员或网络。输入JSON只含虚构记录,程序不调用系统命令、不建立连接、不修改主机。完整文件位于examples/m5-next-ssh,必须一起阅读README、lab.py和case.json。

完整源码与配置 ​

下面包括完整源码与全部输入;这不是产品配置验证器,也不执行协议、密码学或系统策略。判断只依据输入声明,不能用输出证明真实环境。

python
"""Offline teaching model. No network, subprocesses, or system mutations."""
import json
from pathlib import Path
import sys


def require(value, schema):
    if type(value) is not dict or set(value) != set(schema):
        raise ValueError("unexpected or missing fields")
    for key, expected in schema.items():
        if type(value[key]) is not expected:
            raise ValueError("wrong field type: " + key)


def decide(row):
    require(row, {"name": str, "expected_host": str, "observed_host": str, "user_key_authorized": bool, "jump_trusted": bool})
    if not row["expected_host"] or not row["observed_host"]:
        raise ValueError("empty host identity")
    if row["expected_host"] != row["observed_host"]:
        return "STOP_HOST_MISMATCH"
    if not row["jump_trusted"]:
        return "STOP_JUMP_UNTRUSTED"
    if not row["user_key_authorized"]:
        return "DENY_USER"
    return "IDENTITY_PLAN_ACCEPTED"


def main(args):
    if len(args) != 1:
        print("usage: python lab.py case.json", file=sys.stderr)
        return 2
    try:
        source = Path(args[0]).read_text(encoding="utf-8")
        data = json.loads(source)
        if type(data) is not list or not data:
            raise ValueError("nonempty list required")
        # Validate every row before publishing any decisions.
        results = [(row, decide(row)) for row in data]
        for row, result in results:
            print(row["name"] + ": " + result)
        return 0
    except (OSError, ValueError, KeyError, TypeError, OverflowError):
        print("invalid input", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
json
[
  {
    "name": "normal",
    "expected_host": "fictional-A",
    "observed_host": "fictional-A",
    "user_key_authorized": true,
    "jump_trusted": true
  },
  {
    "name": "changed-host",
    "expected_host": "fictional-A",
    "observed_host": "fictional-B",
    "user_key_authorized": true,
    "jump_trusted": true
  },
  {
    "name": "unknown-user",
    "expected_host": "fictional-A",
    "observed_host": "fictional-A",
    "user_key_authorized": false,
    "jump_trusted": true
  }
]

读者自跑、成功和失败 ​

SSH离线推演先复制三个文件到独立目录。Windows PowerShell使用python,Linux Bash使用python3调用lab.py case.json;命令在README完整列出。正常输入输出逐行判定并以0结束;缺字段、非法类型或JSON损坏输出invalid input并以2结束。预期拒绝是业务判定,不等同输入错误,仍以0结束。

恢复、清理与边界 ​

恢复时从仓库还原case.json,重新核对变更前后字段。程序不写结果文件,不产生系统资源,不需要关闭端口或停止服务。清理只删除读者自己复制的练习目录;不要删除仓库示例。修改输入不会改变真实授权、证书或路由。

综合练习与分开的解答 ​

练习:先不看参考答案,解释输入中每个正常和失败判定,修改一项条件让一个判定改变,再写出该变化仍不能证明的现实条件。

参考解答

normal的主机标签一致、跳板可信且用户获准,因此得到IDENTITY_PLAN_ACCEPTED。changed-host即使用户公钥获准也必须先STOP_HOST_MISMATCH。unknown-user的主机可信不等于用户有权,输出DENY_USER。所有标签均为虚构声明,不是真实密码学结果。

具体变体:只把 changed-host 的 observed_host 改为 fictional-A,其余字段不变,将由 STOP_HOST_MISMATCH 变为 IDENTITY_PLAN_ACCEPTED。比较标签并没有验证真实公钥,真实主机绑定仍须独立核验,不能靠修改输入消除告警。

产品配置与操作契约 ​

以下为与离线模型分开的完整产品配置,示例未执行。产品二进制、真实密钥与账户均需读者按前置另行提供;静态解析不等于产品验证。

text
# OpenSSH 9.x; Linux isolated lab paths; replace documentation addresses.
Host lab-jump
    HostName 192.0.2.10
    Port 2222
    User student
    IdentityFile /srv/ssh-lab/secrets/jump_ed25519
    IdentitiesOnly yes
    StrictHostKeyChecking yes
    UserKnownHostsFile /srv/ssh-lab/known_hosts
    ForwardAgent no
    BatchMode yes

Host lab-app
    HostName 192.0.2.20
    Port 2222
    User student
    ProxyJump lab-jump
    IdentityFile /srv/ssh-lab/secrets/app_ed25519
    IdentitiesOnly yes
    StrictHostKeyChecking yes
    UserKnownHostsFile /srv/ssh-lab/known_hosts
    ForwardAgent no
    BatchMode yes

Host *
    ConnectTimeout 10
    ServerAliveInterval 30
    ServerAliveCountMax 2
text
# Standalone Linux sshd configuration; never merge over production configuration.
Port 2222
ListenAddress 192.0.2.10
HostKey /srv/ssh-lab/secrets/host_ed25519
PidFile /srv/ssh-lab/sshd.pid
AuthorizedKeysFile /srv/ssh-lab/authorized_keys
AllowUsers student
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PermitEmptyPasswords no
UsePAM no
AllowAgentForwarding no
AllowTcpForwarding local
AllowStreamLocalForwarding no
PermitOpen 192.0.2.20:2222
GatewayPorts no
X11Forwarding no
PermitTunnel no
PermitTTY no
MaxAuthTries 3
LogLevel VERBOSE
ForceCommand /usr/bin/false
text
# Standalone Linux sshd configuration for the target host.
Port 2222
ListenAddress 192.0.2.20
HostKey /srv/ssh-lab/secrets/host_ed25519
PidFile /srv/ssh-lab/sshd.pid
AuthorizedKeysFile /srv/ssh-lab/authorized_keys
AllowUsers student
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PermitEmptyPasswords no
UsePAM no
AllowAgentForwarding no
AllowTcpForwarding no
AllowStreamLocalForwarding no
GatewayPorts no
X11Forwarding no
PermitTunnel no
PermitTTY no
MaxAuthTries 3
LogLevel VERBOSE
ForceCommand /usr/bin/id
text
# Not a verified trust store. Reader must supply actual, independently verified keys.
# Required entries: [192.0.2.10]:2222 and [192.0.2.20]:2222.
# Format: bracketed-host:port key-type base64-public-key
# No fabricated key is supplied. Empty trust store must cause strict refusal.

完整OpenSSH双端配置(未执行) ​

client.conf、sshd-jump.conf、sshd-app.conf为OpenSSH9.x/Linux隔离实验的完整独立配置,不覆盖系统sshd_config。两台主机需预先由管理者提供实际分配的IP、有效student账户和/usr/bin/false、/usr/bin/id;192.0.2.10/20是文档地址,必须替换后同步PermitOpen与客户端。Windows原生sshd的服务/PAM/账户配置不同,不能套用此服务端文件。

每台服务端在/srv/ssh-lab/secrets/host_ed25519放置其各自真实主机私钥,只有管理员可读;/srv/ssh-lab/authorized_keys由管理员写入该端允许用户的真实公钥,student不能修改。目录不可被student写入,父目录允许必要遍历。客户端两个IdentityFile仅放对应用户私钥,不复制到跳板。各私钥权限、账户shell和OpenSSH服务运行目录由管理员依据发行版手册准备,本项目不创建账户或私钥。

known_hosts.template仅列格式,不伪造公钥。读者从两台主机的可信管理控制台获得真实主机公钥及指纹,与独立资产渠道确认一致,再填写/srv/ssh-lab/known_hosts对应非默认端口条目。ssh-keyscan结果单独不能建立信任。未填写时严格校验必须拒绝。

只有读者在另行批准的隔离环境已准备两端sshd时,客户端命令才有意义:

bash
ssh -F /srv/ssh-lab/client.conf lab-app

预期跳板只允许到目标2222的TCP转发,目标固定执行/usr/bin/id,拒绝任意shell、PTY和转发;输出student账户身份。改目标主机公钥预期在认证前拒绝;删目标authorized_keys中对应公钥预期用户认证拒绝。两种失败不可相互替代。BatchMode避免交互等待,ForwardAgent保持关闭。

恢复用该实验上一份已核验配置与主机绑定,密钥泄露时不得回退到泄露密钥。清理由实验管理员撤销本次两端授权、结束仅本次独立sshd并删除实验私钥副本;不清空系统known_hosts或停生产sshd。

两端还显式设置 AllowStreamLocalForwarding no:Unix-domain socket 转发与 AllowTcpForwarding 独立,ForceCommand 也不是转发控制。跳板只保留指向批准目标的本地 TCP 转发;目标关闭两种转发。仅设固定命令不能得到相同约束。OpenSSH 指令说明

完整项目说明与依赖声明 ​

以下同源文件完整列出读者前置、自跑命令、正常/失败预期和清理;正文只供阅读,示例未执行。

text
# Python >=3.11; standard library only; no third-party dependencies.
完整 README(与项目文件一致)
markdown
# ssh 离线证据推演

示例未执行。前半的 lab.py 只比较预期/观测主机标签、跳板信任与用户公钥授权声明;后半才是需要独立主机与密钥的 OpenSSH 配置。离线判定不建立 SSH 会话,也不验证真实签名。

## 文件与前置

离线部分需要 Python 3.11 以上和标准库,无服务或管理员权限。先从 [case.json](case.json) 核对 expected_host、observed_host、jump_trusted 与 user_key_authorized,并按 [lab.py](lab.py) 区分主机失配与用户未授权。真实主机公钥须来自独立可信渠道;OpenSSH 账户、路径及双端配置的前置见后半节。macOS 未做原生运行验收。

## 读者自跑

先复制全部文件到自己的隔离练习目录并进入该目录。以下仅是读者步骤,本轮未执行。

Windows PowerShell:

```powershell
python .\lab.py .\case.json
```

Linux Bash(macOS若已有Python3也使用此形式):

```bash
python3 ./lab.py ./case.json
```

预期标准输出如下,进程退出0;明确拒绝是正常决策而不是输入错误。

```text
normal: IDENTITY_PLAN_ACCEPTED
changed-host: STOP_HOST_MISMATCH
unknown-user: DENY_USER
```

## 失败路径

把case.json复制为broken.json,再人工删除第一条记录的name字段。使用同一命令把参数换成broken.json,预期标准错误invalid input、退出2,不发布部分判定。破损JSON、非法字段类型、未知字段、空列表或不存在的文件同样退出2。不打印异常原文,防止输入内容意外进入日志。程序严格匹配固定教学字段,不接受任意扩展。

## 恢复与清理

从原仓库重新复制case.json恢复,或撤销练习副本编辑。程序没有写文件、创建用户或启动连接,无需服务清理;只删除自己复制的目录和broken.json,不删除仓库原文件。

## 解释边界

fictional-A是可比较标签而非真实指纹,程序不解析密钥或验证签名。主机信任来自独立核验,不能把网络采集标签直接填成expected_host。

输入由读者提供,不具有独立真实性。需要真实运行验证时,另行取得授权并采集环境证据,不能用本程序作产品验收工具。

## 完整OpenSSH双端配置(未执行)

client.conf、sshd-jump.conf、sshd-app.conf为OpenSSH9.x/Linux隔离实验的完整独立配置,不覆盖系统sshd_config。两台主机需预先由管理者提供实际分配的IP、有效student账户和/usr/bin/false、/usr/bin/id;192.0.2.10/20是文档地址,必须替换后同步PermitOpen与客户端。Windows原生sshd的服务/PAM/账户配置不同,不能套用此服务端文件。

每台服务端在/srv/ssh-lab/secrets/host_ed25519放置其各自真实主机私钥,只有管理员可读;/srv/ssh-lab/authorized_keys由管理员写入该端允许用户的真实公钥,student不能修改。目录不可被student写入,父目录允许必要遍历。客户端两个IdentityFile仅放对应用户私钥,不复制到跳板。各私钥权限、账户shell和OpenSSH服务运行目录由管理员依据发行版手册准备,本项目不创建账户或私钥。

known_hosts.template仅列格式,不伪造公钥。读者从两台主机的可信管理控制台获得真实主机公钥及指纹,与独立资产渠道确认一致,再填写/srv/ssh-lab/known_hosts对应非默认端口条目。ssh-keyscan结果单独不能建立信任。未填写时严格校验必须拒绝。

只有读者在另行批准的隔离环境已准备两端sshd时,客户端命令才有意义:

```bash
ssh -F /srv/ssh-lab/client.conf lab-app
```

预期跳板只允许到目标2222的TCP转发,目标固定执行/usr/bin/id,拒绝任意shell、PTY和转发;输出student账户身份。改目标主机公钥预期在认证前拒绝;删目标authorized_keys中对应公钥预期用户认证拒绝。两种失败不可相互替代。BatchMode避免交互等待,ForwardAgent保持关闭。

恢复用该实验上一份已核验配置与主机绑定,密钥泄露时不得回退到泄露密钥。清理由实验管理员撤销本次两端授权、结束仅本次独立sshd并删除实验私钥副本;不清空系统known_hosts或停生产sshd。本批从未启动这些进程。

两端还显式设置 `AllowStreamLocalForwarding no`:Unix-domain socket 转发与 `AllowTcpForwarding` 独立,`ForceCommand` 也不是转发控制。跳板只保留指向批准目标的本地 TCP 转发;目标关闭两种转发。仅设固定命令不能得到相同约束。[OpenSSH 指令说明](https://man.openbsd.org/sshd_config#AllowStreamLocalForwarding)

继续阅读