Secure Shell Protocol,简称SSH。加密的网络传输协议。
SSH使用客户端-服务器模型,标准端口为22。
SSH最常见的用途是远程登录系统。
服务器端需要开启SSH守护进程以便接受远端的连接,而用户需要使用SSH客户端与其建立连接。
(来自维基百科)
公私钥 免密登录
ssh-keygen
用于生成公私钥。直接输入,会进入交互模式:
- 决定在哪里保存将生成的「公私钥文件」
- 是否为私钥设置密码
默认在~/.ssh/
生成RSA
类型的密钥文件:私钥文件id_rsa
与公钥文件id_rsa.pub
。
ssh-keygen -t rsa -f /path/id_rsa_test -P '' -b 1024 -C "xxx@gmail.com"
-f
指定密钥生成路径及名称。可以直接文件名,则默认放在~/.ssh/
下。-P
设置私钥密码。空字符串表示无密码。若未指定-P
,会交互式提示设置密码。-p
交互式修改私钥密码。-t
指定密钥类型,即使用的加密算法,默认rsa
。-b
指定密钥的位数。-C
公钥备注信息。
自定义密钥名,在ssh连接对应主机时,必须用-i
指定对应的私钥文件才能连接!
ssh -i /xxx/id_rsa_xxx xxx@192.168.1.1
免密码登录远程主机
SSH除了常规「用户名+密码」登录远程主机之外,更常用的方式是采用「密钥对」来免密登录,特别是运维自动化,这是很重要的。
免密登录的前提:将本机的公钥内容,添加到远程主机的authorized_keys
文件中。
手动添加:
将公钥内容复制给有权限上远程主机的人,由Ta来手动单台添加。或者借助自动化工具批量主机添加。
命令添加:
若自己有用户密码可以上远程主机,那么可以本机执行命令来远程添加公钥,更方便:
ssh-copy-id -i ~/.ssh/id_rsa.pub username@remote-IP
输入一次远程主机用户的密码,完成提供公钥的功能后,就不再需要输入了。注意这里是公钥文件!
若远程主机未使用SSH默认22端口,则需要:
centos6中:ssh-copy-id -i ~/.ssh/id_rsa.pub "username@remote-IP -p 23456"
centos7中:ssh-copy-id -i ~/.ssh/id_rsa.pub username@remote-IP -p 23456
若不用这个ssh-copy-id的命令,就要输入一长串命令:
cat ~/.ssh/id_rsa.pub | ssh -p 22 username@remote-IP "umask 077;mkdir -p ~/.ssh;cat – >> ~/.ssh/authorized_keys"
免密登录失败的可能原因
假设已经将公钥加入到远程的authorized_keys
中:
-
ssh服务端
sshd_config
配置问题:不支持基于公钥认证,或不再是默认的公钥认证文件。 -
PubkeyAuthentication
配置项,控制是否基于公钥进行认证,默认值为yes
AuthorizedKeysFile
配置项,指定公钥认证文件。默认为authorized_keys
作为存储用户公钥的文件
-
ssh服务端相关目录或者文件的权限过大。
-
~/.ssh/
目录的权限正常700
~/.ssh/authorized_keys
文件的权限正常为600
- ssh服务端的
authorized_keys
文件中包含windows字符。手动复制容易出现。 - ssh客户端连接时,未指定公私钥对应的用户名。
-
ssh客户端的私钥权限过大。
-
- 私钥的正常权限设置为
600
。
- 私钥的正常权限设置为
- ssh客户端连接时,对于使用了自定义名称的密钥对,未在参数中用
-i
指定对应的私钥。
SSH代理 ssh-agent
ssh代理服务ssh-agent
,在本机后台运行,为ssh客户端管理本机的公私钥。
能解决的问题:
- 若本地有多对密钥,且使用不同密钥登录不同远程主机。此时的密钥名称往往是自定义的,每次SSH连接都必须手动指定对应的私钥文件,会很麻烦。
- 若私钥还设置了密码,则每次SSH连接都要输入一遍密码。
目的:简化繁琐的身份验证交互过程。
- 自动选择对应的私钥。
- 仅初次添加私钥时输入一次密码,后续创建连接不用再输入。(若代理进程挂掉重启了,还是需要重新添加密钥。就比如常见的重启电脑)
agent通常已启动,若没有启动则可以使用2种方式之一:
- 启动一个shell作为当前shell的子shell:
ssh-agent $SHELL
- 启动一个新ssh-agent进程:
eval
ssh-agent``
向agent添加私钥:
ssh-add ~/.ssh/id_rsa_xxx
查看agent中已管理的私钥:
ssh-add -l
-L
则显示对应公钥内容。
从agent中删除XXX密钥的管理:
ssh-add -d XXX
使用代理进行建立连接的简单过程:
当ssh客户端需要与ssh服务端进行认证时,
- 服务端会发送用于验证客户端身份的数据
- 此时,ssh客户端会跟ssh-agent进行交互,通过agent中的私钥对服务端发送过来的数据进行处理
- 然后将经过私钥处理的认证数据发送到服务端
- 服务端通过对应公钥检验数据,验证成功后,建立连接
SSH代理转发
参考文章:https://www.zsythink.net/archives/2422
假设:
有3台主机 A、B、C。
仅A上创建了密钥对,并将「公钥」添加到B、C的authorized_keys
。可以免密登录:
- A -> B
- A -> C
但B、C之间相互没有基于密钥的认证。
那么:
把B上的文件远程传到C上,能否成功?
scp youker@B-IP:/tmp/test.txt dq@C-IP:/tmp/test.txt
结论,无法成功。
因为SSH连接的顺序:A -> B -> C。C主机会要求B验证登录身份。
如果,B可以做中间代理,将C的验证请求转发给A,让A验证完再转发给C,这样就能连接了。
这就是「SSH 代理转发」提供的能力。
并且,C只能看到是B连接过来的!而B无需放A的私钥,或者让B仅为了连接C而另外创建一对公私钥!
这在工作中很常见。B往往不仅连接C,还会连接D、E、F…..
A好比个人电脑,B相当于跳板机(企业中在跳板机之前还会有堡垒机),C、D、E、F….就是一堆内网主机。
使用「SSH代理转发」后,仅需将个人的公钥放到远程主机的authorized_keys
中,就可以一路连接过去了。(当然,一般会做限制,比如内网主机之间限制互相跳。)
启用方法:
-
A主机ssh客户端启用「代理转发」。
-
- 修改
ssh_config
配置文件。将ForwardAgent
的值设置为yes
,默认值为no
。
- 修改
-
B的ssh服务端需要「允许代理转发」,当然,默认都是允许作为中间代理的。
-
- 修改的是
sshd_config
配置文件。注意是sshd。将AllowAgentForwarding
配置项设置为yes
。默认值就是yes
。
- 修改的是
SSH端口转发
参考文章:https://www.zsythink.net/archives/2450
分为2种:
- 本地转发
- 远程转发(反向SSH隧道)。可以用作「内网穿透」。
SSH的Python库 paramiko
Python中使用paramiko
第三方库实现ssh连接。
注意,在连接到目标主机之前,需要处理「连接到未知主机」的问题:
「手动方式」
- 先shell中连接目标主机,输入yes,将目标主机的
host key
保存到known_hosts
文件中。 - 在
client.connect()
之前先执行加载known_hosts
client.load_system_host_keys() # 从系统默认的"~/.ssh/known_hosts" 加载已信任的主机 host key
client.load_host_keys(filename="...") # 从自定义路径加载
client.connect(...)
「其他策略类」
paramiko.AutoAddPolicy()
比较常用,自动保存未知主机host key到known_hosts
paramiko.WarningPolicy()
给出警告并记录到日志,但会进行连接。paramiko.RejectPolicy()
直接抛出异常。- 也可以通过继承
paramiko.MissingHostKeyPolicy
来自定义策略类。
用法如下:
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(...)
「关于执行远程命令」
- 常用的是
client.exec_command(cmd)
,不会打开虚拟终端shell,不出现交互式的命令提示符! - 若要模拟虚拟终端,进行交互输入,则用到
client.invoke_shell()
几种示例:
from paramiko.client import SSHClient, AutoAddPolicy
from paramiko.config import SSH_PORT
from paramiko.rsakey import RSAKey
from io import StringIO
import paramiko
import getpass
def ssh_by_username_passwd(host, port, user, cmd):
"""「用户名+密码」方式进行SSH远程连接,并执行一条简单命令。
"""
# 实例化 ssh client 对象
client = paramiko.SSHClient()
# 自动添加未知主机到信任主机列表
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# * 创建SSH连接
client.connect(
hostname=host, port=port, username=user, password=getpass.getpass()
) # 通过getpass(),在执行脚本时交互输入密码,避免代码中写明文密码!
# 在远程主机执行命令
mystdin, mystdout, mystderr = client.exec_command(cmd) # 结果放入stdout,有错误则放入stderr
# 执行结果
ret = [f'[{host}]']
ret.extend(mystdout.readlines())
# mystdout.readlines() 返回list,每行都是byte字节串,可decode('utf-8')
# mystdout.read() 直接返回所有内容,为byte字节串,也可decode('utf-8')
return_code = mystdout.channel.recv_exit_status() # 命令执行结果的返回码
# 关闭连接
client.close()
return ret
def ssh_by_keyfile(host, port, user, cmd, private_key_file_path=""):
"""「公私钥」方式进行SSH远程连接,并执行一条简单命令。
该方式的前提,是需要两台主机间已经SSH授信。
即本地机器(paramiko脚本所在)的公钥,已经添加到目标远程主机的 ~/.ssh/authorized_keys 文件中。
所以若是无法公私钥方式远程连接,可能和 .ssh/目录、authorized_keys文件 的权限设置。
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 读取私钥文件
praivate_key = paramiko.RSAKey.from_private_key_file(
private_key_file_path
) # 创建密钥对时,若设置了密码,需指定passwoer参数。
# praivate_key = paramiko.RSAKey.from_private_key_file(
# private_key_file_path, password=getpass.getpass()
# )
# 建立连接,指定pkey
client.connect(hostname=host, port=port, username=user, pkey=praivate_key)
stdin, stdout, stderr = client.exec_command(cmd)
ret = stdout.read().decode('utf-8')
client.close()
return ret
def ssh_multiple_cmds(host, port, user, cmds=None, private_key_file_path=""):
"""一次SSH会话连接,执行多条命令。
每条命令若比较复杂,则可以借助 pipes 模块构造复杂 shell 命令行。
from pipes import quote
cmd_1 = quote("cat filname.txt")
cmd_2 = quote("cat 'filname with spaces.txt'")
cmd_3 = quote("cat filname\ with\ spaces.txt")
cmd_4 = quote("echo 'danger!'; rm -r /tmp/*")
cmd_5 = quote("echo `date` && df -hl")
cmds = [cmd_1, cmd_2, cmd_3, cmd_4]
"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
praivate_key = paramiko.RSAKey.from_private_key_file(private_key_file_path)
client.connect(hostname=host, port=port, username=user, pkey=praivate_key)
ret = []
for cmd in cmds:
stdin, stdout, stderr = client.exec_command(cmd)
stdin.close()
ret.append(stdout.read().decode('utf-8'))
stdout.close()
stderr.close()
client.close()
def sftp_by_keyfile(
host,
port,
user,
cmd,
private_key_file="",
local_path="",
remote_path="",
):
"""「公私钥」方式,SFTP上传、下载文件。
一些SSH的特性,需要借助更底层的对象Transport来实现操作:传输文件、端口转发...
"""
# 用Transport建立连接
trans = paramiko.Transport((host, port))
praivate_key = paramiko.RSAKey.from_private_key_file(private_key_file)
trans.connect(username=user, pkey=praivate_key)
# trans.connect(username=user, password=getpass.getpass()) # 密码登录方式则替换为 password 参数进行连接
# 实例化 ssh client 对象,并指定已连接的 Transport
client = paramiko.SSHClient()
client._transport = trans
stdin, stdout, stderr = client.exec_command(cmd)
# 从连接的Transport,实例化一个sftp对象。开启单独的 channel
sftp = paramiko.SFTPClient.from_transport(trans)
# 上传文件
sftp.put(localpath=local_path, remotepath=remote_path)
# # 下载文件
# sftp.get(localpath=local_path, remotepath=remote_path)
# 关闭 Transport 对象即可
trans.close()