searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

PostgreSQL数据库安全(一)

2023-08-30 02:19:36
92
0

数据库安全

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函数的执行流程如下所示:

  1. 调用函数hba_getauthmethod,检查客户端地址、所连接数据库、用户名在文件HBA中是否由能匹配的HBA记录。如果能找到匹配的HBA记录,则将Port结构中的相关认证方法的字段设置为HBA记录中的参数,同时返回状态值STATUS_OK。理论上这个过程不会返回STATUS_ERROR。

  2. 如果在编译时选择了使用SSL,在这里想要检查客户端是否已提供一个有效的证书(通过Port结构中的hba字段的clientcert字段的值来判断)。

  3. 根据不同的认证方法,进行相应的认证过程。

  4. 在认证过程中可能需要和客户端进行多次交互。最后返回值如果为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。

0条评论
0 / 1000
h****n
4文章数
0粉丝数
h****n
4 文章 | 0 粉丝
h****n
4文章数
0粉丝数
h****n
4 文章 | 0 粉丝
原创

PostgreSQL数据库安全(一)

2023-08-30 02:19:36
92
0

数据库安全

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函数的执行流程如下所示:

  1. 调用函数hba_getauthmethod,检查客户端地址、所连接数据库、用户名在文件HBA中是否由能匹配的HBA记录。如果能找到匹配的HBA记录,则将Port结构中的相关认证方法的字段设置为HBA记录中的参数,同时返回状态值STATUS_OK。理论上这个过程不会返回STATUS_ERROR。

  2. 如果在编译时选择了使用SSL,在这里想要检查客户端是否已提供一个有效的证书(通过Port结构中的hba字段的clientcert字段的值来判断)。

  3. 根据不同的认证方法,进行相应的认证过程。

  4. 在认证过程中可能需要和客户端进行多次交互。最后返回值如果为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。

文章来自个人专栏
PG
4 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
1
0