中间人攻击

ssh是一种可以防止中间人攻击的远程登录协议,通常我们用的实现是openssh。那么ssh是如何防止中间人攻击呢?

先来看下ssh登录过程。

  1. ssh客户端连接ssh服务端,服务端将自己的公钥发给客户端
  2. 客户端用公钥加密登录密码,并将加密后的密码发给服务端
  3. 服务端用自己的私钥解密加密后的密码,若正确,则允许登录。

ssh使用rsa/dsa非对称加密,所有客户端均使用rsa公钥加密,只有服务端使用rsa私钥加密。整个过程的确可以防止用户密码不被泄露,但是却不能有效的避免中间人攻击。

什么是中间人攻击呢?

简单来说,攻击者可以截获ssh客户端的请求,然后假冒ssh服务端,发送一个伪造的公钥给ssh客户端,如果客户端没有发现公钥有问题,仍然将密码用伪造的公钥发送给攻击者,攻击者就可以用自己的私钥解密得到客户端的密码了。

写到这里我突然想起来一个哭笑不得的事情。

我司的内网一直不是太好,有段时间大家发现127网段的服务器,会经常出现连接中断的情况,IT查了下说网络不忙,跟他们无关。有天我也要用这个网段的服务器了,但是ssh连接还是频繁掉线。我是连延迟30s都忍受不了的人,这种老是掉线当然不能忍,于是就花了个晚饭的时间进机房排查。

最后发现,原因非常简单,有台服务器的管理口上配置的地址,是该vlan的网关地址,所以vlan内的服务器在出流量上,有时会路由到这个服务器的管理口上(我是通过服务器上的网关的ARP来判断网络中又伪造的网关),所以ssh就不通了。

所以如何辨认李逵还是李葵真是个大难题呀。

回到ssh主题,客户端如何防止中间人攻击呢?由于ssh服务端的公钥是ssh server自己签发的,不像https有一个CA中心(其实也有很多https服务器的证书是自己生成的),客户端只能通过其他手段(网站,mail)预先知道服务端的fingerprint,再跟ssh连接时服务端发过来的fingerprint做比对,才能防止中间人攻击。

ssh 192.168.100.1
The authenticity of host '192.168.100.1 (192.168.100.1)' can't be established.
RSA key fingerprint is SHA256:/buxPubL/LoZ6SU1vF17sUZhytBMny+M/6pz2YKbzj0.
Are you sure you want to continue connecting (yes/no)? no
Host key verification failed.

有没有更好的手段呢?

ssh协议允许客户端将自己的公钥放到服务器上(通常是.ssh/authorized_keys文件),下次客户端在连接服务端时就不需要输入密码了。

这个办法可以方便客户端登录服务端,同时也防止了中间人攻击。

TODO:用过AWS的同学知道,ssh登录AWS的EC2虚拟机的时候,默认不使用密码,而是使用identity_file,这个文件存储的是服务端的私钥。不过还不清楚是怎么做到的。

免密打通的基本原理

简言之就是想办法把客户端的公钥放到服务器上。通常会在客户端用ssh-kengen生成公私钥,然后用ssh-copy-id将公钥拷贝到服务端的.ssh/authorized_keys文件中。

具体怎么实现就不说了,可以参考阮一峰的SSH原理与运用(一):远程登录

full mesh全连接免密打通

最近在帮隔壁组做GreenPlum的事情,发现GP有一个很特别的需求,就是ssh是full mesh全连接打通的,据说是因为其segment之间也有交互,需要ssh免密(其实我不太喜欢这种粗暴的做法,像kubernetes这种应用自己搞定认证走API的做法会更好一些,比如service account,容器中可以访问api server,并不走ssh)。

GP源码里有脚本实现了full mesh password-less:

gpssh-exkeys -f hostlist

hostlist里是需要打通的各个节点。执行命令后会要求输入某个节点密码,如果各个节点的密码一致,只需要输入一次。我们自己的版本里,我加了个-p参数,可以命令行中直接给出密码,方便静默安装。

