PAM
更新时间: 2026/02/09
在Gitcode上查看源码

PAM 概述

PAM(Pluggable Authentication Modules )是由 Sun 提出的一种认证机制。它通过提供一些动态链接库和一套统一的 API,将系统提供的服务和该服务的认证方式分开,使得系统管理员可以灵活地根据需要给不同的服务配置不同的认证方式而无需更改服务程序。 PAM 使用一个层次结构来组织身份验证模块,管理员可以根据需要来加载、卸载、组合多个模块,达到身份验证策略的定制效果。

PAM 框架

PAM 框架由以下几个部分组成:

  1. PAM 应用程序:需要进行身份验证或授权的应用程序,如 sshd、login、telnet等。
  2. PAM 库:一个公共库,它提供了用于进行身份验证和授权的 API。应用程序使用这些 API 与 PAM 进行交互。
  3. PAM 配置文件:PAM 配置文件位于 /etc/pam.d 目录中。每个应用程序都有一个对应的 PAM 配置文件,用于指定身份验证和授权流程。配置文件中包含一个或多个 PAM 模块,它们是用于完成特定任务的代码库。(也有一种形式是一个 PAM 配置文件中包含了所有应用程序的制定流程,下个章节细讲)
  4. PAM 服务模块:用于执行身份验证和授权任务的代码库。模块可用于检查用户密码、检查用户是否在允许的 IP 范围内、检查用户帐户是否已过期等。PAM 模块可以使用已安装的身份验证技术(如密码、Kerberos、LDAP 等)进行身份验证。
  5. 身份验证源:用于进行身份验证的数据源,如本地文件(file),LDAP 目录(ldap)等(一般配置在 /etc/nsswitch.conf 中)。

可见下图,应用程序通过 PAM 应用接口(API)与 PAM 库进行通信,PAM 服务模块通过服务提供接口(SPI)与 PAM 库进行通信。以 PAM 库为中转实现应用程序与 PAM 服务模块之间的通信。

PAM 配置文件

主配置文件 - /etc/pam.conf

主配置文件默认不存在,一般不使用,其中主要由5个列组成。

service-namemodule-typecontrol-flagmodule-patharguments

第一个列的 service-name 其实就是对应了 PAM 应用程序,也就是 sshd、login 这些,代表了这一行是在哪个命令动作下生效的。 由于一般不使用主配置文件,后面就不展开细讲了,后面4个列的内容在于应用程序配置文件中一致,后面会展开说。

应用程序配置文件 - /etc/pam.d/

只要 /etc/pam.d/ 这个目录存在,PAM 就不会尝试去找 /etc/pam.conf。不难看出,这个目录下的文件是由 service-name 来命名的,也就是说一个文件就对应了一个应用程序的配置。 其中有一个比较特殊的 service-name,叫作 other,它代表的是所有没有在该文件中明确配置的其它服务,有点类似于 switch 中的 default。例如,当前 /etc/pam.d/ 下只配置了 login,那么此时 sshd 登录的找不到匹配程序了,就走 other。

这里就sshd来举例,以表格形式展开给大家看看,接下来会在《PAM 模块》章节来一个一个来讲这些参数的意义。

txt
#%PAM-1.0

auth       include      common-auth
auth       required     pam_bmc_login.so
auth       include      /data/trust/pam_faillock
account    required     pam_nologin.so
account    required     pam_bmc_login.so

