数据库安全
PostgreSQL安全机制体系
PostgreSQL首先通过用户标识和认证来验证访问数据库的用户身份,判断其是否为合法用户以及是否有权限访问数据库资源。然后通过基于角色的访问控制(Role Based Access Control,RBAC),并使用存取控制列表(ACL)方法控制请求和保护信息。
一.用户标识和认证
客户端连接认证过程:
1)客户端和服务器端的Postmaster进程建立连接;
2)客户端发送请求信息到守护进程Postmaster;
3) Postmaster根据请求信息检查文件pg_hba.conf是否允许客户端连接,并把认证方式和必要信息发送到客户端;
4)客户端根据收到的不同认证方式,发送相应的认证消息给Postmaster;
5) Postermaster调用认证模块对客户端送来的认证消息进行验证。如果认证通过,初始化一个Postgres进程与客户端进行通信;否则拒绝继续会话,关闭连接。
1.客户端配置文件
pg_hba.conf文件是以文本文件的方式存储的,其中的内容是一组HBA记录的集合,文件中的每一行代表一条记录。HBA记录是以下五种格式之一:
# local DATABASE USER METHOD [OPTIONS]
# host DATABASE USER ADDRESS METHOD [OPTIONS]
# hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS]
第一个字段是连接类型:
-
local是Unix域套接字
-
host是TCP/IP套接字
-
hostssl是经过SSL加密的TCP/IP套接字
-
hostnossl是没有经过SSL加密的TCP/IP套接字
-
hostgssenc是经过GSSAPI加密的TCP/IP套接字
-
hostnogssenc是没有经过GSSAPI加密的TCP/IP套接字
DATABASE:声明记录所匹配的数据库名称。有四种预定义的值, "all", "sameuser", "samerole", "replication"。
USER:声明记录所匹配的数据库用户。可以是all、用户名、前缀为“+”的组名或逗号分隔的列表。在DATABASE和USER字段中还可以编写一个以“@”为前缀的文件名,以包含名称从一个单独的文件。
ADDRESS:指定记录匹配的主机集。
METHOD:表示通过这条记录连接所使用的认证方法。有13种方式:“trust"、“reject"、“md5"、“password"、“scramd-sha-256"、“gss"、“sspi"、“ident"、“peer"、“pam"、“ldap"、“radius"或“cert"。
OPTIONS:是一组用于身份验证的选项,格式为NAME=VALUE。
如果要允许非本地连接,则需要添加更多的host记录。在这种情况下,还需要通过listen_addresss配置参数,或者通过-i或-h命令行开关,使PostgreSQL在非本地接口上侦听。
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all scram-sha-256
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections:
host all all ::1/128 scram-sha-256
# Allow replication connection23s from localhost, by a user with the
# replication privilege.
local replication all scram-sha-256
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
2.认证方法
信任(Trust)认证、口令认证、Kerberos认证、GSSAPI及SSPI认证、LDAP认证、PAM认证、cert认证
3.客户端认证
Postmaster接收到一个用户的连接请求之后会调用ClientAuthentication
函数进行客户端认证。在进行认证之前,首先需要读取配置文件pg_hba.conf 和 pg_ident.conf。前者决定认证的方法,后者用于初始化Ident认证的用户名映射。如果HBA文件加载失败,将直接报错。如果装载成功,则设置一个认证超时定时器,默认为60秒,超过时间将提示认证超时。
读取HBA文件的工作由load_hba
函数实现,它的功能是按行解析HBA文件中的数据,并将其保存在一个节点类型为HbaLine的链表结构里。
typedef struct HbaLine
{
int linenumber;//记录行号
char *rawline;
ConnType conntype;//连接类型
List *databases;//数据库名称
List *roles;//角色名,可以是用户或用户组
struct sockaddr_storage addr;//IP地址范围
struct sockaddr_storage mask;//子网掩码,和IP一起使用
IPCompareMethod ip_cmp_method;
char *hostname;
UserAuth auth_method;//认证方法
char *usermap;//用来存储ident认证的用户名
char *pamservice;//PAM认证的服务名
bool pam_use_hostname;
bool ldaptls;//表示在LDAP中是否使用TLS
char *ldapscheme;
char *ldapserver;//LDAP服务器
int ldapport;//LDAP端口
char *ldapbinddn;//LDAP绑定的用户名
char *ldapbindpasswd;
char *ldapsearchattribute;
char *ldapsearchfilter;
char *ldapbasedn;
int ldapscope;
char *ldapprefix;
char *ldapsuffix;
ClientCertMode clientcert;//是否可以进行证书认证
char *krb_realm;
bool include_realm;
bool compat_realm;
bool upn_username;
List *radiusservers;
char *radiusservers_s;
List *radiussecrets;
char *radiussecrets_s;
List *radiusidentifiers;
char *radiusidentifiers_s;
List *radiusports;
char *radiusports_s;
int addrlen; /* zero if we don't have a valid addr */
int masklen; /* zero if we don't have a valid mask */
} HbaLine;
load_hba
/* Now parse all the lines */
Assert(PostmasterContext);
hbacxt = AllocSetContextCreate(PostmasterContext,
"hba parser context",
ALLOCSET_SMALL_SIZES);
oldcxt = MemoryContextSwitchTo(hbacxt);
foreach(line, hba_lines)
{
TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
HbaLine *newline;
/* don't parse lines that already have errors */
if (tok_line->err_msg != NULL)
{
ok = false;
continue;
}
if ((newline = parse_hba_line(tok_line, LOG)) == NULL)
{
/* Parse error; remember there's trouble */
ok = false;
/*
* Keep parsing the rest of the file so we can report errors on
* more than the first line. Error has already been logged, no
* need for more chatter here.
*/
continue;
}
new_parsed_lines = lappend(new_parsed_lines, newline);
}
......
/* Loaded new file successfully, replace the one we use */
if (parsed_hba_context != NULL)
MemoryContextDelete(parsed_hba_context);
parsed_hba_context = hbacxt;
parsed_hba_lines = new_parsed_lines;
return true;
客户端认证的过程通过调用ClientAuthentication
函数完成,该函数只有一个类型为Port的参数,Port结构中存储着客户端的相关信息,如客户端主机名、端口、用户名、数据库名等。Port结构与客户端认证相关的部分字段如下所示:
typedef struct Port
{
......
/*
* Information that needs to be held during the authentication cycle.
*/
HbaLine *hba;
......
} Port;
ClientAuthentication
函数的执行流程如下所示:
-
调用函数
hba_getauthmethod
,检查客户端地址、所连接数据库、用户名在文件HBA中是否由能匹配的HBA记录。如果能找到匹配的HBA记录,则将Port结构中的相关认证方法的字段设置为HBA记录中的参数,同时返回状态值STATUS_OK。理论上这个过程不会返回STATUS_ERROR。 -
如果在编译时选择了使用SSL,在这里想要检查客户端是否已提供一个有效的证书(通过Port结构中的hba字段的clientcert字段的值来判断)。
-
根据不同的认证方法,进行相应的认证过程。
-
在认证过程中可能需要和客户端进行多次交互。最后返回值如果为STATUS_OK,则表示认证成功,并将认证成功信息发送回客户端;否则发送认证失败信息。
void
ClientAuthentication(Port *port)
{
int status = STATUS_ERROR;
char *logdetail = NULL;
/*
* 获取用于此客户端/数据库组合的身份验证方法。注意:此时我们不解析文件;load_hba已经做了。如果解析hba配置文件失败,hba.c会在服务器日志文件中丢弃一条错误消息。
*/
hba_getauthmethod(port);
CHECK_FOR_INTERRUPTS();
//验证证书
if (port->hba->clientcert != clientCertOff)
{
/* If we haven't loaded a root certificate store, fail */
if (!secure_loaded_verify_locations())
ereport(FATAL,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("client certificates can only be checked if a root certificate store is available")));
if (!port->peer_cert_valid)
ereport(FATAL,
(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
errmsg("connection requires a valid client certificate")));
}
/*
* 现在继续进行实际的验证方式进行检查
*/
switch (port->hba->auth_method)
{
......
}
if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)
|| port->hba->auth_method == uaCert)
{
/*
*确保只有在使用cert方法或验证完整选项时才检查证书
*/
#ifdef USE_SSL
status = CheckCertAuth(port);
#else
Assert(false);
#endif
}
if (ClientAuthentication_hook)
(*ClientAuthentication_hook) (port, status);
if (status == STATUS_OK)
sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
else
auth_failed(port, status, logdetail);
}
void
hba_getauthmethod(hbaPort *port)
{
check_hba(port);
}
---------------------------------------------------
//扫描预先解析的hba文件,查找与端口的连接请求匹配的文件。
static void
check_hba(hbaPort *port)
{
Oid roleid;
ListCell *line;
HbaLine *hba;
//获取目标角色的OID
roleid = get_role_oid(port->user_name, true);
foreach(line, parsed_hba_lines)
{
//检查各种连接类型
......
/* Check database and role */
if (!check_db(port->database_name, port->user_name, roleid,
hba->databases))
continue;
if (!check_role(port->user_name, roleid, hba->roles))
continue;
/* Found a record that matched! */
port->hba = hba;
return;
}
/* If no matching entry was found, then implicitly reject. */
hba = palloc0(sizeof(HbaLine));
hba->auth_method = uaImplicitReject;
port->hba = hba;
}
在认证过程中,服务器要和客户端交互认证相关的信息,有时不止一次。系统通过函数SendAuthRequest
向客户端发送认证请求。
/*
* Send an authentication request packet to the frontend.
*/
static void
sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, int extralen)
{
//初始化一个buf
StringInfoData buf;
//中断检测
CHECK_FOR_INTERRUPTS();
//pq_beginmessage - initialize for sending a message
pq_beginmessage(&buf, 'R');
//将请求转化为二进制加入buf
pq_sendint32(&buf, (int32) areq);
if (extralen > 0)
//将数据附加到StringInfo缓冲区
pq_sendbytes(&buf, extradata, extralen);
//将buf内的信息发送给客户端
pq_endmessage(&buf);
/*
* 刷新消息,以便客户端将看到它,但AUTH_REQ_OK和AUTH_REQ.ASL_IN除外。
* 它们在我们准备好进行查询之前不需要发送。
*/
if (areq != AUTH_REQ_OK && areq != AUTH_REQ_SASL_FIN)
pq_flush();
CHECK_FOR_INTERRUPTS();
}
发送的请求信息中可能包括不同的认证信息,如下表所示:
/* 这些是后端发送的身份验证请求代码。 */
#define AUTH_REQ_OK 0 /* User is authenticated */
#define AUTH_REQ_KRB4 1 /* Kerberos V4. Not supported any more. */
#define AUTH_REQ_KRB5 2 /* Kerberos V5. Not supported any more. */
#define AUTH_REQ_PASSWORD 3 /* Password */
#define AUTH_REQ_CRYPT 4 /* crypt password. Not supported any more. */
#define AUTH_REQ_MD5 5 /* md5 password */
#define AUTH_REQ_SCM_CREDS 6 /* transfer SCM credentials */
#define AUTH_REQ_GSS 7 /* GSSAPI without wrap() */
#define AUTH_REQ_GSS_CONT 8 /* Continue GSS exchanges */
#define AUTH_REQ_SSPI 9 /* SSPI negotiate without wrap() */
#define AUTH_REQ_SASL 10 /* Begin SASL authentication */
#define AUTH_REQ_SASL_CONT 11 /* Continue SASL authentication */
#define AUTH_REQ_SASL_FIN 12 /* Final SASL message */
typedef uint32 AuthRequest;
PostgreSQL提供多种不同的客户端认证方法,这里以MD5认证为例描述MD5认证的过程。
1)调用函数sendAuthRequest
发送一个MD5类型的认证请求AUTH_REQ_MD5。
2)在sendAuthRequest
中除了发送认证请求信息之外,还将调用pq_sendbytes
函数将长度为4位的随机数md5Salt发送给客户端,并等待客户端回应。
3)客户端将通过用户OID、密码和md5Salt进行MD5加密,并将结果作为认证信息再次发送给客户端。
4)服务器端通过函数recv_password_packet
获取客户端返回的结果。如果结果值为空,表明客户端没有发送密钥,返回STATUS_EOF;如果结果值不为空,调用md5_crypt_verify
,获取从系统表pg_authid中取出的用户的密码,同样将其和用户OID、随机数md5Salt一起进行MD5加密。将服务器端计算的结果和客户端提交的结果进行比较,如果相等则返回STATUS_OK。
5)如果返回值为STATUS_OK,则执行函数sendAuthRequest
将认证成功信息发送到客户端,否则执行函数auth_failed
将认证失败信息返回。
//从客户端收集密码响应报文。如果无法获取密码,则返回NULL,否则返回字符串。
static char *
recv_password_packet(Port *port)
{
StringInfoData buf;
//开始读取来自客户端的消息
pq_startmsgread();
......
initStringInfo(&buf);
if (pq_getmessage(&buf, 1000)) /* receive password */
{
/* EOF - pq_getmessage already logged a suitable message */
pfree(buf.data);
return NULL;
}
if (strlen(buf.data) + 1 != buf.len)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("invalid password packet size")));
if (buf.len == 1)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PASSWORD),
errmsg("empty password returned by client")));
/* Do not echo password to logs, for security. */
elog(DEBUG5, "received password packet");
return buf.data;
}
二、基于角色的权限管理
PostgreSQL实现了基于角色的访问控制机制。借助角色机制,当给一组权限相同的用户授权时,不需要对这些用户逐一授权,只要把权限授予角色,再将角色授予这组用户即可。此后只要针对角色进行管理就可以了。同理,如果一组权限要改变,只需改变角色的权限,此时该角色所包含的所有成员的权限就会被自动修改。
1.用户和角色
一般来说,一个角色可以视为一个数据库用户,或者一组数据库用户。对于PostgreSQL来说,用户和角色是完全两个相同的对象,唯一不同的是在创建时角色的缺省值中没有LOGIN权限。
PostgreSQL系统中的权限分为两种:系统权限和对象权限。系统权限是指系统规定用户使用数据库的权限(如连接数据库、创建数据库、创建用户等)。对象权限是指在表、序列、函数等数据库对象上执行特殊动作的权限,其权限类型有SELECT、INSERT、UPDATE、DELETE、REFERENCES、TRIGGER、CREATE、CONNECT、TEMPORARY、EXECUTE和USAGE等。
每个角色都包含一组属性,这些属性定义了该角色的系统权限,以及与客户端认证系统的交互。这些角色属性在创建角色时可以使用SQL语言命令CREATE ROLE直接进行设定,也可以在设定以后使用SQL命令ATER ROLE来修改。角色属性包括:
PostgreSQL的用户构建过程如下所示,超级用户拥有任何权限,可以创建其他超级用户,也可以创建其他角色(如拥有一定权限的用户或组),或者创建数据库对象等。以超级用户(Postgres)为根节点,不断创建其他超级用户或其他角色等节点。系统中的角色从逻辑上构成了一颗以超级用户为根的树。
2.角色相关的系统表
有关角色的属性信息可以在系统表pg_authid中找到。另外,系统表pg_roles是系统表pg_authid公开可读部分的视图(口令域为空白),它提供了访问数据库角色有关信息的接口。
系统表pg_auth_menbers存储了角色之间的成员关系,一般使用GRANT/REVOKE命令在该表上进行添加/删除角色成员操作。
例子:
执行命令CREATE ROLE创建三个拥有不同权限的角色,即在系统表pg_authid中插入三个元组。
执行GRANT命令赋予角色间的成员关系,即在存储角色成员关系的系统表pg_auth_members中插入两个元组。
3.角色管理
PostgreSQL中提供了对角色的各种管理操作,如角色的创建、修改、删除、授权、回收等。
(1).创建角色
CREATE ROLE name [[WITH] option[...]]
其中option可以是:
创建角色通过函数CreateRole
实现,该函数有一个类型ParseState的参数和一个为CreateRoleStmt的参数。其中ParseState是在解析分析期间的状态信息,创建角色主要和CreateRoleStmt有关。
typedef struct CreateRoleStmt
{
NodeTag type;
RoleStmtType stmt_type; /* 将要创建的角色类型ROLE/USER/GROUP */
char *role; /* 角色名 */
List *options; /* 角色属性列表,DefElem结构体 */
} CreateRoleStmt;
typedef struct DefElem
{
NodeTag type;
char *defnamespace; /* 节点对应的命名空间 */
char *defname; //节点对应的角色属性名
Node *arg; /* 表示值或类型名*/
DefElemAction defaction; /* SET/ADD/DROP等行为 */
int location; /* 令牌位置, or -1 if unknown */
} DefElem;
/*
* CREATE ROLE
*/
Oid
CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
{
......
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
case ROLESTMT_ROLE:
break;
case ROLESTMT_USER:
canlogin = true;
/* may eventually want inherit to default to false here */
break;
case ROLESTMT_GROUP:
break;
}
/* 从语句节点树中提取选项 */
foreach(option, stmt->options)
{
DefElem *defel = (DefElem *) lfirst(option);
//属性名对比,进行相应赋值操作
if (strcmp(defel->defname, "password") == 0)
{
if (dpassword)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("conflicting or redundant options"),
parser_errposition(pstate, defel->location)));
dpassword = defel;
}
else if (strcmp(defel->defname, "sysid") == 0)
{
ereport(NOTICE,
(errmsg("SYSID can no longer be specified")));
}
......
}
//判断值是否已经取到,转化为准确值
if (dpassword && dpassword->arg)
password = strVal(dpassword->arg);
if (dissuper)
issuper = intVal(dissuper->arg) != 0;
......
//首先检查一些权限
if (issuper)
{
if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
}
else if (isreplication)
{
if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create replication users")));
}
......
//检查用户没有尝试在保留的“pg_”命名空间中创建角色。
if (IsReservedName(stmt->role))
ereport(ERROR,
(errcode(ERRCODE_RESERVED_NAME),
errmsg("role name \"%s\" is reserved",
stmt->role),
errdetail("Role names starting with \"pg_\" are reserved.")));
......
//检查pg_authid关系,确保角色不存在。
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_authid_dsc = RelationGetDescr(pg_authid_rel);
if (OidIsValid(get_role_oid(stmt->role, true)))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("role \"%s\" already exists",
stmt->role)));
......
//构建要插入的元组
MemSet(new_record, 0, sizeof(new_record));
MemSet(new_record_nulls, false, sizeof(new_record_nulls));
new_record[Anum_pg_authid_rolname - 1] =
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
......
//在pg_authid表中插入记录
CatalogTupleInsert(pg_authid_rel, tuple);
/*
* Add the new role to the specified existing roles.
*/
if (addroleto)
{
RoleSpec *thisrole = makeNode(RoleSpec);
List *thisrole_list = list_make1(thisrole);
List *thisrole_oidlist = list_make1_oid(roleid);
thisrole->roletype = ROLESPEC_CSTRING;
thisrole->rolename = stmt->role;
thisrole->location = -1;
foreach(item, addroleto)
{
RoleSpec *oldrole = lfirst(item);
HeapTuple oldroletup = get_rolespec_tuple(oldrole);
Form_pg_authid oldroleform = (Form_pg_authid) GETSTRUCT(oldroletup);
Oid oldroleid = oldroleform->oid;
char *oldrolename = NameStr(oldroleform->rolname);
//增加成员角色
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
GetUserId(), false);
ReleaseSysCache(oldroletup);
}
}
......
}
(2).修改角色属性
如果要修改一个数据库角色,可以使用SQL语言命令ATER ROLE。该命令可以表示三种情况:修改角色属性、重命名角色或者为特定的配置变量修改角色的会话缺省值。其语法命令为:
(3).删除角色
如果要删除一个数据库角色,可以使用SQL命令DROP ROLE。
DROP ROLE
(4).授权和回收角色
GRANT/REVOKE role [...] TO username[...] [WITH ADMIN OPTION]
(5).删除角色的数据库对象授权
如果要删除一个数据库角色所拥有的数据库对象授权,可以使用SQL命令DROP OWNED。
(6).修改数据库对象属主
如果要修改数据库对象的属主,则可以使用SQL命令REASSIGN OWNED。