PostgreSQL 如何提升LDAP或AD域认证的可用性

5 minute read

背景

PostgreSQL 如何配置AD域认证或LDAP认证,请参考:

http://blog.163.com/digoal@126/blog/static/16387704020145914717111/

http://blog.163.com/digoal@126/blog/static/1638770402014563264469/

引入LDAP,AD认证,可能会增加故障点,当认证服务器出现故障时,认证将失败。

本文主要介绍一下PostgreSQL是如何解决这个问题的,以及部分代码的分析。

当用户选择了使用AD域或者LDAP进行认证时,可以选择使用单机或多机的配置,多机主要是为了防止LDAP,AD认证服务器的单点故障。

单机模式

# simple bind :     
host all new 0.0.0.0/0 ldap ldapserver=172.16.3.150 ldapport=389 ldapprefix="uid=" ldapsuffix=",ou=People,dc=my-domain,dc=com"    
# search bind :     
host all new 0.0.0.0/0 ldap ldapserver=172.16.3.150 ldapport=389 ldapsearchattribute="uid" ldapbasedn="ou=People,dc=my-domain,dc=com"    

多机模式,使用空格隔开,可以在ldapserver中设置,覆盖ldapport中的设置。

# simple bind :     
host all new 0.0.0.0/0 ldap ldapserver="172.16.3.150 172.16.3.151:388 10.1.1.1" ldapport=389 ldapprefix="uid=" ldapsuffix=",ou=People,dc=my-domain,dc=com"    
# search bind :     
host all new 0.0.0.0/0 ldap ldapserver="172.16.3.150 172.16.3.151:388 10.1.1.1" ldapport=389 ldapsearchattribute="uid" ldapbasedn="ou=People,dc=my-domain,dc=com"    

防止LDAP,AD认证服务器的单点故障还有一种解法,使用域名。但是也有一些注意事项,如下:

在域名服务器中为一个域名配置多台主机地址,这是非常惯用的手法,但是这种方法也有一定的问题。

例如某个企业在全国各地的IDC机房都有对应的AD服务器,使用域名的方式,如果将这些AD服务器的IP都指给一个域名,在DNS响应gethostbyname请求时,一般是以轮询的方式返回列表。

例如:

某次请求返回

IP_A, IP_B, IP_C    

当本地的DNS cache TTL超时后,接下来的请求可能返回

IP_B, IP_C, IP_A    

客户端在拿到这些地址信息后,通常取的是第一个IP hostent->h_addr_list[0] 作为解析出来的IP拿来使用。

那么就存在一个问题,在进行AD域认证时,可能有时候取到的是本IDC的AD域服务器,有时候取到的是其他IDC的AD域服务器。

怎么让DNS返回的就是本地IDC的AD域服务器呢?

常用的手法是使用智能DNS,根据来源IP,返回地址。

gethostbyname代码:

NAME    
       gethostbyname, gethostbyaddr, sethostent, gethostent, endhostent, h_errno, herror, hstrerror, gethostbyaddr_r, gethostbyname2, gethostbyname2_r, gethostbyname_r, gethostent_r - get network host entry    
  
SYNOPSIS    
       #include <netdb.h>    
       extern int h_errno;    
  
       struct hostent *gethostbyname(const char *name);    
......    
  
       The hostent structure is defined in <netdb.h> as follows:    
  
           struct hostent {    
               char  *h_name;            /* official name of host */    
               char **h_aliases;         /* alias list */    
               int    h_addrtype;        /* host address type */    
               int    h_length;          /* length of address */    
               char **h_addr_list;       /* list of addresses */    
           }    
           #define h_addr h_addr_list[0] /* for backward compatibility */    
  
       The members of the hostent structure are:    
  
       h_name The official name of the host.    
  
       h_aliases    
              An array of alternative names for the host, terminated by a NULL pointer.    
  
       h_addrtype    
              The type of address; always AF_INET or AF_INET6 at present.    
  
       h_length    
              The length of the address in bytes.    
  
       h_addr_list    
              An array of pointers to network addresses for the host (in network byte order), terminated by a NULL pointer.    
  
       h_addr The first address in h_addr_list for backward compatibility.    

src/backend/libpq/auth.c

/*    
 * Initialize a connection to the LDAP server, including setting up    
 * TLS if requested.    
 */    
