SSH 端口转发与免密速查:-L/-R/-D 的方向、ProxyJump 的写法、authorized_keys 的权限
端口转发配错时最典型的症状是"命令没报错,连不上"。方向记反、参数用了等号形式、或者 key 的权限被 umask 改过,三种情况都不会给你明确的错误。下面把三种转发的方向、免密登录的权限要求、以及我在本机实测到的两个静默失败整理在一起。
三种转发与跳板
| 需求 | 参数 | 方向 | 示例 |
|---|---|---|---|
| 本地端口映射到远端内网 | -L | 本地监听 → 远端发起连接 | ssh -N -L 8080:127.0.0.1:80 user@bastion |
| 远端端口映射回本地 | -R | 远端监听 → 本地发起连接 | ssh -N -R 9000:127.0.0.1:5432 user@bastion |
| 本地 SOCKS 代理 | -D | 本地监听,目标由客户端协议决定 | ssh -N -D 1080 user@bastion |
| 经跳板直连目标 | -J / ProxyJump | 连接链 | ssh -J user@bastion user@10.0.0.5 |
| 只转发不执行命令 | -N | — | 与 -L/-R/-D 搭配 |
| 转发失败即退出 | -o ExitOnForwardFailure=yes | — | 脚本里需要,否则会静默继续 |
-L 的记忆方式是"左边是本地":-L 本地端口:目标主机:目标端口,目标主机是从远端视角解析的。-R 反过来,监听建在远端。-D 只给一个本地端口,具体连哪里由客户端通过 SOCKS 协议告诉 ssh。
实测一条 -L 隧道打通后的样子(容器内自建 sshd,端口 2222):
$ ssh -N -L 18080:127.0.0.1:8080 -p 2222 root@127.0.0.1
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:18080/ok
200
$ ss -tlnp | grep 18080
LISTEN 0 128 127.0.0.1:18080 0.0.0.0:* users:(("ssh",pid=5991,fd=5))
LISTEN 0 128 [::1]:18080 [::]:* users:(("ssh",pid=5991,fd=4))
注意监听地址是 127.0.0.1 和 ::1,只有本机能连。要让同网段其他机器连进来,得在 -L 里显式写绑定地址(-L 0.0.0.0:18080:...),并确认 GatewayPorts 的取值。
免密登录的最小配置
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 # 生成
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host # 或手工追加到 authorized_keys
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys # 目录 700,文件 600
ssh -o BatchMode=yes user@host true # 用 BatchMode 验证,不会被密码提示掩盖
BatchMode=yes 这一步很关键:不加它,key 失败时会退回密码提示,人一输密码就以为配好了,脚本里才发现不通。
三个坑
第一,-o 用等号写转发参数会静默失效。这是我在本机对比出来的:
$ ssh -G -o "LocalForward=8080:127.0.0.1:80" web1 | grep -i localforward
(无输出)
$ ssh -G -o "LocalForward 8080 127.0.0.1:80" web1 | grep -i localforward
localforward 8080 [127.0.0.1]:80
LocalForward 需要"监听规格 + 目标"两个参数,等号形式只能塞一个字符串,ssh 解析后直接忽略,不报错。判断实际生效配置的通用手段是 ssh -G <host>,它把合并后的最终配置打出来;四种转发都能在里面看到原文,比如同时给 -R、-D、ProxyJump 时会输出 remoteforward 9000 [127.0.0.1]:5432、dynamicforward 1080、proxyjump bastion。
第二,authorized_keys 的权限判定包含目录。sshd 的 StrictModes 默认开启,文件或目录对组/其他人可写就拒绝使用该 key。实测三种权限下的结果:
# authorized_keys 0600
login-ok
# authorized_keys 0666
Authentication refused: bad ownership or modes for file /root/.ssh/authorized_keys
# ~/.ssh 改成 0777、文件仍 0600
Authentication refused: bad ownership or modes for directory /root/.ssh
日志里明确写了是 file 还是 directory,按它改就行。常见来源是共享目录里 umask 000、或者用 cp 保留了一个 664 的副本。
第三,-R 默认只绑回环。远端监听的默认 bind_address 是回环地址,要让外部访问需要服务端 GatewayPorts 配合,且在 -R 里显式指定绑定地址。这一点在不同 sshd 版本上表现一致,属于设计而非 bug。
把隧道写成配置而不是长命令
一次性调试用命令行参数没问题,长期使用建议落到 ~/.ssh/config:
Host db-tunnel
HostName bastion.example.com
User ops
IdentityFile ~/.ssh/id_ed25519
LocalForward 15432 10.0.0.12:5432
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
SessionType none
之后 ssh db-tunnel 就等价于那一长串参数,ssh -G db-tunnel 可以核对最终生效值。SessionType none 相当于 -N,ServerAliveInterval 让空闲连接不被中间的 NAT 或防火墙静默断开,ExitOnForwardFailure 保证端口占用时立刻失败而不是假装成功。
隧道断了不会自动重连,这是 ssh 本身的行为。要常驻需要外部工具(如 autossh)或一个 systemd unit 反复拉起;无论哪种,都建议把 ExitOnForwardFailure 打开,否则进程活着但转发没建起来,监控看起来一切正常。
两个常见场景
第一个是"从本机连生产内网数据库"。数据库只监听内网地址、不暴露公网,跳板机可以登录。做法是把本地端口映射到数据库内网地址:
ssh -N -L 15432:10.0.0.12:5432 ops@bastion.example.com
psql -h 127.0.0.1 -p 15432 -U app # 本地 15432 就是内网 5432
这里的目标地址 10.0.0.12 是从跳板机视角解析的,不是从本机。如果跳板机能到但本机不能到,说明方向写对了;如果反过来,就是把 -L 和 -R 记混了。
第二个是"把本地服务临时暴露给远端联调"。本机跑着还没上线的接口,远端同事要调,用 -R 把远端的一个端口映射回本地:
ssh -N -R 18080:127.0.0.1:8000 ops@bastion.example.com
# 远端访问 http://127.0.0.1:18080 即打到本机的 8000
远端监听默认只在跳板机的回环上,所以同事需要在跳板机上再转发一次,或者让管理员开 GatewayPorts。这一点经常被误解成"隧道没建起来",实际上是安全默认值在起作用。
端口被占用时 ssh 会直接报 bind: Address already in use,ExitOnForwardFailure=yes 能让它立刻退出而不是留一个半通的会话。排查占用用 ss -tlnp | grep <port>,比 netstat 快且能看到进程。
小结
三种转发先确定"谁监听、谁发起",再决定用 -L 还是 -R;跳板用 -J,不要手工嵌套两层 ssh。配完用 ssh -G 核对最终配置、用 ss -tlnp 核对监听地址、用 curl 或 nc 打一次真实流量,三步都过才算通。隧道用完要显式关掉,长期挂着的转发会在跳板机上留下监听端口。
免密登录的问题几乎都出在权限位和 StrictModes 上,日志会直接指出是文件还是目录。用 BatchMode=yes 做验收,能避免密码兜底把失败伪装成成功。
数据来源与检索时间
| 来源 | URL | 对应数据 |
|---|---|---|
| man7.org · ssh_config(5) | https://man7.org/linux/man-pages/man5/ssh_config.5.html | LocalForward/RemoteForward/DynamicForward 语义、-R 默认只绑回环、ProxyJump、BatchMode、ExitOnForwardFailure、-o 支持 = 写法 |
| man7.org · sshd(8) | https://man7.org/linux/man-pages/man8/sshd.8.html | ~/.ssh/authorized_keys 与 ~/.ssh 推荐权限、StrictModes 拒绝条件、permitopen/permitlisten 限制 |
| man7.org · sshd(8) | https://man7.org/linux/man-pages/man8/sshd.8.html | sshd -G/-T 打印生效配置、-e 把日志写到 stderr(本次抓日志用的方式) |
检索时间:2026-11-10(Asia/Shanghai)。
核验说明
- 真跑:容器内
ssh-keygen -A与自建sshd -p 2222,authorized_keys在 0600 / 0666、~/.ssh在 0700 / 0777 四种组合下的登录结果与 sshd 原始日志,均为实测输出。 - 真跑:
ssh -N -L 18080:127.0.0.1:8080建立隧道后curl返回 200,ss -tlnp显示127.0.0.1:18080与[::1]:18080两个监听。 - 真跑:
ssh -G对-o LocalForward=...(等号)与-o "LocalForward ... ..."(空格)两种写法的输出差异,等号形式确实没有localforward行。 - 仅依据文档,未在本机运行:
ProxyJump的真实跳板链路未构造(只验证了ssh -G的解析结果);GatewayPorts对-R外部可达性的影响未实测。