年纪大了,记忆里急剧衰退。拥有的服务器越来越多,加上某些服务访问频率非常低,好几次都差点忘了登陆密码,惊出一身冷汗。就单独开个坑来总结用过的各种登陆姿势和进化过程,总的来说是从记密码 -> 不记忆/记录密码 -> 一键登录的过程。

SSH免密登陆

所谓免密登录,也就是说不需要人工再输入用户名和密码了,但这并不意味着没有了鉴权的动作,服务器毕竟是自己的,不能随便允许别人登录到上面去搞破坏。ssh 免密登录通过证书进行鉴权。

这里的证书分为公钥与私钥,我们可以简单地理解为:公钥 = 锁;私钥 = 钥匙。

流程基本是这样的:如果我们想要从 A 免密登录到 B,就需要把公钥(锁)放到 B 的特定位置,而 A 拥有私钥(钥匙)的完整副本。当 A 拿着私钥去访问 B 的时候,B 发现自己身上有一把锁(B 可能有很多公钥)可以被 A 的私钥打开,于是给 A 放行,A 就成功登录到 B 了。

对应到具体的实操,就是我的mac笔记本、本地的常用主机,生成自己的钥匙对,分别把自己的公钥放到需要免密登陆的host上。

生成钥匙对

ssh-keygen -t rsa

-t : 参数用于指定生成密钥的加密算法,选择rsa加密

**-C**:参数可以为密钥文件指定新的注释,格式为username@host。我会把这个username设置为要登陆的身份,而host是我登陆的本地机器名,比如我要以root的身份,从我的mac1登陆目标主机,我会设置为-C "root@mac1"

**-f**:指定密钥对的文件名。用目标主机名_登陆用户名,比较明显的区分钥匙用途。这个参数必须使用绝对路径,相对路径会失败。

**-b**:参数指定密钥的二进制位数。1024够了

**-F**:参数检查某个主机名是否在known_hosts文件里,搞不懂,macos下会执行失败

总结下来,我用的命令是:

ssh-keygen -t rsa -C "tx@macos1" -b 1024 -f /Users/username/.ssh/hostname_loginUser

一路回车,直到出现随机图案。

将公钥加到目标服务器

公钥必须上传到服务器,才能使用公钥登录,就像是把锁加到服务器的一个用户下面,然后我们在本地可以用那个用户的身份登陆服务器。

用户公钥保存在服务器的~/.ssh/authorized_keys文件里,我们知道Linux有多用户,要以哪个用户的身份登录到服务器,密钥就必须保存在该用户主目录的~/.ssh/authorized_keys文件。我们要做的就是把刚刚生成的hostname_loginUser.pub文件的内容加到~/.ssh/authorized_keys里。

用ssh远程执行命令:

cat ~/.ssh/hostname_loginUser.pub | ssh loginUser@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

上面的命令还是要手动输入密码的

注意,authorized_keys文件的权限要设为644:

chmod 644 ~/.ssh/authorized_keys

或者干脆直接先ssh登陆远程机器,本地复制公钥贴过去然后修改authorized_keys的权限。

还有一个ssh-copy-id 命令也可以实现远程脚本上传,但这个命令会有一些默认行为,懒得看了。。

执行登陆

命令行登录

ssh -i ~/.ssh/hostname_loginUser -p 666 -l loginUser ipaddress

-i : i for identity ,指定密钥文件

但这个还是麻烦,于是进一步通过别名登录。

host别名登录

在本地电脑的~/.ssh文件夹下新建一个文件名为config的文件,对要登陆的主机进行别名配置:

# 保持 ssh 服务连接
ServerAliveInterval  10
ServerAliveCountMax  5

Host hostName_loginUser
    HostName       ipaddress
    User           loginUser
    Port           22/那台机器设置的ssh端口,一般不建议用22
    IdentityFile   ~/.ssh/hostname_loginUser    #上面生成的私钥