static int    
InitializeLDAPConnection(Port *port, LDAP **ldap)    
{    
        int                     ldapversion = LDAP_VERSION3;    
        int                     r;    
  
        *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);    
        if (!*ldap)    
        {    
#ifndef WIN32    
                ereport(LOG,    
                                (errmsg("could not initialize LDAP: %m")));    
#else    
                ereport(LOG,    
                                (errmsg("could not initialize LDAP: error code %d",    
                                                (int) LdapGetLastError())));    
man ldap_init  
  
NAME    
       ldap_init, ldap_initialize, ldap_open - Initialize the LDAP library and open a connection to an LDAP server    
  
SYNOPSIS    
       #include <ldap.h>    
  
       LDAP *ldap_open(host, port)    
       char *host;    
       int port;    
  
       LDAP *ldap_init(host, port)    
       char *host;    
       int port;    
  
DESCRIPTION    
       ldap_open() opens a connection to an LDAP server and allocates an LDAP structure which is used to identify the connection and to maintain per-connection information.      
       ldap_init() allocates an LDAP structure but does not open an initial connection.      
       ldap_initialize() allocates an LDAP structure but does not open an initial connection.      
       ldap_init_fd() allocates an LDAP structure using an existing  connection on the provided socket.      
       One of these routines must be called before any operations are attempted.    
  
       ldap_open()  takes  host, the hostname on which the LDAP server is running, and port, the port number to which to connect.      
       If the default IANA-assigned port of 389 is desired, LDAP_PORT should be specified for port.      
  
       The host parameter may contain a blank-separated list of hosts to try to connect to, and each host may optionally by of the form host:port.      
       If present, the :port overrides the port parameter  to ldap_open().       
  
       Upon  successfully  making a connection to an LDAP server, ldap_open() returns a pointer to an opaque LDAP structure, which should be passed to subsequent calls to ldap_bind(), ldap_search(),    
       etc.     
  
       Certain fields in the LDAP structure can be set to indicate size limit, time limit, and how aliases are handled during operations;      
       read  and  write  access  to  those  fields  must  occur  by  calling ldap_get_option(3) and ldap_set_option(3) respectively, whenever possible.    
  
       ldap_init() acts just like ldap_open(), but does not open a connection to the LDAP server.  The actual connection open will occur when the first operation is attempted.    

感兴趣的童鞋可以下载openldap的源码看看。

yum install -y openldap-debuginfo  

PostgreSQL 的ldap server配置说明,指定多台主机时,空格隔开即可,与ldap_init介绍一致。

http://www.postgresql.org/docs/9.5/static/auth-methods.html#AUTH-LDAP

ldapserver    
    Names or IP addresses of LDAP servers to connect to. Multiple servers may be specified, separated by spaces.    
  
ldapport    
    Port number on LDAP server to connect to. If no port is specified, the LDAP library's default port setting will be used.    

PostgreSQL 使用HbaLine存储pg_hba.conf中的数据结构。与LDAP认证相关的ldapserver和ldapport都在其中。

src/include/libpq/hba.h

typedef struct HbaLine    
{    
    int         linenumber;    
    char       *rawline;    
    ConnType    conntype;    
    List       *databases;    
    List       *roles;    
    struct sockaddr_storage addr;    
    struct sockaddr_storage mask;    
    IPCompareMethod ip_cmp_method;    
    char       *hostname;    
    UserAuth    auth_method;    
  
    char       *usermap;    
    char       *pamservice;    
    bool        ldaptls;    
    char       *ldapserver;    
    int         ldapport;    
    char       *ldapbinddn;    
    char       *ldapbindpasswd;    
    char       *ldapsearchattribute;    
    char       *ldapbasedn;    
    int         ldapscope;    
    char       *ldapprefix;    
    char       *ldapsuffix;    
    bool        clientcert;    
    char       *krb_realm;    
    bool        include_realm;    
    char       *radiusserver;    
    char       *radiussecret;    
    char       *radiusidentifier;    
    int         radiusport;    
}   

语义解析,判断是否使用LDAP认证的部分:

/*    
 * Parse one tokenised line from the hba config file and store the result in a    
 * HbaLine structure, or NULL if parsing fails.    
 *    
 * The tokenised line is a List of fields, each field being a List of    
 * HbaTokens.    
 *    
 * Note: this function leaks memory when an error occurs.  Caller is expected    
 * to have set a memory context that will be reset if this function returns    
 * NULL.    
 */    
static HbaLine *    
parse_hba_line(List *line, int line_num, char *raw_line)    
{    
......    
#endif    
        else if (strcmp(token->string, "ldap") == 0)    
#ifdef USE_LDAP    
                parsedline->auth_method = uaLDAP;    
#else    
                unsupauth = "ldap";    
#endif    
......    
        /*    
         * Check if the selected authentication method has any mandatory arguments    
         * that are not set.    
         */    
        if (parsedline->auth_method == uaLDAP)    
        {    
                MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");    
  
                /*    
                 * LDAP can operate in two modes: either with a direct bind, using    
                 * ldapprefix and ldapsuffix, or using a search+bind, using    
                 * ldapbasedn, ldapbinddn, ldapbindpasswd and ldapsearchattribute.    
                 * Disallow mixing these parameters.    
                 */    
                if (parsedline->ldapprefix || parsedline->ldapsuffix)    
                {    
                        if (parsedline->ldapbasedn ||    
                                parsedline->ldapbinddn ||    
                                parsedline->ldapbindpasswd ||    
                                parsedline->ldapsearchattribute)    
                        {    
                                ereport(LOG,    
                                                (errcode(ERRCODE_CONFIG_FILE_ERROR),    
                                                 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, or ldapurl together with ldapprefix"),    
                                                 errcontext("line %d of configuration file \"%s\"",    
                                                                        line_num, HbaFileName)));    
                                return NULL;    
                        }    
                }    
                else if (!parsedline->ldapbasedn)    
                {    
                        ereport(LOG,    
                                        (errcode(ERRCODE_CONFIG_FILE_ERROR),    
                                         errmsg("authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set"),    
                                         errcontext("line %d of configuration file \"%s\"",    
                                                                line_num, HbaFileName)));    
                        return NULL;    
                }    
        }    
  
......   

LDAP认证方法配置的option的语义解析部分:

/*    
 * Parse one name-value pair as an authentication option into the given    
 * HbaLine.  Return true if we successfully parse the option, false if we    
 * encounter an error.    
 */    
static bool    
parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, int line_num)    
{    
......    
        else if (strcmp(name, "ldapurl") == 0)    
        {    
#ifdef LDAP_API_FEATURE_X_OPENLDAP    
                LDAPURLDesc *urldata;    
                int                     rc;    
#endif    
  
                REQUIRE_AUTH_OPTION(uaLDAP, "ldapurl", "ldap");    
#ifdef LDAP_API_FEATURE_X_OPENLDAP    
                rc = ldap_url_parse(val, &urldata);    
                if (rc != LDAP_SUCCESS)    
                {    
                        ereport(LOG,    
                                        (errcode(ERRCODE_CONFIG_FILE_ERROR),    
                                         errmsg("could not parse LDAP URL \"%s\": %s", val, ldap_err2string(rc))));    
                        return false;    
                }    
  
                if (strcmp(urldata->lud_scheme, "ldap") != 0)    
                {    
                        ereport(LOG,    
                                        (errcode(ERRCODE_CONFIG_FILE_ERROR),    
                        errmsg("unsupported LDAP URL scheme: %s", urldata->lud_scheme)));    
                        ldap_free_urldesc(urldata);    
                        return false;    
                }    
  
                hbaline->ldapserver = pstrdup(urldata->lud_host);    
                hbaline->ldapport = urldata->lud_port;    
                hbaline->ldapbasedn = pstrdup(urldata->lud_dn);    
  
                if (urldata->lud_attrs)    
                        hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]);          /* only use first one */    
                hbaline->ldapscope = urldata->lud_scope;    
                if (urldata->lud_filter)    
                {    
                        ereport(LOG,    
                                        (errcode(ERRCODE_CONFIG_FILE_ERROR),    
                                         errmsg("filters not supported in LDAP URLs")));    
                        ldap_free_urldesc(urldata);    
                        return false;    
                }    
                ldap_free_urldesc(urldata);    
#else                                                   /* not OpenLDAP */    
                ereport(LOG,    
                                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),    
                                 errmsg("LDAP URLs not supported on this platform")));    
#endif   /* not OpenLDAP */    
        }    
        else if (strcmp(name, "ldaptls") == 0)    
        {    
                REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap");    
                if (strcmp(val, "1") == 0)    
                        hbaline->ldaptls = true;    
                else    
......   

Flag Counter

digoal’s 大量PostgreSQL文章入口