PostgreSQL 客户端认证机制

最近实验室数据库需要提高安全性,因为以前只有自己人用,基本上是在裸奔,知道端口号和IP就可以连接上,不需要任何认证信息,没有安全可言。最简单的认证方式就是用户名和密码,但平常使用PG的时候都没有用到,就研究了一下PG的认证方式,取取经。

配置与使用

PG的认证方式有很多种,而且对于不同的database,不同的user,不同的访问IP都可以选择不同的方式认证。具体在配置文件pg_hba.conf中说明。

local           DATABASE  USER  METHOD  [OPTIONS]

host           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]

hostssl      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]

hostnossl  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]

 

配置的时候大写字母要被对应的内容所取代。

其中local匹配本地Unix域套接字访问。host 对应TCP/IP的链接认证,即可以匹配SSL链接请求,也可以匹配非SSL链接请求。hostssl 匹配使用TCP/IP的SSL链接请求,如果需要PG支持SSL,需要在客户端和服务器端安装openSSL,并且在编译阶段使用 –with-openssl,具体配置参考这里。Hostnossl匹配普通的TCP/IP 链接。

后面内容与字面描述一致,ADDRESS使用CIDR-address(IP/掩码)格式,声明匹配的客户端IP地址范围。METHOD是具体的认证方式,OPTION是具体认证方式可以配置的选项,以name-value的形式出现。例如:

local   all               all   127.0.0.1/32          md5

host   postgres    all   192.168.12.10/32  md5

host   all               all   192.168.0.0/16      ident map=omicron

 

第一个表示本地使用Unix域套接字访问,任何用户访问任何数据库的认证方式都是md5。安装PG后对本地的访问默认是trust, 更改为md5后使用psql登录:

postgres@ubuntu:~$ psql
Password:

默认登陆的是postgres数据库,初始是没有密码的,输入任何密码都会被拒绝,需要在后台设置密码:

alter role postgres password 'your password';

密码会使用md5加密保存,并且会在系统内部随机生成一个四位大小的随机数,和明文密码一起经过md5加密,为了提高破解的难度,也称作“加盐”。

ident 认证方式是比较登录PG的用户和登录客户端的用户名是否相同,假设使用用户名messi登录了linux,在此账户下访问PG,如果访问的数据库用户不是messi,就不允许登陆。但是大多数情况下,两个用户名并不相同,所以PG提供了另一个映射文件:pg_ident.conf ,其中内容格式如下:

map-name     system-username      database-username

omicron          iniesta                            messi

 

omicron 是map的名字,在上述第三个例子中定义ident,此时登录OS的用户是iniesta,但可以访问PG中用户为messi的数据库。如果system-username字段以一个反斜杠(/)开始, 那么该字段的剩余部分被作为一个规则表达式对待。可以参看PG的正则表达式语法。除了md5,ident认证方式,还有其它多种认证方式,请参看官方文档

 源码解析

当postmaster启动的时候,会读取并解析上述两个配置文件,分别调用load_hba() load_ident() 两个函数,这两个函数会逐行的解析里面的内容,每一行的内容都有一个对应的数据结构来描述,分别是:

typedef struct IdentLine
{
	int		linenumber;        /*记录行号*/
	char	   *usermap;        /*map名*/
	char	   *ident_user;     /*认证的用户,也就是需要映射的OS登录用户名*/
	char	   *pg_role;        /*pg 角色名*/
	regex_t		re;         /*正则表达式*/
} IdentLine;
typedef struct HbaLine
{
	int		linenumber;    /*记录行号*/
	char	   *rawline;           /*原本的内容*/
	ConnType	conntype;      /*链接类型*/
	List	   *databases;         
	List	   *roles;      
	struct sockaddr_storage addr; /*IP地址范围*/
	struct sockaddr_storage mask; /*子网掩码*/
	IPCompareMethod ip_cmp_method;
	char	   *hostname;
	UserAuth	auth_method;  /*认证方式 md5 ident等*/

	char	   *usermap;          /*ident map名*/
	char	   *pamservice;       /*PAM服务*/
	bool		ldaptls;      /*ldap中是否使用TLS*/
	char	   *ldapserver;       
	int		ldapport;
	char	   *ldapbinddn;
	char	   *ldapbindpasswd;
	char	   *ldapsearchattribute;
	char	   *ldapbasedn;
	int			ldapscope;
	char	   *ldapprefix;
	char	   *ldapsuffix;
	bool		clientcert;     /*ssl客户端证书认证*/
	char	   *krb_server_hostname; /*krb 服务器名*/
	char	   *krb_realm;
	bool		include_realm;
	char	   *radiusserver;      /*radi服务名*/
	char	   *radiussecret;
	char	   *radiusidentifier;
	int			radiusport;
} HbaLine;

逐行解析并放入这两个数据结构中,存放内容的空间会在特定的内存上下文中。解析完毕后会用PG中的List结构把每一行组成一个链表,分别会有静态全局变量指向链表头,以备后续认证访问。当postmaster正在运行,如果想改变认证方式,修改配置文件后需要发送SIGHUP信号,或者执行pg_ctl -D ./DATADIR reload命令,会重新读取配置文件。