# 可以有多份配置,比如一台机器的多个用户或者配置多个机器
Host hostName2_loginUser2
    HostName       ipaddress
    User           loginUser
    Port           22/那台机器设置的ssh端口,一般不建议用22
    IdentityFile   ~/.ssh/hostname_loginUser    #上面生成的私钥

然后在本地电脑执行登陆命令:

# 登陆名字为hostName的机器,以loginUser的身份。这个跟上面生成的密钥对是对应的
ssh hostName_loginUser

到这一步,已经有了通过命令行的一键登录方法,比频繁的输入ssh -p 666 user@host然后再敲密码,方便了很多。

但这个还不够方便,因为我在日常开发的时候,要配合多种终端工具,有时候会出现工具间的冲突。比如在一个item2的window里,在本地会开tmuxsession, 在tmuxwindow里,若执行ssh登录,登陆的shell是套在tmux的window中的,而远程机器可能也会用tmux来管理会话(否则一些常用的工作区就要一遍遍的去打开),这就造成了tmuxtmux的奇观。结果会命令混乱,而且在iterm2的窗口里,会有两层tmux状态栏。

我没去研究这种问题是不是有个比较官方的解决方法,但是我一想,这种不同机器的会话,都放在同一个iterm2窗口里本身也很奇怪,于是就通过不同窗口来管理不同机器的会话,这样更加清晰简单。

所以让这个操作更傻瓜的下一步,就是让免密登陆和常用工具相结合,达到的效果应该是打开窗口A,自动登陆A机器绘画,打开窗口B,自动登陆B机器会话。

关闭密码登陆

有了ssh密钥登陆以后,可以考虑关闭密码登陆的入口,但这个操作有一点风险,万一ssh密钥登陆还没搞好/密钥丢失(大部分情况是换电脑导致的),就会比较麻烦,需要从服务提供商/宿主机的层面,通过临时shell去打开密码登陆。

编辑/etc/ssh/sshd_config文件:

# 禁用密码验证 
PasswordAuthentication no
# 启用密钥验证 
RSAAuthentication yes
PubkeyAuthentication yes
# 指定公钥数据库文件 
AuthorsizedKeysFile .ssh/authorized_keys

在这一步要确认可以通过ssh密钥登陆远程服务之后,再进行重启,否则会出现上面提到的风险,同时多保留一个登陆后的会话,万一出问题可以再改回来。

然后重启ssh服务:

sudo systemctl sshd restart && sudo systemctl sshd status

看到sshd服务状态是绿色的running

执行远程命令

通过上面配置好主机的host别名以后,就能在本地电脑快速给远程主机下达执行命令:

# 远程执行单条命令
ssh hostName "cd ~; ls -al"
# 远程执行脚本
ssh hostName < hahaha.sh

配置同步

流水的设备铁打的数据,说不定哪天换电脑换配置,再来一遍实在崩溃。。

上面这一坨东西可以分为服务器本地机器两部分,对我来说本地机器其实是比较容易更换的,而且我可能在多个设备上折腾东西,每个设备都可能有访问远程服务器的需求,所以就得进一步考虑把整个配置过程进行自动化。另外这里就带来一个新的问题是安全性,多个设备,每个设备都生成自己的密钥对,一旦某个设备的密钥对泄露了,那服务器就集体遭殃。

自动化生成本地机器的一键登录配置

准备两个文件,一个是json,脚本用来读取,然后根据json的配置为远程服务器生成本地的密钥;另一个是根据json中remotehosts列的机器信息,准备好密码,因为每生成一个密钥,会通过ssh-copy-id向目标服务器加公钥。

{
  "localhostName": "mybook",
  "localUserName": "tanxiao",
  "keyPath": "/Users/rx/mm",
  "remotehosts": [
    {
      "hostname": "bandwagonhostA",
      "loginUser": "tx",
      "ip": "123.123.123.1"
    },
    {
      "hostname": "vultur",
      "loginUser": "tx",
      "ip": "123.123.123.2"
    },
    {
      "hostname": "aliyunUS",
      "loginUser": "root",
      "ip": "123.123.123.3"
    }
  ]
}

