如何实现 MySQL 的一次一密登录

如何实现 MySQL 的一次一密登录

背景介绍

在日常工作环境中, 开发或者测试人员经常需要连接测试库、线上库等查看表结构或数据来验证程序的功能. 实际上让 DBA 协助开发者查看信息会是特别繁琐且无趣的事情, 所以为了方便起见会将数据库的权限分发给开发或者测试人员. 不过长此以往下去有几件事情会让我们烦恼不已:

1. 账号会在开发者之间相互传递;
2. 为了方便开发者会以快捷命令的方式查看信息, 密码信息容易暴露;
3. 开发者忘记密码, DBA 可能需要重置以通知所有其他人员修改密码;

事实上, 上述几种情况是很难避免的, 只要有人工参与就会有这些潜在危险的隐患, 所以我们就需要提供一个相对方便记住的又能保证相对安全的方式供开发者使用. 下面则从不同层面简单的对这种方式进行描述.

管理主机

如果从系统层面来看, 我们建议最好把所有的开发者都集中到一台管理主机上登录, 只有开发者连接到这台机器上, 才能通过该机器连接测试库, 线上库等进行查询信息操作. 如下图所示:


     +------------+     ssh      +--------------+                +-----------+
     | developers |  ----------> | manager host |    --------->  | databases |
     +------------+              +--------------+                +-----------+

开发者通过 ssh 登录该主机, 这个步骤最好是以 key 的方式登录, 开发者的私钥最好设置密码; 在登录主机后, 开发者再连接后面的数据库, 不过这个步骤又回到了我们上述提到的三个问题, 只是发生的环境在我们可控的主机上, 而不是在开发者的层面.

另外如果可以的话, 建议在 manager host 主机中部署 google-authenticator-libpam, 让开发者一次一密以 keyboard interactive 的方式登录 manager host, 这样可以避免开发者私钥文件泄露引起的安全隐患(当然 pam 生成的安全字符串不能泄露)。

这种方式其实并没有本质上的改进, 只是将所有不稳定因素都限制到一台主机中, 在安全方面进步不少.

MySQL pam 插件

官方和 percona, mariadb 等分支版本都提供了 pamauth_pam 插件, 我们可以基于此完成很多类似 ldap, 一次一密, 系统用户等方式登录 MySQL 数据库, 更多见 more.

这些插件确实为我们提供了很方便的方式来连接数据库, 但是它们都有一个共同的问题, 就是不支持远程连接. 如下图所示:

   +--------------+                +-----------+
   | manager host |    --------->  | databases |
   +--------------+                +-----------+

我们需要在数据库主机中开启 pam 插件以方便开发者登录数据库, 但是开发者并不能在 manager 主机中以 pam 的方式连接后面的数据库, 当然或许可以通过 ssh host -e "xxxxx" 的方式连接, 但是作为系统管理员或者 DBA 不大可能为所有数据库主机都开通相关的用户权限, 为每台数据库主机设置 pam 插件及建立相关用户也是特别繁琐的事情.

如果开发者访问的数据库很少, 可以考虑 pam 插件和 google authentication 相结合的方式供开发者访问. 这种方式同样解决不了上述提到的三个问题.

代理访问

我们也可以从中间件层面考虑这个问题, 简单描述则为中间件接收用户发送过来的用户名和密码进行校验, 如果通过则使用真实的数据库用户名和密码去和后端的数据库进行交互, 如下图所示:

             user/password                 mysql_user/pass
  +------+                     +-------+                     +--------------+
  | user |  ---------------->  | proxy | ------------------> | MySQL Server |
  +------+                     +-------+                     +--------------+

这里用户输入的用户名和密码最好是伪造的, password 应该具有既好记又比较安全的特点. 这里我们想到了 google authentication 的方式, 使用基于时间的 totp 方法动态生成用户输入的 password.

portproxy 则基于该方式实现开发者一次一密的访问数据库. 原理则比较简单, portproxy 劫持用户发送过来的用户名和密码信息, portproxy 默认以 user+totp 作为用户的默认密码, 如果校验成功, 则使用真实的用户密码构造 MySQL 的验证报文, 再发送到后端的 MySQL 数据库, 其流程大致如下:

             user/user+totp                   mysql_user/pass
  +------+                     +-----------+                     +--------------+
  | user |  ---------------->  | portproxy | ------------------> | MySQL Server |
  +------+                     +-----------+                     +--------------+

这种方式可以很容易的解决开发者遗忘密码的问题, 只要记住用户名及能够获取对应的 totp 6位数字即可连接数据库; 如果再加上管理机, 就能限制所有开发者在一台机器上操作, 也能比较方便的杜绝开发者互相传递数据库密码; 另外也可以在用户输入密码前封装一层, 只允许 tty 方式接收用户输入的密码, 这样就可以避免开发者以快捷方式连接数据库; 当然如果开发者足够厉害也是可以绕过我们的限制, 这种情况下也能解决上述的1, 3 两个问题.

portproxy 如何实现一次一密验证

事实上, portproxy 是解析了 mysql connection 的验证协议才实现了劫持的目的, 正常情况下, mysql 的连接建立过程如下: connection

clientserver 三次握手完成后, server 开始给 client 发送初始的报文, 其中就包含了协议版本, MySQL Server 版本, 用户名, 连接 id 以及随机且固定长度的初始验证数据;

client 接收到 server 的初始报文后, 解析出验证的协议版本(MySQL 老的加密协议或者新的协议), 以及20位初始的随机验证数据. 通过 20位的数据和用户输入的密码经过下面的算法校验用户是否有效:

SHA1( password ) XOR SHA1( "20-bytes random data from server" <concat> SHA1( SHA1( password ) ) )

portproxy 就是通过三次握手后接收 server 发送的初始报文解析出我们需要的用户名和20位随机数据, 再将用户名+totp 作为默认的密码校验开发者是否输入正确的password(user+totp), 如果正确则重新使用真实的用户名和密码以及20位随机数据构造新的验证数据报文发送给 server, 通过后则连接建立完成, 开发者就可以正常访问数据库. 更多通信协议见 client-server-protocol.

arster 用户名为例进行以下操作:

# sys-google-totp -secret "OLENMTM3BTB36EUY"
otp message:
202340 (22 second(s) remaining)

# mysql -h 10.0.21.5 -P 33306 -u arster -p
......
......

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql userread@[10.0.21.5:33306 (none)] > 
mysql userread@[10.0.21.5:33306 (none)] > quit

这里输入的密码就应该是 arster202340, totp 默认 30 秒变更一次, 开发者需要保证有足够的时间输入密码, 如果时间不够则重新执行 sys-google-totp 命令获取新的 6 位数字. 校验成功后, portproxy 则使用真实的 userread 用户重新构造数据报文并发送给后面的数据库.

总结

实际工作中, DBA 或系统管理员最烦的可能就是开发者忘记密码, 如果 DBA 也没有记录用户密码, 就只有重置这种方式, 最后再通知所有开发者进行修改. portproxy 的方式能够解决开发者忘记密码的问题, 稍加设置或封装就可以解决另外两个问题. 当然如果一些公司的devops做的足够好的话就可以不用考虑这些, 只需要保证开发者不乱传账号信息或者账号信息不被盗取就可以避免我们上述讨论的三个问题.