具体的认证行为在postgres进程中,因为PG的多进程架构,当新的链接请求到达pastmaster之后,会fork一个postgres进程进行处理,上述解析的内容也会从postmaster继承过来,但是postmaster重新读配置文件解析后,却不会影响已经执行的postgres,大多数情况postgres正在运行说明已经通过了验证,也不用理会认证方式的改变。一个新链接包含的内容保存在 Port 这个数据结构中,也有一个全局的指针指向它,在认证过程中与客户端的通信都是基于Port中的文件描述符。postgres也许并不需要关心此链接是一个本地的Unix域套接字,还是网络Socket。也把Port这个结构贴出来,源码里面的注释已经挺明白的:

typedef struct Port
{
	pgsocket	sock;			/* File descriptor */
	bool		noblock;		/* is the socket in non-blocking mode? */
	ProtocolVersion proto;		/* FE/BE protocol version */
	SockAddr	laddr;			/* local addr (postmaster) */
	SockAddr	raddr;			/* remote addr (client) */
	char	   *remote_host;	/* name (or ip addr) of remote host */
	char	   *remote_hostname;/* name (not ip addr) of remote host, if
								 * available */
	int			remote_hostname_resolv; /* see above */
	char	   *remote_port;	/* text rep of remote port */
	CAC_state	canAcceptConnections;	/* postmaster connection status */

	/*
	 * Information that needs to be saved from the startup packet and passed
	 * into backend execution.  "char *" fields are NULL if not set.
	 * guc_options points to a List of alternating option names and values.
	 */
	char	   *database_name;
	char	   *user_name;
	char	   *cmdline_options;
	List	   *guc_options;

	/*
	 * Information that needs to be held during the authentication cycle.
	 */
	HbaLine    *hba;
	char		md5Salt[4];		/* Password salt */

	/*
	 * Information that really has no business at all being in struct Port,
	 * but since it gets used by elog.c in the same way as database_name and
	 * other members of this struct, we may as well keep it here.
	 */
	TimestampTz SessionStartTime;		/* backend start time */

	/*
	 * TCP keepalive settings.
	 *
	 * default values are 0 if AF_UNIX or not yet known; current values are 0
	 * if AF_UNIX or using the default. Also, -1 in a default value means we
	 * were unable to find out the default (getsockopt failed).
	 */
	int			default_keepalives_idle;
	int			default_keepalives_interval;
	int			default_keepalives_count;
	int			keepalives_idle;
	int			keepalives_interval;
	int			keepalives_count;

#if defined(ENABLE_GSS) || defined(ENABLE_SSPI)

	/*
	 * If GSSAPI is supported, store GSSAPI information. Otherwise, store a
	 * NULL pointer to make sure offsets in the struct remain the same.
	 */
	pg_gssinfo *gss;
#else
	void	   *gss;
#endif

	/*
	 * SSL structures (keep these last so that USE_SSL doesn't affect
	 * locations of other fields)
	 */
#ifdef USE_SSL
	SSL		   *ssl;
	X509	   *peer;
	char	   *peer_cn;
	unsigned long count;
#endif

	/* This field will be in a saner place in 9.4 and up */
	int			remote_hostname_errcode;		/* see above */
} Port;

可以看到,和客户端认证相关的除了基本的信息,比如要访问的数据库,客户端的地址,用户名等,里面包含了一个HbaLine 指针和 md5Salt。认证的入口是InitProcess函数,最终会调用ClientAuthentication,首先根据访问信息去匹配满足要求的HbaLine,如果匹配不到就会报错并退出数据库。接着验证客户端证书(如果有),然后就根据认证方式进入不同的认证方式分支。认证过程会进行多次通信。最终得到认证结果选择是否中断链接。

以md5认证方式为例,第一步需要组装信息发送给客户端,要求客户端输入密码。具体发送消息的格式如下:

Type(‘R’char) Type(MD5 int) Salt(char[4])

 

一个值为‘R’的char,表示需要客户端回应,接着是认证的类型,客户端根据类型做不同信息相应,最后是MD5特有的“盐”值,需要从数据库中取出来,发送给客户端,让客户端完成加密工作。第二步就是等待客户端发送过来的密码,与数据库的密码进行比对,完成验证。这里最重要的就是客户端需要统一信息发送接收和解析的格式。

总结

PG的多进程架构让认证流程变得很清晰,项目中采用多线程+epoll处理多个链接,逻辑就会稍微复杂一点。如果SSL加密可靠,似乎可以在网络上传送密码的明文,所有加密工作可以放到数据库端来做,这样客户端就少了一道加密的工作。关于ident认证方式,某种程度上借用了OS登录认证的保护,值得借鉴。

参考

PostgreSQL 数据库内核分析

http://www.jb51.net/article/40300.htm

Tags:

About zhangxiaojian