# SELinux needs to be the first session rule. This ensures that any
# lingering context has been cleared. Without out this it is possible
# that a module could execute code in the wrong domain.
# When the module is present, "required" would be sufficient (When SELinux
# is disabled, this returns success.)
session    [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close

account    include      common-account
password   include      common-password
session    optional     pam_keyinit.so force revoke
session    include      common-session
session    required     pam_loginuid.so

# SELinux needs to intervene at login time to ensure that the process
# starts in the proper default security context. Only sessions which are
# intended to run in the user's context should be run after this.
# When the module is present, "required" would be sufficient (When SELinux
# is disabled, this returns success.)
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
module-typecontrol-flagmodule-patharguments
authincludecommon-auth
authrequiredpam_bmc_login.so
authinclude/data/trust/pam_faillock
accountrequiredpam_nologin.so
accountrequiredpam_bmc_login.so
session[success=ok ignore=ignore module_unknown=ignore default=bad]pam_selinux.soclose
accountincludecomon-account
passwordincludecommon-password
sessionoptionalpam_keyinit.soforce revoke
sessionincludecommon-session
sessionrequiredpam_loginuid.so
session[success=ok ignore=ignore module_unknown=ignore default=bad]pam_selinux.soopen

特定模块配置文件 - /etc/security/

一些特定的插件模块配置文件在 /etc/security/ 目录下,用于对应模块在工作时读取配置。

这里就time.conf举例,这些默认的配置文件注释还是很详细的,可以看见单行构成语法为 services;ttys;users;times ,并且参数注释都在里面,指定了什么服务、什么终端、什么用户、什么时间可用。 这个配置文件就是为 account 下的 pam_time 这个模块来提供配置,当应用程序中配置了这一行后,用户登录后会就会基于这个配置文件进行登录时间检查,如果在匹配服务、终端和用户下,如果不在允许的实际范围内,则会基于 control-type 进行拦截和告警。这个的作用简单来说类似于 BMC 中配置登录规则的时间规则,只是这个配置文件的时间选择比较局限。

txt
# this is an example configuration file for the pam_time module. Its syntax
# was initially based heavily on that of the shadow package (shadow-960129).
#
# the syntax of the lines is as follows:
#
#       services;ttys;users;times
#
# white space is ignored and lines maybe extended with '\\n' (escaped
# newlines). As should be clear from reading these comments,
# text following a '#' is ignored to the end of the line.
#
# the combination of individual users/terminals etc is a logic list
# namely individual tokens that are optionally prefixed with '!' (logical
# not) and separated with '&' (logical and) and '|' (logical or).
#
# services
#       is a logic list of PAM service names that the rule applies to.
#
# ttys
#       is a logic list of terminal names that this rule applies to.
#
# users
#       is a logic list of users or a netgroup of users to whom this
#       rule applies.
#
# NB. For these items the simple wildcard '*' may be used only once.
#
# times
#       the format here is a logic list of day/time-range
#       entries the days are specified by a sequence of two character
#       entries, MoTuSa for example is Monday Tuesday and Saturday. Note
#       that repeated days are unset MoMo = no day, and MoWk = all weekdays
#       bar Monday. The two character combinations accepted are
#
#               Mo Tu We Th Fr Sa Su Wk Wd Al
#
#       the last two being week-end days and all 7 days of the week
#       respectively. As a final example, AlFr means all days except Friday.
#
#       each day/time-range can be prefixed with a '!' to indicate "anything
#       but"
#
#       The time-range part is two 24-hour times HHMM separated by a hyphen
#       indicating the start and finish time (if the finish time is smaller
#       than the start time it is deemed to apply on the following day).
#
# for a rule to be active, ALL of service+ttys+users must be satisfied
# by the applying process.
#

#
# Here is a simple example: running blank on tty* (any ttyXXX device),
# the users 'you' and 'me' are denied service all of the time
#

#blank;tty* & !ttyp*;you|me;!Al0000-2400

# Another silly example, user 'root' is denied xsh access
# from pseudo terminals at the weekend and on mondays.

#xsh;ttyp*;root;!WdMo0000-2400

#
# End of example file.
#

其它配置文件 - 如 /etc/nsswitch.conf

PAM 模块

模块语法解析

鉴别模块 - auth

用于授予用户访问帐户或服务的权限。提供此服务的模块可以验证用户并设置用户凭证。 对应 API:

  1. pam_authenticate() 该函数被用来验证用户的合法性(即口令认证),用户就是在 pam_start() 参数传递的 User。User 被要求提供一个密码或其它输入。然后该 API 会通过回调指定服务模块中对应的 pam_sm_authenticate() 函数来检测 User 的输入并验证是否合法。
  2. pam_setcred() 该函数被用来创建、维持、或删除一个用户的证书。pam_setcred() 应该在 User 已经通过验证(after pam_authenticate)并且在会话建立之前(before pam_open_session) 被调用。删除 User 证书的操作必须在会话被关闭之后执行(after pam_close_session)。User 证书应该被应用程序创建,而不是被 pam 库或者服务模块创建。

账户管理 - account

用于执行基于非身份验证的帐户管理。它通常用于根据指定的一些规则(如账户失效、时间、ip、系统资源等),来判断用户对系统的访问是限制还是允许。 对应 API:

  1. pam_acc_mgmt() 该函数通常被用来确认用户账户是否有效,一般在 User 已经通过验证后(after pam_authenticate)被调用执行,通常来说需要确认的内容包括:账户是否过期、账户访问权限等。

口令管理 - password

用于强制实施口令强度规则并执行验证令牌更新。 对应 API:

  1. pam_chauthok() 该函数一般用于重设用户的口令,进行一些复杂度之类的检查。

会话管理 - session

用于设置和终止登录会话。 对应 API:

  1. pam_open_session() 该函数为已经成功通过验证的用户建立一个用户会话,该会话应该用户退出、超时等情况下被函数 pam_close_session() 终止。
  2. pam_close_session() 该函数被用来关闭 pam_open_session() 创建的会话。

控制标志 - control-flag

控制标志告诉了 PAM 库改如何处理对应 PAM 服务模块产生的成功或者失败情况。 由以下几个关键字标识:

  1. required: 一票否决,也就是说本模块必须返回成功才通过认证。但是如果本模块返回失败,失败结果也不会立即中断流程进行通知,而是要等所有模块都执行完毕后,才会将失败结果返回到应用程序。 由于不会因为验证失败而继续停止验证过程,可以让用户无法感知是哪个规则项验证失败。

  2. requisite: 一票否决,与 required 不同点是,本模块如果返回失败,不会再执行其它模块,会将失败结果直接返回到应用程序。

  3. sufficient: 一票通过,本模块返回成功会直接完成身份认证,不必再执行其它模块。本模块返回失败结果可忽略,该关键字优先级高于 required 和 requisite。也就是说,即使 required 返回失败,sufficient 返回成功也会直接导致成功。

  4. optional: 可选模块,本模块执行结果成功与否不会对身份认证起关键作用,返回值一般忽略。 (个人感觉可能一般是用于执行一些附加动作,没它也行,有它更好的那种)

  5. include: 调用模块,指定了一个其它的配置文件名,用以调用其它配置文件中定义的配置信息。 如:common-auth、common-account、common-password、common-session等,也有像我们自己指定配置的 pam_faillock 这种。

  6. 复杂控制标志: 复杂的控制标志能够让管理员可以指定在验证过程中发生某种事件时可以执行的动作。将这些控制标志用方括号包括起来并由一系列的 value=action 所构成,每个值之间用空格分开,如:

sehll
[success=ok ignore=ignore module_unknown=ignore default=bad]

各value可参考 _pam_types.h 中提供的返回值:

txt
Where valueN corresponds to the return code from the function invoked in the module for which the line is defined. It is selected from one of these: success, open_err, symbol_err, service_err, system_err, buf_err, perm_denied, auth_err, cred_insufficient, authinfo_unavail, user_unknown, maxtries, new_authtok_reqd, acct_expired, session_err, cred_unavail, cred_expired, cred_err, no_module_data, conv_err, authtok_err, authtok_recover_err, authtok_lock_busy, authtok_disable_aging, try_again, ignore, abort, authtok_expired, module_unknown, bad_item, conv_again, incomplete, and default.
The last of these, default, implies 'all valueN's not mentioned explicitly. Note, the full list of PAM errors is available in /usr/include/security/_pam_types.h. The actionN can be: an unsigned integer, n, signifying an action of 'jump over the next n modules in the stack'; or take one of the following forms:

ignore

when used with a stack of modules, the module's return status will not contribute to the return code the application obtains.
bad
this action indicates that the return code should be thought of as indicative of the module failing. If this module is the first in the stack to fail, its status value will be used for that of the whole stack.
die
equivalent to bad with the side effect of terminating the module stack and PAM immediately returning to the application.
ok
this tells PAM that the administrator thinks this return code should contribute directly to the return code of the full stack of modules. In other words, if the former state of the stack would lead to a return of PAM_SUCCESS, the module's return code will override this value. Note, if the former state of the stack holds some value that is indicative of a modules failure, this 'ok' value will not be used to override that value.
done
equivalent to ok with the side effect of terminating the module stack and PAM immediately returning to the application.
reset
clear all memory of the state of the module stack and start again with the next stacked module.

模块路径 - module-path

表示指定的服务模块程序或配置文件所在的路径。 如果该指定内容在 /lib64/security/ 目录下,可直接使用相对路径,如 BMC 中自己新增的 pam_bmc_login 如果该指定内容在其它路径下,须指明绝对路径,如 BMC 中自维护的配置 /data/trust/pam_faillock

参数 - arguments

指定服务模块程序 SPI 中需要的参数,对应了 SPI 参数中的 argc 和 argv。一般来说每个 SPI 实现的参数都不相同,这个是可以由对应 SPI 的开发者自己指定的,也有一些 PAM 通用的参数:

  1. debug:该模块应当用syslog( )将调试信息写入到系统日志文件中。
  2. no_warn:该模块不应把警告信息发送给应用程序。
  3. use_first_pass:该模块不能提示用户输入密码,而应使用前一个模块从用户那里得到的密码。
  4. try_first_pass:该模块首先应当使用前一个模块从用户那里得到的密码,如果该密码验证不通过,再提示用户输入新的密码。
  5. use_mapped_pass:该模块不能提示用户输入密码,而是使用映射过的密码。
  6. expose_account:允许该模块显示用户的帐号名等信息,一般只能在安全的环境下使用,因为泄漏用户名会对安全造成一定程度的威胁。

模块结构

对整个框架来说,一次 PAM 调用由 pam_start() 开始,由 pam_end() 结束。正常来说,PAM 各服务模块的执行顺序由配置文件中编写的顺序来制定。执行对应服务时,若该服务配置了多个模块,这些模块就会堆叠起来(一个队列),按序执行并将结果存入 PAM 堆栈中,如下图所示。

PAM 应用程序

pam_start()

任何一个支持 PAM 的应用程序在进行认证时,必须以 pam_start() 开始进行初始化

c
#include <security/pam_appl.h>
Int
pam_start(
const char *service_name,
const char *user,
const struct pam_conv *pam_conversation,
pam_handle_t **pamh
);

service_name
应用的名字,这个名字参数非常重要。
当应用程序执行验证操作时,libpam 库会在 /etc/pam.d/ 下寻找以 service_name 命名的配置文件,该配置文件中指定了相关参数,其中包括将要调用哪个动态链接库进行验证。
配置文件的格式与前面讲的 /etc/pam.conf 的格式基本一致,只是去掉了 service-name 这一项,因为该配置文件的名字就是 service-name。
其实 linux 系统中 /etc/pam.conf 这个配置文件在整个 pam 框架中已经起不到什么作用,至少在 ubuntu10.04 系统中是否对这个配置文件进行配置,根本什么也不影响。

user
用户名,即 PAM 框架所作用的用户的名称。说白了就是你要对哪个用户进行 pam 验证,该用户就是此轮验证中被操作的对象。

&conv
对话函数 conv,用于提供 PAM 与用户或应用程序进程通信的通用方法。对话函数是必需的,因为 PAM 模块无法了解如何进行通信。通信可以采用 GUI、命令行、智能读卡器或其他设备等方式进行。这个对话函数是一个回调函数,这里只是对它的一个注册,接下来的验证过程中,应用程序和服务模块之间的所有信息交互都要通过它。对话函数需要应用程序的作者自己编写。

&pamh
PAM 句柄 pamh,即 PAM 框架用于存储有关当前操作信息的不透明句柄。成功调用 pam_start() 后将返回此句柄,后续调用 SPI 均会以该句柄作为上下文传递操作信息。

pam_end()

整个 PAM 验证过程中,应用程序最后调用的函数。从此以后句柄 pamh 不再有效,而且所有的占用的内存将被释放。

c
#include <security/pam_appl.h>
int
pam_end(
pam_handle_t *pamh,
int pam_status);

pamh
pam_start() 函数中创建的 PAM 句柄。

pam_status
应用程序在执行 pam_end() 前所执行的最后一个 pam API 函数的返回值。

PAM 服务模块

PAM API 与 SPI 对照

上面也说过,每次 PAM 认证由 pam_start() 开始,由 pam_end() 结束,期间做实际认证工作的 API 有6个(应用程序 API 不止6个),每个 API 都有对应的服务模块 SPI 作为回调进行实现。

管理模块应用程序 API服务模块 SPI
authpam_authenticate()pam_sm_authenticate()
authpam_setcred()pam_sm_setcred()
accountpam_acct_mgmt()pam_sm_acct_mgmt()
sessionpam_open_session()pam_sm_open_session()
sessionpam_close_session()pam_sm_close_session()
passwordpam_chauthtok()pam_sm_chauthtok()

API 函数原型为:

c
int
pam_xxx(
pam_handle_t *pamh, // 传递给 SPI 的参数,对应SPI参数中的 pamh
int flags           // 传递给 SPI 的参数,对应 SPI 参数中的 flags
);

SPI 函数原型为:

c
PAM_EXTERN int
pam_sm_xxx(
pam_handle_t *pamh, // 由 API 参数 pamh 传递
 int flags,         // 由 API 参数 flags 传递
 int argc,          // 由配置文件中 arguments 决定
 const char **argv  // 由配置文件中 arguments 传递
);

PAM SPI 编写

我们一般不会侵入去写 PAM 的 API,当需要进行定制时,应该自己写一个服务模块程序,实现对应的 SPI。需要注意的是,我们要写哪个模块的业务,就需要在程序中实现对应模块的 SPI 原型。 如:现在需要写一个服务模块 pam_test,要让它做用户身份鉴别的动作(也就是 auth)。那么在 pam_test 中必须实现 pam_sm_authenticate() 和 pam_sm_setcred() 两个接口,用于应用程序找到 auth 对应的 API pam_authenticate() 和 pam_setcred() 回调时能找到对应的接口。
同一个服务模块中可以声明多种不同的 SPI,让它作用于不同的管理模块。如上文中展示的 sshd 配置文件,其中 pam_bmc_login 同时用于 auth 和 account,也就是说在该程序中至少实现了 pam_sm_authenticate()、pam_sm_setcred() 和 pam_sm_acct_mgmt()三个接口。

一些 PAM 操作接口

PAM 中也有很多接口可用,这里就举例了几个比较常用的。

pam_get_item

c
#include <security/pam_modules.h>
int pam_get_item(
  const pam_handle_t *pamh,
  int item_type,
  const void **item
);

pam_get_item 接口用于从 PAM 句柄 pamh 中获取到一些认证过程中的值,有哪些内容具体可见链接。 例:BMC 中自己制定了一些登录接口限制、登录ip规则等,即可通过该接口中直接获取到对应的登录接口和ip,用来业务校验。

pam_set_item

c
#include <security/pam_modules.h>
int pam_set_item(
  const pam_handle_t *pamh,
  int item_type,
  const void **item
);

pam_set_item 接口与 pam_get_item 相对,可以向 pamh 中设置属性值。这个就基于使用者的需要了。 例:BMC 中对于 root 用户是转换为 <root> 用户进行管理的,所以在登录校验过程中会将对应的 user 重设为 <root> 后再进行后续执行。

pam_get_user

c
#include <security/pam_modules.h>
int pam_get_user(
  const pam_handle_t *pamh,
  const char **user,
  const char *prompt
);

pam_get_user 函数返回 pam_start() 指定的用户的名称。
如果未指定用户,它将返回 pam_get_item (pamh, PAM_USER, ...) 将返回的内容。
如果这是 NULL,它将通过 pam_conv() 机制获取用户名,它将使用以下列表中的第一个非 NULL 字符串提示用户:

  1. 传递给函数的提示参数。
  2. pam_get_item(pamh, PAM_USER_PROMPT, ...) 返回的内容
  3. 默认提示符:“login:”

无论通过何种方式获得用户名,指向它的指针都会作为 *user 的内容返回。请注意,此内存不应被模块释放或修改。 此函数设置与 pam_set_item() 和 pam_get_item() 函数关联的 PAM_USER 项。

结语

PAM 有很多非常自由的服务模块,通过一系列配置可以轻松达到一些认证限制、资源限制的要求。而且尝试代价很低,一个环境有多种认证方式,只改一种就算改坏了也不会破坏环境。(比如改坏 sshd 就用 login、ftp等形式登录进去把文件改回来就好了) 想要尝试可以参考以下步骤:

a. 使用现成的服务模块

  • 在应用程序的配置文件中加入一行该服务模块
  • 调整该服务模块的配置文件
  • 调用应用程序

b. 编写服务模块

  • 实现对应的 SPI,在接口中完成自己想做的业务动作
  • 编译成二进制
  • 在应用程序的配置文件中加入一行自己的服务模块
  • 调用应用程序