简单记录下实现(gpdb/gpMgmt/bin/gpssh-exkeys)。

第一步 横空出世

主节点上,创建 id_rsa 公私密钥对,并将公钥加到 authorized_keys ,并使用ssk-keyscan收集本机ssh服务端的fingerprint,从而达到ssh连接本机免密。

cmd = 'ssh-keygen -t rsa -N \"\" -f %s < /dev/null >/dev/null 2>%s' % (GV.id_rsa_fname, errfile)
authorizeLocalID(localID) # cat id_rsa.pub to '%s/.ssh/authorized_keys' % homeDir
cmd = 'ssh-keyscan -t rsa %s >> %s 2>%s' % (host.host(), GV.known_hosts_fname, errfile)

第二步 遍访群雄

主节点上,用ssh-keyscan询问、收集所有主机的fingerprint。这步要注意,如果hostlist里只填写了节点的IP地址,gpssh-exkey不会去resolve这个IP的主机名,所以最终打通ssh的时候,只有IP地址是免密无任何提示的,如果这时用主机名ssh登录,会告警,提示knownhosts记录冲突。解决的办法就是hostlist中填写IP、主机名各一个记录。

for h in GV.allHosts:
    cmd = 'ssh-keyscan -t rsa %s >> %s 2>%s' % (h.host(), 
    		GV.known_hosts_fname, errfile)

第三步 纵横捭阖

打通maser节点到各节点免密,并收集existing节点的.ssh信息,具体来说做了四点微小的工作:

  1. 将主节点的公钥写到各个节点上,以达到master到各节点免密,
  2. 若指定已有节点 -e existingHosts,则收集各节点的authorized_keys/known_hosts/id_rsa.pub,保存到./tmp/{hostx}/中
  3. 若指定已有节点 -e existingHosts,则收集已有节点的公钥id_rsa.pub到./tmp/authorized_keys
  4. ssh 测试主节点到各个节点是否免密打通

GP使用了paramiko包。paramiko是一个sshv2协议的python实现,包括客户端和服务端。这里主要是用了客户端的功能。sendLocalID作为入口,client.connect成功之后就可以p.exec_command远程执行命令了。相对于命令行中用expect去输入密码,paramiko要优雅干净一些。

def tryParamikoConnect(self, client, pwd=None, silence=False):
    try:
        client.connect(self.m_host, password=pwd)
        return True
def sendLocalID(self, ID, passwd, tempDir):
    p = paramiko.SSHClient()
    p.load_system_host_keys()
    ok = self.tryParamikoConnect(p, silence=True)
    if not ok:
    for pwd in passwd:
        ok = self.tryParamikoConnect(p, pwd, silence=True)
        if ok: break
      while not ok:
          print >> sys.stderr, '  ***'
          pwd = getpass.getpass('  *** Enter password for %s: ' % (self.m_host), sys.stderr)
          if pwd: ok = self.tryParamikoConnect(p, pwd)
          ...
      cmd = 'echo \"%s\" >> .ssh/authorized_keys && echo ok ok ok' % ID
      if GV.opt['-v']: print '[INFO %s]: %s' % (self.m_host, cmd)
      (cin, cout, cerr) = p.exec_command(cmd)

第四步 去蕪存菁

删掉 knowhosts 和 authorized_keys 文件中重复的记录。

第五步 平定天下

对于新节点,直接将master节点的 authorized_keys/known_hosts/id_rsa{.,pub} 拷贝(覆盖)到节点对应用户的.ssh目录下;

对于已有节点,则需先将该节点下的 authorized_keys/known_hosts 文件与新增节点的记录merge生成新文件,但id_rsa{.pub}文件则尊重老同志,原来节点已有的情况就不要更新了;如果没有,则使用master节点的公私钥。之后再将这四个文件拷贝到已有节点上。

至此,full mesh全连接ssh免密打通的工作就完成了。