Linux ssh 笔记
整理记录 SSH 的常见概念、认证方式与典型用法。
基本使用(1)
ssh 最基本的用途就是登录远程服务器
1 | ssh user@hostname |
其中:
user是登录用户名hostname是主机名,它可以是域名,也可以是某个具体的 IP 地址或局域网内部的主机名。
可以缺省用户名,此时将使用本地用户名作为远程服务器的登录用户名。
1 | ssh hostname |
用户名也可以通过 -l 参数指定,这样用户名和主机名就不用写在一起了,在脚本中可能更方便
1 | ssh -l username hostname |
ssh 默认连接远程服务器的 22 端口。使用 -p 参数也可以指定其他端口;前提是远程 SSH 服务端确实监听在该端口上。
1 | ssh -p 8821 foo.com |
ssh 在连接到远程服务器后会进行验证:如果第一次通过 ssh 连接某一台服务器,
命令行会显示一段文字,表示不认识这台机器,提醒用户确认是否需要连接
1 | The authenticity of host 'foo.com (192.168.121.111)' can't be established. |
这段文字通常表示:客户端本地还没有记录该主机的主机密钥,因此需要用户确认是否信任该主机。
如果 known_hosts 中已经存在该主机的记录,但当前服务端提供的主机密钥与记录不匹配,OpenSSH 通常会直接给出明显的安全警告,而不是进入这种首次确认流程。
所谓“服务器指纹”,是服务端主机公钥(host key)的哈希值。客户端可以利用它校验自己连接的是否仍然是同一台主机。下面的命令可以查看某个公钥对应的指纹:
1 | ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub |
ssh 会将已接受过的主机公钥或其哈希标识记录在 ~/.ssh/known_hosts 文件中。
之后再次连接时,客户端会拿当前服务端提供的主机公钥与该文件中的记录进行比对:
- 如果是首次见到的主机,通常会提示用户确认;
- 如果公钥匹配,则继续连接;
- 如果公钥不匹配,则会触发主机密钥变更警告,这通常意味着服务器重装、更换了主机密钥,或者存在中间人攻击风险。
我们可以使用下面的命令检查指定的主机名是否在 known_hosts 中,或者将指定主机名从 known_hosts 中移除
1 | # find |
在主机密钥校验通过后,ssh 会进入用户认证阶段。服务端可能允许多种认证方式,例如密码认证、公钥认证、键盘交互认证等;具体是否要求输入密码,取决于服务端配置以及客户端可用的认证材料。
有时我们只需要临时登录并执行一条简单命令,可以把命令直接加在后面,在登录之后会自动执行这条命令并输出到本地,然后自动退出 ssh 登录,例如
1 | ssh user@hostname cat /etc/hosts |
密钥
非对称加密算法
SSH 支持多种公钥算法,用于公钥认证、主机密钥以及签名验证:
- RSA:兼容性最好,历史最久,当前仍被广泛支持。
- ECDSA:基于椭圆曲线,常见于现代 OpenSSH 环境。
- Ed25519:通常被认为是在现代环境中兼顾安全性、性能与密钥体积的优选方案之一。
除此之外,还有一些已经不建议在新环境中继续使用的旧选项,例如 SSH 协议版本 1 相关能力以及 DSA(ssh-dss)。
关于密钥的位数:
- RSA:建议至少使用 2048 位;在较长生命周期的场景中,3072 位或 4096 位更常见。
- ECDSA:只支持
256、384、521三种曲线参数。 - Ed25519:参数固定,不需要像 RSA 那样手动选择位数。
在新的个人或团队环境中,通常优先选择
Ed25519;如果需要兼容旧系统,再考虑RSA。ECDSA 也可使用,但并不是“位数越长越值得默认选择”,应结合兼容性与现网要求决定。
密钥文件
密钥主要用于证明客户端身份。对于同一台客户端机器上的同一用户,通常会维护一组或多组密钥对。
用户必须妥善保管私钥,并将对应公钥部署到需要登录的远程服务器上,见下文。
密钥通常存放在 ~/.ssh 目录下,下面是这些文件的默认名称
id_rsa,id_rsa.pub:用于 SSH 协议版本2 的 RSA 私钥和公钥。id_ecdsa,id_ecdsa.pub:ECDSA 私钥和公钥。id_ed25519,id_ed25519.pub:ED25519 私钥和公钥。identity,identity.pub:历史遗留名称,对现代 OpenSSH 基本没有实际意义。
OpenSSH 通常要求私钥文件不能对其它用户可读或可写,否则可能拒绝使用。常见做法是将私钥设为
600,公钥设为644。
私钥文件和公钥文件都是文本文件,私钥例如
1 | -----BEGIN OPENSSH PRIVATE KEY----- |
不同算法的安全性不能只通过“文件长度”直接比较。一般来说,Ed25519 和 ECDSA 的密钥材料更短,但这并不代表安全性更低。
公钥例如
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCmP1rbqoMBBO9rBNSfRYZyyRUlWeF1pldu3bW8MMfXf/oR78CEN4MT643ZV994qlukZJHjja3cE9X23mUley78DMUaSCIDF+KuBlZYNv1Iaok6MMbUMNfXXESms5bjAT76kpx35IGvbSyOnpgmXMeMcxGbRtgbe448mZCESh8/+mUciznqZiHjhC8oZWJOrvJyx/TYR/wfoNz1MxfafBf0HEUSShxU6HjfQHlt5+rA++mzjOtlTEcqxWIXKDvDy3iVn885kwpUQazQ8D2rJZBmycggRI4TGw6iZYyp5YZ9akXnW2qmT71GNxI6Xt0SUOQIlHHD/9U1yZhebAbkCaPzHbiG1XII78PVxOUcSyo7cp5fVmINwgjEuq0oCbwSdfGPISl32NKeBzGDSLz/qwb9YQXGZRv5FDWP7TZkO78r8+RGKDDWHP8F7XuE3sPrObkCTkMim1jxWdIdXBs/JbG/MJQaplseUKGTKlmeJ5x9SvsmlaE09i2ST7dIAnlrtK0= your_email@domain.com |
公钥内容只有一行,比私钥长度小很多,在公钥文件的开头会表明当前算法的类型,在文件末尾的空格之后会有一个注释,标记公钥所对应的用户名和主机名,这个注释并不是密钥的一部分,只是便于区分不同的公钥,我们可以直接修改注释,这不会对密钥对产生影响。
密钥生成
OpenSSH 提供的 ssh-keygen 可以用于生成密钥。当然,也可以用其他兼容工具生成,但最常见的仍然是直接使用 ssh-keygen。
1 | ssh-keygen |
ssh-keygen 需要我们确认或修改几个信息:
- 保存的路径和文件名,使用默认的路径和文件名即可,还可以通过
-f选项指定,见下文 - 私钥保护口令(passphrase)。如果设置了口令,客户端在使用私钥时通常需要先解锁该私钥;也可以借助
ssh-agent缓存解锁状态。是否留空取决于你的安全要求
最后 ssh-keygen 会生成一对密钥对,将其存储到指定位置,然后显示公钥的指纹和一个基于公钥生成的图像,
用图像来判断比使用字符串形式的指纹对人类更加友好
1 | The key fingerprint is: |
ssh-keygen 支持很多选项,下面介绍几个常用选项。
-t 选项可以指定密钥类型。现代 OpenSSH 中常直接显式指定,例如:
1 | ssh-keygen -t rsa |
对于位数可变的算法,-b 选项可以指定密钥的二进制位数
1 | ssh-keygen -t rsa -b 4096 |
对于位数可变的算法,-b 必须使用该算法支持的合法参数。对于 Ed25519 这类固定参数算法,-b 不适用。
-C 选项可以为公钥文件指定注释,格式为 username@host,默认是当前用户名和当前主机名
1 | ssh-keygen -C "your_email@domain.com" |
-f 参数可以指定生成的私钥文件(否则使用默认名称,并存放在 ~/.ssh/ 目录下)
1 | ssh-keygen -f mykey |
这个命令会在当前目录下生成私钥文件 mykey 和公钥文件 mykey.pub。
-N 参数可以指定私钥的保护密码(passphrase)
1 | ssh-keygen -N secretword |
可以使用下面的命令根据已有私钥导出对应公钥:
1 | ssh-keygen -y -f key_file_name # 输出到标准输出流 |
例如,显式生成一把 RSA 密钥:
1 | ssh-keygen -t rsa -C "your_email@domain.com" -f key_file_name -N '' |
基本使用(2)
在可控环境中,通常更推荐使用公钥认证而不是纯密码认证。大致流程如下:
- 预备步骤,客户端通过 ssh-keygen 生成自己的公钥和私钥,手动将客户端的公钥放入远程服务器中的指定位置。
- 第一步,客户端向服务器发起 SSH 连接,并协商协议参数。
- 第二步,服务端确认该用户可接受公钥认证后,向客户端发起认证挑战。
- 第三步,客户端使用私钥对相关认证数据进行签名,并把签名发送给服务端。
- 第四步,服务端使用已保存的公钥验证签名;验证通过则认证成功。
密钥对的生成在上面已经介绍,下面关注将公钥上传到服务器中。
用户公钥保存在服务器的 ~/.ssh/authorized_keys 文件中,把公钥添加到这个文件之中就是把公钥上传到服务器了。authorized_keys 是纯文本文件,其中可以包含多个公钥,每个公钥占一行,把新的公钥追加进去即可。
如果 authorized_keys 文件不存在,也可以直接手动创建。
OpenSSH 会检查
~/.ssh目录和authorized_keys的权限。常见可用设置是:~/.ssh为700,authorized_keys为600;在部分环境中644也可能可用,但更稳妥的是收紧为仅账户本人可写。
用户可以先用密码登录远程服务器,再手动编辑该文件;也可以直接在本机执行下面的命令:
1 | cat ~/.ssh/id_rsa.pub | ssh user@host "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" |
注意将这里的文件替换为需要上传的密钥文件,需要确保服务器上的 authorized_keys 没有格式错误,并且文件权限正确。
配置好公钥后,可以用 -i 指定私钥文件进行认证:
1 | ssh -i /path/to/private_key user@hostname |
如果省略 -i,ssh 客户端通常会尝试若干默认身份文件,例如:
~/.ssh/id_rsa~/.ssh/id_ecdsa~/.ssh/id_ed25519~/.ssh/id_dsa(旧算法,现代环境通常不再使用)
我们还可以使用 ~/.ssh/config 文件进行更详细的配置,见下文。
配置
客户端配置
ssh 客户端通常需要关注如下两个配置文件:
~/.ssh/known_hosts:记录已接受的远程主机公钥标识。部分场景下还可能出现known_hosts.old之类的备份文件,但不应把它视为所有环境中的固定行为;~/.ssh/config:用户级配置文件,与之相对的是全局配置文件/etc/ssh/ssh_config,用户级的优先级更高。
config 配置文件由一系列 Host 块组成,每个块包含一组针对特定主机或一组主机的配置选项。
当多个 Host 块都匹配某个目标时,同一配置项通常以“首次取得的值”为准,因此一般把更具体的规则放在前面、通用规则放在后面。例如:
1 | Host myserver1.example.com |
解释一下这里的选项:
Host <pattern>:匹配主机别名或目标模式,可以是具体主机名、别名,也可以包含通配符HostName:实际要连接的主机名User:登录用户名Port:端口号(默认是22)。IdentityFile:用于身份认证的私钥文件路径;若未指定,客户端会尝试默认身份文件以及 agent 中已加载的身份ServerAliveInterval:客户端定期向服务端发送保活消息的间隔秒数ServerAliveCountMax:连续多少次保活无响应后,客户端判定连接失效
服务端配置
ssh 服务端通常需要关注如下两个配置文件:
~/.ssh/authorized_keys:记录允许通过公钥认证登录该账户的客户端公钥;/etc/ssh/sshd_config:ssh 服务端(sshd 服务)的全局配置文件。
修改服务器上的 /etc/ssh/sshd_config 可以控制 sshd 的监听参数以及用户认证行为,例如
1 | # 设置 sshd 的监听端口(还需要同步调整防火墙等网络策略) |
修改后通常需要重新加载或重启 sshd 服务才能生效,具体取决于发行版和所采用的服务管理方式。
除了配置文件,ssh 服务端在 /etc/ssh/ 目录中还存储了所有的服务端密钥对,例如 /etc/ssh/ssh_host_ecdsa_key,
这些服务端密钥用于向客户端证明“自己就是这台主机”。如果系统重装、重新初始化 SSH 服务,或显式重建主机密钥,客户端侧保存的主机指纹就会发生不匹配,需要重新核验并更新记录。对于需要保持主机身份连续性的服务器,确实应提前备份这些主机密钥。
在自建 Git 服务器中,git 账户通常不应获得常规交互式 shell。常见做法是把该账户的登录 shell 设为 git-shell:
1 | git:x:1003:1003:,,,:/home/git:/usr/bin/git-shell |
如果直接禁止该账户通过 SSH 登录,也可能连带影响基于 SSH 的 Git 仓库访问,因此要根据部署方式区分“禁止交互式 shell”和“禁止 SSH 连接”这两件事。
文件权限小结
整理一下 Linux 下常见的 .ssh/ 权限建议:
~/.ssh/目录:通常设为700- 私钥:通常设为
600,400也常见 - 公钥:通常设为
644 authorized_keys:建议设为600config、known_hosts:通常设为600或644,关键是不能让权限宽松到被 OpenSSH 视为不安全
进阶使用
端口常识
下面是不同端口号段的含义:
- 0-1023: 知名端口,预留给常见的服务和协议。
- 1024-49151: 注册端口,一般分配给用户进程或特定服务。
- 49152-65535: 动态端口或私有端口,通常用于临时目的,如客户端程序向服务器发起连接。
知名端口例如:
- HTTP:80
- HTTPS:443
- FTP:21
- SSH:22
- DNS:53
可以使用下面的命令查看当前是否有进程在某个端口上处于监听状态:
1 | # Linux |
如果想停止该监听,应该定位对应的进程并终止它。对于 SSH 场景,更准确地说是停止 sshd 进程或调整其监听配置,而不是“关闭端口本身”。
端口转发
ssh 除了登录服务器,还有一大用途:建立加密隧道,也就是端口转发(port forwarding)。
常见用途包括:
- 通过 SSH 通道承载原本未加密的应用层流量;
- 借助可达的 SSH 主机作为跳板,访问原本不可直接建立连接的目标服务。
ssh 提供三种形式的端口转发:
- 本地转发(
-L选项) - 远程转发(
-R选项) - 动态转发(
-D选项)
在下文中还可以加上:
-N选项,它表示这个 SSH 会话只用于建立转发,不执行远程命令。-f选项,它表示 ssh 连接在后台进行。
实践发现:
- 如果两个选项都不加,那么 SSH 会在建立转发的同时打开一个远程 shell,并且 shell 退出后隧道自动关闭;
- 如果加上
-N选项,那么不会打开远程 shell;只要这个 ssh 进程结束,隧道就会关闭; - 如果加上
-N -f选项,ssh 会在认证成功后转入后台运行,隧道会持续到该后台进程退出; -f一般需要配合不会占用前台会话的用法一起使用,常见就是和-N搭配。
在下面的各种转发命令中都省略了关于这些选项的讨论。
如果需要经常进行端口转发,可以把转发直接写入配置文件中,这里不作讨论。
“代理”和“SSH 转发”有交集,但不是完全等价的概念。SSH 的本地/远程/动态转发,更准确地说是在一条已建立的 SSH 连接上承载额外的 TCP 流量,并在本地或远端暴露一个访问入口。
从直观理解的角度看,不管是代理还是转发,都可以把它们想成通信路径中额外引入了一个“中间层”,而且这个中间层一定是通信某一方主动引入的。这样理解有助于把握“为什么多了一跳之后,访问路径和可见性会发生变化”。
不过这只是帮助理解的类比,并不是严格定义。严格来说,正向代理、反向代理、SSH 本地转发、远程转发、动态转发,工作层次和使用场景并不完全相同,不能简单互相等同。
本地转发(Local port forwarding)
本地转发是在本地计算机创建一个监听 socket,并将连接到该本地端口的 TCP 连接通过 SSH 通道转发到远端某个主机和端口。
在这种情况下,SSH 服务器充当跳板,用于连接本地主机原本无法直接访问的目标服务。
1 | 场景: |
本地转发的命令格式为(在 A 上执行命令)
1 | ssh -L A-tunnel-port:C-host:C-port B-host |
其中:
A-tunnel-port:主机 A 本地监听的端口,作为转发入口C-host:主机 C,可以是 C 的内网 ip,只要 B 可以访问即可C-port:主机 C 上目标服务的端口;如果目标是 SSH 服务,通常就是 22 端口B-host:跳板机 B
例如(在 A 上执行命令)
1 | ssh -L 22022:C-host:22 user@B-host |
建立 SSH 隧道后,主机 A 如果想登录主机 C,可以连接本地的 localhost:22022:
1 | ssh -p 22022 user@localhost |
本地转发的一个更常见场景是:远程服务器 C-host 上某个服务正在 C-port 上监听,
本地主机 A-host 虽然可以通过 SSH 连接到 C-host,但由于防火墙等原因无法直接与 C-port 建立连接。此时可以使用本地转发(B-host=C-host),把连接到本地 A-tunnel-port 的流量转送到远端 C-port。
1 | ssh -N -L A-tunnel-port:localhost:C-port user@C-host |
这里 -N 表示不执行远程命令,仅建立隧道。
更常见的本地转发例子:
- 访问远程 MySQL,但数据库只监听在服务器本机的
3306端口
1 | ssh -N -L 3306:localhost:3306 user@db-server |
建立隧道后,本地数据库客户端可以直接连接:
1 | mysql -h 127.0.0.1 -P 3306 -u root -p |
- 访问远程机器上的 Web 管理界面,例如只允许本机访问的
http://localhost:8080
1 | ssh -N -L 8080:localhost:8080 user@web-server |
此时在本地浏览器访问 http://127.0.0.1:8080,实际访问到的是远程机器上的 localhost:8080。
- 通过跳板机访问内网中的 SSH 主机
1 | ssh -N -L 22022:10.0.0.15:22 user@jump-host |
这个例子在公司内网或云上多层网络环境中很常见。
远程转发(Remote port forwarding)
远程转发是在远端 SSH 服务器上创建一个监听端口,再把连到该端口的 TCP 连接转发回发起 SSH 连接的一侧,或该侧可访问的其他目标。
1 | 场景: |
远程转发的命令格式为(在跳板 B 上执行命令)
1 | ssh -R A-tunnel-port:C-host:C-port A-host |
其中:
A-tunnel-port:主机 A 上由 SSH 远程转发创建的监听端口,作为转发入口C-host:主机 C,可以是 C 的内网 ip,只要 B 可以访问即可C-port:主机 C 上目标服务的端口;如果目标是 SSH 服务,通常就是 22 端口A-host:主机 A
例如(在跳板 B 上执行命令)
1 | ssh -R 22022:C-host:22 user@A-host |
建立 SSH 隧道后,主机 A 如果想登录主机 C,可以连接自己本机上的 localhost:22022:
1 | ssh -p 22022 user@localhost |
更常见的远程转发例子:
- 把本地开发中的 Web 服务临时暴露到远程服务器上
假设本地正在运行一个开发服务:
1 | python3 -m http.server 3000 |
然后把本地 3000 端口转发到远程服务器的 18080 端口:
1 | ssh -N -R 18080:localhost:3000 user@remote-host |
这样登录到 remote-host 后,就可以访问:
1 | curl http://127.0.0.1:18080 |
- 让远程机器访问本地仅在回环地址监听的 SSH 服务
1 | ssh -N -R 22022:localhost:22 user@remote-host |
之后在 remote-host 上可以连接:
1 | ssh -p 22022 local-user@127.0.0.1 |
这类用法常见于临时远程协助或反向打通访问链路。
是否允许远端其他主机访问该远程转发端口,取决于
sshd_config中的GatewayPorts等服务端配置。默认情况下,很多系统只允许远端本机访问该端口。
动态转发(Dynamic port forwarding)
动态转发会在本地开启一个 SOCKS 代理监听端口。应用把请求发送到这个 SOCKS 端口后,SSH 客户端再通过已建立的 SSH 连接把流量转发到远端,由远端继续访问目标地址。
一个常见场景是:将访问外部网站的请求先送到本地 SOCKS 代理,再经由 SSH 服务器代为访问目标站点。
与本地/远程转发不同,动态转发不是预先写死一个固定目标地址,而是由 SOCKS 协议在每次建立连接时告知代理“这次要访问的目标地址和端口”。因此必须在系统或应用中显式配置 SOCKS 代理,例如服务器 localhost、端口 X。
应用的请求进入本地 SOCKS 监听端口后,会经 SSH 隧道送到远端,再由远端代表客户端与目标地址建立连接并返回结果。
动态转发的命令格式为
1 | ssh -D local-port tunnel-host |
例如使用本地的2121端口
1 | ssh -D 2121 tunnel-host |
这里会默认建立到 tunnel-host 的交互式 SSH 会话;也可以加上 -N,仅建立动态转发而不执行远程命令。关闭该 SSH 会话后,转发也会随之停止。
更常见的动态转发例子:
- 给浏览器配置一个临时 SOCKS5 代理
1 | ssh -N -D 1080 user@proxy-host |
然后在浏览器或系统代理设置中填写:
- 协议:SOCKS5
- 地址:
127.0.0.1 - 端口:
1080
这样浏览器的请求就会先进入本地 SOCKS 代理,再通过 proxy-host 转发出去。
- 给命令行工具临时指定 SOCKS 代理
1 | ssh -N -D 1080 user@proxy-host |
这里 --socks5-hostname 表示连同域名解析也交给远端代理处理。
- 配合
ProxyJump或跳板机建立动态转发
1 | ssh -J user@jump-host -N -D 1080 user@target-host |
这个命令表示:先经过 jump-host 登录 target-host,再基于到 target-host 的 SSH 会话在本地开启一个 SOCKS 代理。
代理转发
-W用于把标准输入/输出直接转发到目标主机和端口,常用于自定义跳板链路。较老环境中也常见用nc/netcat实现类似效果。
这部分内容不作详细讨论,只是提供一个典型例子:内网中有主机 A 和主机 B,主机 A 无法直接通过 SSH 连接 example.com,但主机 B 可以,那么可以在主机 A 中添加如下配置
1 | Host B |
这样主机 A 就会经由主机 B 与 example.com 建立 SSH 连接。此时仍然使用的是主机 A 的密钥对,需要确保对应公钥已经部署到 example.com 上,而不需要把私钥放到跳板机 B 中。
跳板选项
-J(ProxyJump)用于声明跳板机,是现代 OpenSSH 中比手写ProxyCommand更直接的写法。
-J 选项可以简化经由跳板机建立 SSH 连接的写法,在本地主机 A 上执行如下命令
1 | ssh -J userB@B userC@C |
其中 B 是跳板机,C 是目标主机。
原本的端口转发命令也可以直接加上跳板机,例如
1 | ssh -J userB@B -N -L 8989:localhost:8888 userC@C |