脚本如下,保存成比如gen_my_key.sh的文件,然后chmod +x gen_my_key.sh, 然后./gen_my_key.sh执行,期间会需要输入准备好的json文件绝对路径,然后等着输密码就行了。

#!/bin/bash

# read the JSON file
json=$(<config.json)

# parse JSON data into variables
localhostName=$(echo "$json" | jq -r '.localhostName')
localUserName=$(echo "$json" | jq -r '.localUserName')
keyPath=$(echo "$json" | jq -r '.keyPath')
remotehosts=$(echo "$json" | jq -r '.remotehosts[]')

# loop through each remote host in the JSON file
for host in $remotehosts; do
  hostname=$(echo "$host" | jq -r '.hostname')
  ip=$(echo "$host" | jq -r '.ip')
  loginUser=$(echo "$host" | jq -r '.loginUser')
  
  # generate ssh key using ssh-keygen
  ssh-keygen -t rsa -b 1024 -C "${loginUser}@${localhostName}" -f "${keyPath}/${hostname}_${loginUser}"

  # copy public key to remote host
  ssh-copy-id -i "${keyPath}/${hostname}_${loginUser}.pub" "${loginUser}@${ip}"
done

把上面的bash脚本保存,json的数据脱敏,格式不变保存一个template防丢,统一丢到仓库。

这里就还有一个恶心问题,那这么一坨明文密码我怎么搞。。。这个就涉及到密码管理器了。

通过SSH跳板机登陆内网机器

跳板机,除了有个安全性,另一个好处是可以快速登录内网机器,免密登陆。把上面免密ssh登陆的问题再拔高一个层次,将本机的笔记本想象成跳板机,后面被免密登陆的当成内网机器,就跟跳板机的结构有点像了。

跳板机经常在自己有服务器集群,或者有一堆虚拟机的情况下使用,优势很明显。比如你搞个pev或者ESXi,然后生成一堆虚拟机,这时候你可能就得弄个跳板机去登陆那一堆虚拟机了,在跳板机里搞个机器列表,输入代号登陆。这个是有很多开源软件来实现的,完全没必要自己手撸。

todo

EXPECT脚本登陆

expect是另一种终端登陆方法,但这个我觉得还不如ssh来的简单。expect本身是一个命令,保存成脚本后开头是#!/usr/bin/expect。对应到iterm里进行一键配置,command就应该是这个写好的expect脚本本身,作为shell环境初始化的命令。

这个我的机器虽然多一点,但也没那么多,配置起来更麻烦的样子。

另外,网上看到很多expect的配置教程,直接把明文密码放在脚本里,更好的方法应该是放密钥地址。

todo

iTerm2一键登录

我上面提过一嘴说,操作更傻瓜的下一步,就是让免密登陆和常用工具相结合。因为和服务器交互离不开终端,不管是和本地电脑交互还是和远程电脑交互,都依赖于它,最终一键登录还是要从工具出发,实现打开一个窗口就是我要的环境。

iTerm2的profile提供了执行命令的能力,指定习惯的shell,然后执行配置的命令,就能实现一键傻瓜登录。

然后就可以通过打开不容的iTerm2 profile的方式去登陆指定终端,事情到这里并没结束,还是不够傻瓜。iTerm2可以给profile配置快捷键,但这个快捷键不是全局生效,要么就cmd+o打开profile,然后选择去打开哪个会话。Mac OS下,大部分操作都希望能通过alfred来唤起,然后我是找了个野生的workflow

密码管理器

有一些比较出名的密码管理器,比如1password, 并不是说这种东西不好,只是即便本地加密+云端存储,心理上还是觉得膈应(我知道这不太符合技术流思维,毕竟当理解了原理并能理论上验证攻击代价的情况下就算是在云端也是安全的)。所以保守的倾向还是能够本地管理,自己的云端同步,设备共享。

chrome的密码存储安全吗?