KernelRanch
返回文章列表

SSH 端口转发与免密速查:-L/-R/-D 的方向、ProxyJump 的写法、authorized_keys 的权限

2026-11-10 #运维

端口转发配错时最典型的症状是"命令没报错,连不上"。方向记反、参数用了等号形式、或者 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.htmlLocalForward/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.htmlsshd -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 外部可达性的影响未实测。