编写 MariaDB PAM 认证插件
您可能知道,自 5.2.0 版本(2010 年 4 月发布)起,我们支持 可插拔认证。利用此功能,可以实现任意的用户认证和账户管理策略,完全取代 MariaDB 内置的使用用户名/密码组合和 mysql.user
表的认证方式。
此外,您可能听说过,Oracle 最近发布了一个 PAM 认证插件 用于 MySQL。可惜的是,这个插件无法在 MariaDB 上运行 — 尽管 MySQL 对可插拔认证的实现是基于我们的,但 API 不兼容。而且,由于它是闭源的,无法对其进行修改以使其在 MariaDB 中运行。并且 — 我不是编造的 — 这个插件不支持客户端和服务器之间的通信,因此即使有了这个插件和 PAM 的强大功能,唯一可能的认证方法仍然是简单的用户名/密码组合。
但我对自己说,编写认证插件很简单。我要自己写一个认证插件!带着二十一点和应召女郎。
我从安装开发头文件开始
sudo rpm -ivh MariaDB-devel-5.2.9-102.el5.x86_64.rpm
在 Debian 或 Ubuntu 上,您需要安装 libmariadbclient-dev
。顺便说一句,免责声明 — 我是针对 MariaDB-5.2 进行的,但稍作修改,这个插件也可以用于 MySQL-5.5。
现在我创建一个工作目录,并偷懒地复制了 auth_socket 插件源代码 — 它是 MariaDB 自带的认证插件之一 — 来自 Launchpad(仅 auth_socket.c)。精简后,删掉旧代码,它就变成了我的 pam.c
文件。
#define MYSQL_DYNAMIC_PLUGIN #include <mysql/plugin_auth.h> static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info) { } static struct st_mysql_auth pam_info = { MYSQL_AUTHENTICATION_INTERFACE_VERSION, "dialog", pam_auth }; mysql_declare_plugin(pam) { MYSQL_AUTHENTICATION_PLUGIN, &pam_info, "pam", "Sergei Golubchik", "PAM based authentication", PLUGIN_LICENSE_GPL, NULL, NULL, 0x0100, NULL, NULL, NULL } mysql_declare_plugin_end;
在文件末尾是插件描述符 — 所有插件类型都具有相同的结构。在其上方是认证插件描述符,它告诉 MariaDB 哪个函数执行实际的认证,以及客户端应该使用哪个插件。
我再说一遍 — 是 客户端 应该使用。事实上,认证过程始终是一个对话。服务器提出问题(“用户名?”,“密码?”),客户端回答。由于可加载插件可能导致服务器提出最意想不到的问题(“左手食指的指纹是?”),客户端也应该支持知道如何回答这些问题的插件。而它确实支持这些插件 — 或者更准确地说,libmysqlclient 会自动透明地为客户端应用程序支持这些插件。
然而,在这个特殊情况下,问题并不是很奇怪。PAM 可能只要求最终用户输入一些文本,因此客户端插件需要能够打印提示文本、读取用户的输入,并将其发送回服务器。然后重复此过程,直到服务器满意为止。幸运的是,MariaDB 已经有一个插件可以执行与用户的这种对话。毫不奇怪,这个插件叫做 dialog,在我的插件描述符中,我指定 pam 服务器插件需要客户端加载 dialog 插件才能继续进行认证。
现在,让我们看看这个插件骨架是否能工作
gcc -o pam.so pam.c `mysql_config --cflags` -shared -fPIC -lpam
它编译并甚至可以加载到服务器中。到目前为止看起来不错,然后我打开 man pam。
根据手册页(man pages),要执行 PAM 认证,需要执行以下步骤:
- 使用函数
pam_start()
初始化 PAM 子系统。 - 调用函数
pam_authenticate()
执行实际的认证。 - 使用函数
pam_acct_mgmt()
验证用户账户。 - 在认证过程中,PAM 可以更改用户名。使用函数
pam_get_item(PAM_USER)
获取新名称。 - 最后,总是应该调用函数
pam_end()
。
为了与客户端通信,PAM 允许指定一个 对话函数 — 这是 PAM 在需要时将调用的函数。
然后我将上述逻辑放入主函数 pam_auth()
中
#include <string.h> #include <security/pam_modules.h> #include <security/pam_appl.h> static int conv(int n, const struct pam_message **msg, struct pam_response **resp, void *data) { } #define DO(X) if ((status = (X)) != PAM_SUCCESS) goto end static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info) { pam_handle_t *pamh = NULL; int status; const char *new_username; struct param param; struct pam_conv c = { &conv, ¶m }; /* get the service name, as specified in CREATE USER ... IDENTIFIED WITH pam_auth AS "service" */ const char *service = info->auth_string ? info->auth_string : "mysql"; param.ptr = param.buf + 1; param.vio = vio; DO( pam_start(service, info->user_name, &c, &pamh) ); DO( pam_authenticate (pamh, 0) ); DO( pam_acct_mgmt(pamh, 0) ); DO( pam_get_item(pamh, PAM_USER, (const void**)&new_username) ); if (new_username) strncpy(info->authenticated_as, new_username, sizeof(info->authenticated_as)); end: pam_end(pamh, status); return status == PAM_SUCCESS ? CR_OK : CR_ERROR; }
插件几乎完成了。唯一缺少的部分是对话函数 conv()
。根据 PAM 文档,它将被调用时带有一个“问题”数组,这些问题应显示给用户,并且它必须返回用户的答案。此外,它还将获得一个不透明的指针参数 — 世界上几乎所有的 API 中的回调函数几乎总是带有它。从这个函数中,我将把“问题”发送给客户端,并接收答案。客户端的 dialog 插件将执行与用户的实际通信。
在可插拔认证 API 中发送和接收数据很容易。主认证函数 — 在我们这里是 pam_auth()
— 的一个参数是一个所谓的 vio 句柄。这个句柄提供了 read_packet()
和 write_packet()
函数,客户端和服务器插件可以使用它们进行相互通信。服务器将处理所有其他事情 — 传递数据包、分割和重组它们、加密(如果使用了 SSL)、使用 Unix 套接字、TCP/IP、命名管道、共享内存、确保服务器插件与正确的客户端插件通信、维护线路上的向后兼容协议等等。顺便说一句,vio 这个名字就是这样来的 — 它表示 Virtual I/O(虚拟 I/O)。
还有一个最后的困难需要克服。PAM 可以发送四种不同类型的消息,其中两种是纯信息性的,意思是“将此打印给用户”,另外两种是输入消息,意思是“打印此并读取回复”。然而,dialog 插件只支持“打印此并读取回复”类型的操作。为了解决这种 API 不匹配问题,我们的对话函数将累积 PAM 信息性消息,直到看到第一个输入消息。然后它将把所有累积和连接的消息作为一条大的提示字符串,在一个数据包中发送给 dialog 插件。这就是我的意思:
struct param { unsigned char buf[10240], *ptr; MYSQL_PLUGIN_VIO *vio; }; static int conv(int n, const struct pam_message **msg, struct pam_response **resp, void *data) { struct param *param = (struct param *)data; unsigned char *end = param->buf + sizeof(param->buf) - 1; int i; for (i = 0; i < n; i++) { /* if there's a message - append it to the buffer */ if (msg[i]->msg) { int len = strlen(msg[i]->msg); if (len > end - param->ptr) len = end - param->ptr; memcpy(param->ptr, msg[i]->msg, len); param->ptr+= len; *(param->ptr)++ = 'n'; } /* if the message style is *_PROMPT_*, meaning PAM asks a question, send the accumulated text to the client, read the reply */ if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF || msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { int pkt_len; unsigned char *pkt; /* allocate the response array. freeing it is the responsibility of the caller */ if (*resp == 0) { *resp = calloc(sizeof(struct pam_response), n); if (*resp == 0) return PAM_BUF_ERR; } /* dialog plugin interprets the first byte of the packet as the magic number. 2 means "read the input with the echo enabled" 4 means "password-like input, echo disabled" C'est la vie. */ param->buf[0] = msg[i]->msg_style == PAM_PROMPT_ECHO_ON ? 2 : 4; if (param->vio->write_packet(param->vio, param->buf, param->ptr - param->buf - 1)) return PAM_CONV_ERR; pkt_len = param->vio->read_packet(param->vio, &pkt); if (pkt_len < 0) return PAM_CONV_ERR; /* allocate and copy the reply to the response array */ (*resp)[i].resp = strndup((char*)pkt, pkt_len); param->ptr = param->buf + 1; } } return PAM_SUCCESS; }
就这样。现在我可以像上面那样编译它(重复了两次,因为第一次忘了加 -lpam),加载它,配置 PAM 对“mysql”服务使用 pam_skey,创建用户,最后登录。
$ mysql -u root Welcome to the MariaDB monitor. Commands end with ; or g. Your MariaDB connection id is 1 Server version: 5.2.9-MariaDB-debug Source distribution MariaDB [(none)]> CREATE USER serg IDENTIFIED VIA pam USING 'mysql'; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> ^DBye $ mysql -u serg challenge otp-md5 99 th91334 password: <enter> (turning echo on) pasword: OMEN US HORN OMIT BACK AHOY Welcome to the MariaDB monitor. Commands end with ; or g. Your MariaDB connection id is 2 Server version: 5.2.9-MariaDB-debug Source distribution MariaDB [(none)]> SELECT "Hey-ho! It works!!!";
Sergei 干得好!这个插件很快会在 MariaDB 版本中发布吗?
是的,绝对会。
我们更倾向于与 Percona 合作,他们最近开始开发一个 PAM 插件。但是 Percona 的 PAM 插件没有使用“dialog”插件,因此它与 GUI 客户端配合得不好,并且发送了比必要更多的包(更多往返)。
我建议将我的更改包含在 Percona 的 PAM 插件中
https://code.launchpad.net/~maria-captains/percona-pam-for-mysql/use_dialog/+merge/80551
希望它能成功。然后我们会把它包含到我们的代码树中。否则,我们可以根据这篇博客文章创建一个插件。
毕竟——编写 PAM 认证插件非常简单 :)
在这里
http://kb.askmonty.org/en/pam-authentication-plugin