LDAPS
Hi hackers,
I've run into a few requests for $SUBJECT in the field. I understand
that this is a bit controversial: LDAP + StartTLS (what we already
support) is better than LDAPS because it's a proper standard, and LDAP
auth in general is not as good as some other authentication methods
that we should be encouraging. I don't actually have a strong view on
whether we should support it myself, but I took a swing at it to see
if there would be any technical obstacles. I did not find any. That
said, I've only tested the attached lightly on FreeBSD + OpenLDAP and
don't know if it'll work elsewhere. Thoughts?
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldaps-v1.patchapplication/octet-stream; name=ldaps-v1.patchDownload
From ad2ff251fd708647712677306d50b789560a5d16 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 3 Nov 2017 23:17:48 +1300
Subject: [PATCH] Add ldaps support for ldap authentication.
While ldaptls=1 provides a standards-based way to do LDAP authentication with
StartTLS encryption, there was an earlier de facto standard way to do LDAP
over SSL called LDAPS. Even though it's not enshrined in a standard, it's a
common request since popular LDAP servers support it and some users prefer it.
Author: Thomas Munro
---
doc/src/sgml/client-auth.sgml | 18 ++++++++++++++----
src/backend/libpq/auth.c | 21 ++++++++++++++++++---
src/backend/libpq/hba.c | 15 ++++++++++++++-
src/include/libpq/hba.h | 1 +
src/test/ldap/t/001_auth.pl | 39 +++++++++++++++++++++++++++++++++++----
5 files changed, 82 insertions(+), 12 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 99921ba079..612a857ea7 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1502,6 +1502,16 @@ omicron bryanh guest1
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>ldapscheme</literal></term>
+ <listitem>
+ <para>
+ Set to <literal>ldaps</literal> to use LDAPS. This is a non-standard
+ way of using LDAP with SSL supported by some popular LDAP server
+ implementation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>ldaptls</literal></term>
<listitem>
@@ -1594,7 +1604,7 @@ omicron bryanh guest1
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
+ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1615,9 +1625,9 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
<para>
To use encrypted LDAP connections, the <literal>ldaptls</literal>
- option has to be used in addition to <literal>ldapurl</literal>.
- The <literal>ldaps</literal> URL scheme (direct SSL connection) is not
- supported.
+ option should be used in addition to <literal>ldapurl</literal>. A
+ non-standard alternative is to use
+ <literal>ldapschema=ldaps</literal>.
</para>
<para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ab74fd8dfd..a2d32bc2ed 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2320,11 +2320,20 @@ static int errdetail_for_ldap(LDAP *ldap);
static int
InitializeLDAPConnection(Port *port, LDAP **ldap)
{
+ const char *scheme;
+ char *uri;
int ldapversion = LDAP_VERSION3;
int r;
- *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
- if (!*ldap)
+ /* Use a minimal URI to initialize an LDAP connection. */
+ scheme = port->hba->ldapscheme;
+ if (scheme == NULL)
+ scheme = "ldap";
+ uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+ port->hba->ldapport);
+ r = ldap_initialize(ldap, uri);
+ pfree(uri);
+ if (r != LDAP_SUCCESS)
{
#ifndef WIN32
ereport(LOG,
@@ -2458,7 +2467,13 @@ CheckLDAPAuth(Port *port)
}
if (port->hba->ldapport == 0)
- port->hba->ldapport = LDAP_PORT;
+ {
+ if (port->hba->ldapscheme != NULL &&
+ strcmp(port->hba->ldapscheme, "ldaps") == 0)
+ port->hba->ldapport = LDAPS_PORT;
+ else
+ port->hba->ldapport = LDAP_PORT;
+ }
sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index b2c487a8e8..61ff70f3a9 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1728,7 +1728,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
- if (strcmp(urldata->lud_scheme, "ldap") != 0)
+ if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+ strcmp(urldata->lud_scheme, "ldaps") != 0)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1739,6 +1740,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
+ hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
hbaline->ldapserver = pstrdup(urldata->lud_host);
hbaline->ldapport = urldata->lud_port;
hbaline->ldapbasedn = pstrdup(urldata->lud_dn);
@@ -1764,6 +1766,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->ldaptls = false;
}
+ else if (strcmp(name, "ldapscheme") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+ if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid ldapscheme value: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ hbaline->ldapscheme = pstrdup(val);
+ }
else if (strcmp(name, "ldapserver") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e711bee8bf..5f68f4c666 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef struct HbaLine
char *pamservice;
bool pam_use_hostname;
bool ldaptls;
+ char *ldapscheme;
char *ldapserver;
int ldapport;
char *ldapbinddn;
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..85d0079b17 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 17;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -33,13 +33,16 @@ elsif ($^O eq 'freebsd')
$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_certs = "${TestLib::tmp_check}/slapd-certs";
my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
my $ldap_server = 'localhost';
my $ldap_port = int(rand() * 16384) + 49152;
+my $ldaps_port = $ldap_port + 1;
my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
my $ldap_basedn = 'dc=example,dc=net';
my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
my $ldap_rootpw = 'secret';
@@ -63,13 +66,26 @@ access to *
database ldif
directory $ldap_datadir
+TLSCACertificateFile $slapd_certs/ca.crt
+TLSCertificateFile $slapd_certs/server.crt
+TLSCertificateKeyFile $slapd_certs/server.key
+
suffix "dc=example,dc=net"
rootdn "$ldap_rootdn"
rootpw $ldap_rootpw});
+append_to_file($ldap_conf,
+qq{TLS_REQCERT never
+});
+
mkdir $ldap_datadir or die;
+mkdir $slapd_certs or die;
-system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/ca.key", "-x509", "-out", "$slapd_certs/ca.crt", "-subj", "/cn=CA";
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/server.key", "-out", "$slapd_certs/server.csr", "-subj", "/cn=server";
+system_or_bail "openssl", "x509", "-req", "-in", "$slapd_certs/server.csr", "-CA", "$slapd_certs/ca.crt", "-CAkey", "$slapd_certs/ca.key", "-CAcreateserial", "-out", "$slapd_certs/server.crt";
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url";
END
{
@@ -178,9 +194,24 @@ test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
note "diagnostic message";
+# note bad ldapprefix with a question mark that triggers a diagnostic message
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="?uid=" ldapsuffix=""});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 2, 'any attempt fails due to bad search pattern');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapscheme=ldaps ldapport=$ldaps_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'LDAPS');
+
unlink($node->data_dir . '/pg_hba.conf');
-$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" ldaptls=1});
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)"});
$node->reload;
$ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 0, 'LDAPS with URL');
--
2.14.1
On Sat, Nov 4, 2017 at 2:05 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:
That
said, I've only tested the attached lightly on FreeBSD + OpenLDAP and
don't know if it'll work elsewhere.
Oops, that version's TAP test was a little too dependent on my
system's ldap.conf file. Here's a version that sets the LDAPCONF env
var to fix that.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldaps-v2.patchapplication/octet-stream; name=ldaps-v2.patchDownload
From ec5bf08233a1f9cd9848895bda7f515203cde083 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 3 Nov 2017 23:17:48 +1300
Subject: [PATCH] Allow ldaps when using ldap authentication.
While ldaptls=1 provides a standards-based way to do LDAP authentication with
StartTLS encryption, there was an earlier de facto standard way to do LDAP
over SSL called LDAPS. Even though it's not enshrined in a standard, it's a
common request since popular LDAP servers support it and some users prefer it.
Author: Thomas Munro
Discussion: https://postgr.es/m/CAEepm=1s+pA-LZUjQ-9GQz0Z4rX_eK=DFXAF1nBQ+ROPimuOYQ@mail.gmail.com
---
doc/src/sgml/client-auth.sgml | 18 ++++++++++++++----
src/backend/libpq/auth.c | 21 ++++++++++++++++++---
src/backend/libpq/hba.c | 15 ++++++++++++++-
src/include/libpq/hba.h | 1 +
src/test/ldap/t/001_auth.pl | 41 +++++++++++++++++++++++++++++++++++++----
5 files changed, 84 insertions(+), 12 deletions(-)
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 99921ba079..612a857ea7 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1502,6 +1502,16 @@ omicron bryanh guest1
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>ldapscheme</literal></term>
+ <listitem>
+ <para>
+ Set to <literal>ldaps</literal> to use LDAPS. This is a non-standard
+ way of using LDAP with SSL supported by some popular LDAP server
+ implementation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>ldaptls</literal></term>
<listitem>
@@ -1594,7 +1604,7 @@ omicron bryanh guest1
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
+ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1615,9 +1625,9 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
<para>
To use encrypted LDAP connections, the <literal>ldaptls</literal>
- option has to be used in addition to <literal>ldapurl</literal>.
- The <literal>ldaps</literal> URL scheme (direct SSL connection) is not
- supported.
+ option should be used in addition to <literal>ldapurl</literal>. A
+ non-standard alternative is to use
+ <literal>ldapschema=ldaps</literal>.
</para>
<para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ab74fd8dfd..a2d32bc2ed 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2320,11 +2320,20 @@ static int errdetail_for_ldap(LDAP *ldap);
static int
InitializeLDAPConnection(Port *port, LDAP **ldap)
{
+ const char *scheme;
+ char *uri;
int ldapversion = LDAP_VERSION3;
int r;
- *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
- if (!*ldap)
+ /* Use a minimal URI to initialize an LDAP connection. */
+ scheme = port->hba->ldapscheme;
+ if (scheme == NULL)
+ scheme = "ldap";
+ uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+ port->hba->ldapport);
+ r = ldap_initialize(ldap, uri);
+ pfree(uri);
+ if (r != LDAP_SUCCESS)
{
#ifndef WIN32
ereport(LOG,
@@ -2458,7 +2467,13 @@ CheckLDAPAuth(Port *port)
}
if (port->hba->ldapport == 0)
- port->hba->ldapport = LDAP_PORT;
+ {
+ if (port->hba->ldapscheme != NULL &&
+ strcmp(port->hba->ldapscheme, "ldaps") == 0)
+ port->hba->ldapport = LDAPS_PORT;
+ else
+ port->hba->ldapport = LDAP_PORT;
+ }
sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index b2c487a8e8..61ff70f3a9 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1728,7 +1728,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
- if (strcmp(urldata->lud_scheme, "ldap") != 0)
+ if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+ strcmp(urldata->lud_scheme, "ldaps") != 0)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1739,6 +1740,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
+ hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
hbaline->ldapserver = pstrdup(urldata->lud_host);
hbaline->ldapport = urldata->lud_port;
hbaline->ldapbasedn = pstrdup(urldata->lud_dn);
@@ -1764,6 +1766,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->ldaptls = false;
}
+ else if (strcmp(name, "ldapscheme") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+ if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid ldapscheme value: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ hbaline->ldapscheme = pstrdup(val);
+ }
else if (strcmp(name, "ldapserver") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e711bee8bf..5f68f4c666 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef struct HbaLine
char *pamservice;
bool pam_use_hostname;
bool ldaptls;
+ char *ldapscheme;
char *ldapserver;
int ldapport;
char *ldapbinddn;
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..3b06ec1a6a 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 17;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -33,13 +33,16 @@ elsif ($^O eq 'freebsd')
$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_certs = "${TestLib::tmp_check}/slapd-certs";
my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
my $ldap_server = 'localhost';
my $ldap_port = int(rand() * 16384) + 49152;
+my $ldaps_port = $ldap_port + 1;
my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
my $ldap_basedn = 'dc=example,dc=net';
my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
my $ldap_rootpw = 'secret';
@@ -63,13 +66,27 @@ access to *
database ldif
directory $ldap_datadir
+TLSCACertificateFile $slapd_certs/ca.crt
+TLSCertificateFile $slapd_certs/server.crt
+TLSCertificateKeyFile $slapd_certs/server.key
+
suffix "dc=example,dc=net"
rootdn "$ldap_rootdn"
rootpw $ldap_rootpw});
+# don't bother to check the server's cert (though perhaps we should)
+append_to_file($ldap_conf,
+qq{TLS_REQCERT never
+});
+
mkdir $ldap_datadir or die;
+mkdir $slapd_certs or die;
-system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/ca.key", "-x509", "-out", "$slapd_certs/ca.crt", "-subj", "/cn=CA";
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/server.key", "-out", "$slapd_certs/server.csr", "-subj", "/cn=server";
+system_or_bail "openssl", "x509", "-req", "-in", "$slapd_certs/server.csr", "-CA", "$slapd_certs/ca.crt", "-CAkey", "$slapd_certs/ca.key", "-CAcreateserial", "-out", "$slapd_certs/server.crt";
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url";
END
{
@@ -81,6 +98,7 @@ chmod 0600, $ldap_pwfile or die;
$ENV{'LDAPURI'} = $ldap_url;
$ENV{'LDAPBINDDN'} = $ldap_rootdn;
+$ENV{'LDAPCONF'} = $ldap_conf;
note "loading LDAP data";
@@ -178,9 +196,24 @@ test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
note "diagnostic message";
+# note bad ldapprefix with a question mark that triggers a diagnostic message
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="?uid=" ldapsuffix=""});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 2, 'any attempt fails due to bad search pattern');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapscheme=ldaps ldapport=$ldaps_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'LDAPS');
+
unlink($node->data_dir . '/pg_hba.conf');
-$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" ldaptls=1});
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)"});
$node->reload;
$ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 0, 'LDAPS with URL');
--
2.14.1
On Sat, Nov 4, 2017 at 2:05 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:
I've only tested the attached lightly on FreeBSD + OpenLDAP and
don't know if it'll work elsewhere.
While rebasing this on top of a nearby changes, I looked into how
portable it is. The previous version unconditionally used
ldap_initialize() instead of ldap_init() in order to be able to pass
in ldap or ldaps. According to the man pages on my system:
At this time, ldap_open() and ldap_init() are deprecated in favor of
ldap_initialize(), essentially because the latter allows to specify a
schema in the URI and it explicitly returns an error code.
But:
1. It looks like ldap_initialize() arrived in OpenLDAP 2.4 (2007),
which means that it won't work with RHEL5's OpenLDAP 2.3. That's a
vintage still found in the build farm. This new version of the patch
has a configure test so it can fall back to ldap_init(), dropping
ldaps support. That is possibly also necessary for other
implementations.
2. Windows doesn't have ldap_initialize(), but it has
ldap_sslinit()[1]https://msdn.microsoft.com/en-us/library/aa366996(v=vs.85).aspx which adds an SSL boolean argument. I've included
(but not tested) code for that. I would need a Windows + LDAP savvy
person to help test that. I'm not sure if it should also do an
LDAP_OPT_SSL check to see if the server forced the connection back to
plaintext as shown in the Microsoft docs[2]https://msdn.microsoft.com/en-us/library/aa366105(v=vs.85).aspx, or if that should be
considered OK, or it should be an option.
BTW, Stephen Layland posted a patch for ldaps years ago[3]/messages/by-id/20080426010240.GS5734@68k.org. It must
have worked some other way though, because he mentions RHEL 4 and
OpenLDAP 2.2/2.3. Unfortunately the patch wasn't attached and the
referenced webserver has disappeared from the intertubes.
I've added this to the January Commitfest.
[1]: https://msdn.microsoft.com/en-us/library/aa366996(v=vs.85).aspx
[2]: https://msdn.microsoft.com/en-us/library/aa366105(v=vs.85).aspx
[3]: /messages/by-id/20080426010240.GS5734@68k.org
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldaps-v3.patchapplication/octet-stream; name=ldaps-v3.patchDownload
From 0d5556ccd2f5b28f4e31a6093b5a610b6a5baf3a Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 3 Nov 2017 23:17:48 +1300
Subject: [PATCH] Allow ldaps when using ldap authentication.
While ldaptls=1 provides an RFC 4513 conforming way to do LDAP authentication
with TLS encryption, there was an earlier de facto standard way to do LDAP
over SSL called LDAPS. Even though it's not enshrined in a standard, it's
still widely used and sometimes required by organizations' network policies.
There seems to be no reason not to support it when available in the client
library. Therefore, add support when using OpenLDAP 2.4+ or Windows. It can
be configured with ldapscheme=ldaps or ldapurl=ldaps://...
Author: Thomas Munro
Discussion: https://postgr.es/m/CAEepm=1s+pA-LZUjQ-9GQz0Z4rX_eK=DFXAF1nBQ+ROPimuOYQ@mail.gmail.com
---
configure | 11 ++++++++
configure.in | 1 +
doc/src/sgml/client-auth.sgml | 18 ++++++++++---
src/backend/libpq/auth.c | 60 ++++++++++++++++++++++++++++++++++++++-----
src/backend/libpq/hba.c | 16 +++++++++++-
src/include/libpq/hba.h | 1 +
src/include/pg_config.h.in | 3 +++
src/test/ldap/t/001_auth.pl | 41 ++++++++++++++++++++++++++---
8 files changed, 135 insertions(+), 16 deletions(-)
diff --git a/configure b/configure
index b8995ad547..fe27084168 100755
--- a/configure
+++ b/configure
@@ -10419,6 +10419,17 @@ fi
else
LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
fi
+ for ac_func in ldap_initialize
+do :
+ ac_fn_c_check_func "$LINENO" "ldap_initialize" "ac_cv_func_ldap_initialize"
+if test "x$ac_cv_func_ldap_initialize" = xyes; then :
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_LDAP_INITIALIZE 1
+_ACEOF
+
+fi
+done
+
else
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ldap_bind in -lwldap32" >&5
$as_echo_n "checking for ldap_bind in -lwldap32... " >&6; }
diff --git a/configure.in b/configure.in
index 0e5aef37b4..24eddf7e25 100644
--- a/configure.in
+++ b/configure.in
@@ -1106,6 +1106,7 @@ if test "$with_ldap" = yes ; then
else
LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
fi
+ AC_CHECK_FUNCS([ldap_initialize])
else
AC_CHECK_LIB(wldap32, ldap_bind, [], [AC_MSG_ERROR([library 'wldap32' is required for LDAP])])
LDAP_LIBS_FE="-lwldap32"
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 99921ba079..3212c58fba 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1502,6 +1502,16 @@ omicron bryanh guest1
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>ldapscheme</literal></term>
+ <listitem>
+ <para>
+ Set to <literal>ldaps</literal> to use LDAPS. This is a non-standard
+ way of using LDAP with SSL supported by some popular LDAP server
+ implementation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>ldaptls</literal></term>
<listitem>
@@ -1594,7 +1604,7 @@ omicron bryanh guest1
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
+ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1615,9 +1625,9 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
<para>
To use encrypted LDAP connections, the <literal>ldaptls</literal>
- option has to be used in addition to <literal>ldapurl</literal>.
- The <literal>ldaps</literal> URL scheme (direct SSL connection) is not
- supported.
+ option should be used in addition to <literal>ldapurl</literal>. A
+ non-standard alternative is to use
+ <literal>ldapscheme=ldaps</literal>.
</para>
<para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 6c915a7289..51d448198a 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2320,22 +2320,62 @@ static int errdetail_for_ldap(LDAP *ldap);
static int
InitializeLDAPConnection(Port *port, LDAP **ldap)
{
+ const char *scheme;
int ldapversion = LDAP_VERSION3;
int r;
- *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+ /* Use a minimal URI to initialize an LDAP connection. */
+ scheme = port->hba->ldapscheme;
+ if (scheme == NULL)
+ scheme = "ldap";
+#ifdef WIN32
+ *ldap = ldap_sslinit(port->hba->ldapserver,
+ port->hba->ldapport,
+ strcmp(scheme, "ldaps") == 0);
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())));
-#endif
+
+ return STATUS_ERROR;
+ }
+#else
+#ifdef HAVE_LDAP_INITIALIZE
+ {
+ char *uri;
+
+ uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+ port->hba->ldapport);
+ r = ldap_initialize(ldap, uri);
+ pfree(uri);
+ if (r != LDAP_SUCCESS)
+ {
+ ereport(LOG,
+ (errmsg("could not initialize LDAP: %s",
+ ldap_err2string(r))));
+
+ return STATUS_ERROR;
+ }
+ }
+#else
+ if (strcmp(scheme, "ldaps") == 0)
+ {
+ ereport(LOG,
+ (errmsg("ldaps not supported with this LDAP library")));
+
return STATUS_ERROR;
}
+ *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+ if (!*ldap)
+ {
+ ereport(LOG,
+ (errmsg("could not initialize LDAP")));
+
+ return STATUS_ERROR;
+ }
+#endif
+#endif
if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
{
@@ -2458,7 +2498,13 @@ CheckLDAPAuth(Port *port)
}
if (port->hba->ldapport == 0)
- port->hba->ldapport = LDAP_PORT;
+ {
+ if (port->hba->ldapscheme != NULL &&
+ strcmp(port->hba->ldapscheme, "ldaps") == 0)
+ port->hba->ldapport = LDAPS_PORT;
+ else
+ port->hba->ldapport = LDAP_PORT;
+ }
sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ca78a7e0ba..8942a7fa4f 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1728,7 +1728,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
- if (strcmp(urldata->lud_scheme, "ldap") != 0)
+ if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+ strcmp(urldata->lud_scheme, "ldaps") != 0)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1739,6 +1740,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
+ if (urldata->lud_scheme)
+ hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
if (urldata->lud_host)
hbaline->ldapserver = pstrdup(urldata->lud_host);
hbaline->ldapport = urldata->lud_port;
@@ -1766,6 +1769,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->ldaptls = false;
}
+ else if (strcmp(name, "ldapscheme") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+ if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid ldapscheme value: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ hbaline->ldapscheme = pstrdup(val);
+ }
else if (strcmp(name, "ldapserver") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e711bee8bf..5f68f4c666 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef struct HbaLine
char *pamservice;
bool pam_use_hostname;
bool ldaptls;
+ char *ldapscheme;
char *ldapserver;
int ldapport;
char *ldapbinddn;
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cfdcc5ac62..fcc3f8f2a8 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -307,6 +307,9 @@
/* Define to 1 if you have the <ldap.h> header file. */
#undef HAVE_LDAP_H
+/* Define to 1 if you have the `ldap_initialize' function. */
+#undef HAVE_LDAP_INITIALIZE
+
/* Define to 1 if you have the `crypto' library (-lcrypto). */
#undef HAVE_LIBCRYPTO
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..3b06ec1a6a 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 17;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -33,13 +33,16 @@ elsif ($^O eq 'freebsd')
$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_certs = "${TestLib::tmp_check}/slapd-certs";
my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
my $ldap_server = 'localhost';
my $ldap_port = int(rand() * 16384) + 49152;
+my $ldaps_port = $ldap_port + 1;
my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
my $ldap_basedn = 'dc=example,dc=net';
my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
my $ldap_rootpw = 'secret';
@@ -63,13 +66,27 @@ access to *
database ldif
directory $ldap_datadir
+TLSCACertificateFile $slapd_certs/ca.crt
+TLSCertificateFile $slapd_certs/server.crt
+TLSCertificateKeyFile $slapd_certs/server.key
+
suffix "dc=example,dc=net"
rootdn "$ldap_rootdn"
rootpw $ldap_rootpw});
+# don't bother to check the server's cert (though perhaps we should)
+append_to_file($ldap_conf,
+qq{TLS_REQCERT never
+});
+
mkdir $ldap_datadir or die;
+mkdir $slapd_certs or die;
-system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/ca.key", "-x509", "-out", "$slapd_certs/ca.crt", "-subj", "/cn=CA";
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/server.key", "-out", "$slapd_certs/server.csr", "-subj", "/cn=server";
+system_or_bail "openssl", "x509", "-req", "-in", "$slapd_certs/server.csr", "-CA", "$slapd_certs/ca.crt", "-CAkey", "$slapd_certs/ca.key", "-CAcreateserial", "-out", "$slapd_certs/server.crt";
+
+system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url";
END
{
@@ -81,6 +98,7 @@ chmod 0600, $ldap_pwfile or die;
$ENV{'LDAPURI'} = $ldap_url;
$ENV{'LDAPBINDDN'} = $ldap_rootdn;
+$ENV{'LDAPCONF'} = $ldap_conf;
note "loading LDAP data";
@@ -178,9 +196,24 @@ test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
note "diagnostic message";
+# note bad ldapprefix with a question mark that triggers a diagnostic message
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="?uid=" ldapsuffix=""});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 2, 'any attempt fails due to bad search pattern');
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapscheme=ldaps ldapport=$ldaps_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'LDAPS');
+
unlink($node->data_dir . '/pg_hba.conf');
-$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" ldaptls=1});
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)"});
$node->reload;
$ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 0, 'LDAPS with URL');
--
2.14.1
This patch looks reasonable to me. I have also seen occasional requests
for this in the field.
If someone could test this on Windows, I think we could move ahead with it.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On 12/26/17 15:53, Peter Eisentraut wrote:
This patch looks reasonable to me. I have also seen occasional requests
for this in the field.If someone could test this on Windows, I think we could move ahead with it.
A small point on the test changes. You change the test under
"diagnostic message", but I'm not sure why. Do the changes invalidate
the existing test?
We should probably also add another "note" call to introduce the LDAPS
tests section.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Wed, Jan 3, 2018 at 5:31 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 12/26/17 15:53, Peter Eisentraut wrote:
This patch looks reasonable to me. I have also seen occasional requests
for this in the field.If someone could test this on Windows, I think we could move ahead with it.
Thanks for looking at this.
A small point on the test changes. You change the test under
"diagnostic message", but I'm not sure why. Do the changes invalidate
the existing test?
Yeah. In master, I was relying on the server rejecting ldaptls=1
requests due to lack of configured certificate in order to generate a
diagnostic message. Now that there is a certificate, I needed to find
another way to get requests rejected with a diagnostic message. I
have added a brief note to the commit message about this.
We should probably also add another "note" call to introduce the LDAPS
tests section.
I realised that I should probably also include a new test for
ldaptls=1, so that we can see that both ways of doing TLS are working.
I added that test, and added a "note" to label the whole section as
"TLS". Please see attached.
--
Thomas Munro
http://www.enterprisedb.com
Attachments:
ldaps-v4.patchapplication/octet-stream; name=ldaps-v4.patchDownload
From 2f988bda8be9b714252d6ffcde9634edfcfb792c Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@enterprisedb.com>
Date: Fri, 3 Nov 2017 23:17:48 +1300
Subject: [PATCH] Allow ldaps when using ldap authentication.
While ldaptls=1 provides an RFC 4513 conforming way to do LDAP authentication
with TLS encryption, there was an earlier de facto standard way to do LDAP
over SSL called LDAPS. Even though it's not enshrined in a standard, it's
still widely used and sometimes required by organizations' network policies.
There seems to be no reason not to support it when available in the client
library. Therefore, add support when using OpenLDAP 2.4+ or Windows. It can
be configured with ldapscheme=ldaps or ldapurl=ldaps://...
Add tests for both ways of requesting LDAPS and a test for the pre-existing
ldaptls=1. Modify the 001_auth.pl test for "diagnostic messages", which was
previously relying on the server rejecting ldaptls=1.
Author: Thomas Munro
Reviewed-By: Peter Eisentraut
Discussion: https://postgr.es/m/CAEepm=1s+pA-LZUjQ-9GQz0Z4rX_eK=DFXAF1nBQ+ROPimuOYQ@mail.gmail.com
---
configure | 11 ++++++++
configure.in | 1 +
doc/src/sgml/client-auth.sgml | 18 ++++++++++---
src/backend/libpq/auth.c | 60 ++++++++++++++++++++++++++++++++++++++-----
src/backend/libpq/hba.c | 16 +++++++++++-
src/include/libpq/hba.h | 1 +
src/include/pg_config.h.in | 3 +++
src/test/ldap/t/001_auth.pl | 53 +++++++++++++++++++++++++++++++++++---
8 files changed, 147 insertions(+), 16 deletions(-)
diff --git a/configure b/configure
index d9b7b8d7ec..d1ddb332aa 100755
--- a/configure
+++ b/configure
@@ -10424,6 +10424,17 @@ fi
else
LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
fi
+ for ac_func in ldap_initialize
+do :
+ ac_fn_c_check_func "$LINENO" "ldap_initialize" "ac_cv_func_ldap_initialize"
+if test "x$ac_cv_func_ldap_initialize" = xyes; then :
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_LDAP_INITIALIZE 1
+_ACEOF
+
+fi
+done
+
else
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ldap_bind in -lwldap32" >&5
$as_echo_n "checking for ldap_bind in -lwldap32... " >&6; }
diff --git a/configure.in b/configure.in
index 5245899109..05e8586ff3 100644
--- a/configure.in
+++ b/configure.in
@@ -1106,6 +1106,7 @@ if test "$with_ldap" = yes ; then
else
LDAP_LIBS_FE="-lldap $EXTRA_LDAP_LIBS"
fi
+ AC_CHECK_FUNCS([ldap_initialize])
else
AC_CHECK_LIB(wldap32, ldap_bind, [], [AC_MSG_ERROR([library 'wldap32' is required for LDAP])])
LDAP_LIBS_FE="-lwldap32"
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c8a1bc79aa..6c7dcd7c4e 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1502,6 +1502,16 @@ omicron bryanh guest1
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>ldapscheme</literal></term>
+ <listitem>
+ <para>
+ Set to <literal>ldaps</literal> to use LDAPS. This is a non-standard
+ way of using LDAP with SSL supported by some popular LDAP server
+ implementation.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry>
<term><literal>ldaptls</literal></term>
<listitem>
@@ -1594,7 +1604,7 @@ omicron bryanh guest1
An RFC 4516 LDAP URL. This is an alternative way to write some of the
other LDAP options in a more compact and standard form. The format is
<synopsis>
-ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
+ldap[s]://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replaceable>basedn</replaceable>[?[<replaceable>attribute</replaceable>][?[<replaceable>scope</replaceable>][?[<replaceable>filter</replaceable>]]]]
</synopsis>
<replaceable>scope</replaceable> must be one
of <literal>base</literal>, <literal>one</literal>, <literal>sub</literal>,
@@ -1615,9 +1625,9 @@ ldap://<replaceable>host</replaceable>[:<replaceable>port</replaceable>]/<replac
<para>
To use encrypted LDAP connections, the <literal>ldaptls</literal>
- option has to be used in addition to <literal>ldapurl</literal>.
- The <literal>ldaps</literal> URL scheme (direct SSL connection) is not
- supported.
+ option should be used in addition to <literal>ldapurl</literal>. A
+ non-standard alternative is to use
+ <literal>ldapscheme=ldaps</literal>.
</para>
<para>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index b7f9bb1669..c2af52ce41 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2355,22 +2355,62 @@ static int errdetail_for_ldap(LDAP *ldap);
static int
InitializeLDAPConnection(Port *port, LDAP **ldap)
{
+ const char *scheme;
int ldapversion = LDAP_VERSION3;
int r;
- *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+ /* Use a minimal URI to initialize an LDAP connection. */
+ scheme = port->hba->ldapscheme;
+ if (scheme == NULL)
+ scheme = "ldap";
+#ifdef WIN32
+ *ldap = ldap_sslinit(port->hba->ldapserver,
+ port->hba->ldapport,
+ strcmp(scheme, "ldaps") == 0);
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())));
-#endif
+
+ return STATUS_ERROR;
+ }
+#else
+#ifdef HAVE_LDAP_INITIALIZE
+ {
+ char *uri;
+
+ uri = psprintf("%s://%s:%d", scheme, port->hba->ldapserver,
+ port->hba->ldapport);
+ r = ldap_initialize(ldap, uri);
+ pfree(uri);
+ if (r != LDAP_SUCCESS)
+ {
+ ereport(LOG,
+ (errmsg("could not initialize LDAP: %s",
+ ldap_err2string(r))));
+
+ return STATUS_ERROR;
+ }
+ }
+#else
+ if (strcmp(scheme, "ldaps") == 0)
+ {
+ ereport(LOG,
+ (errmsg("ldaps not supported with this LDAP library")));
+
return STATUS_ERROR;
}
+ *ldap = ldap_init(port->hba->ldapserver, port->hba->ldapport);
+ if (!*ldap)
+ {
+ ereport(LOG,
+ (errmsg("could not initialize LDAP")));
+
+ return STATUS_ERROR;
+ }
+#endif
+#endif
if ((r = ldap_set_option(*ldap, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS)
{
@@ -2493,7 +2533,13 @@ CheckLDAPAuth(Port *port)
}
if (port->hba->ldapport == 0)
- port->hba->ldapport = LDAP_PORT;
+ {
+ if (port->hba->ldapscheme != NULL &&
+ strcmp(port->hba->ldapscheme, "ldaps") == 0)
+ port->hba->ldapport = LDAPS_PORT;
+ else
+ port->hba->ldapport = LDAP_PORT;
+ }
sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ca78a7e0ba..8942a7fa4f 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1728,7 +1728,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
- if (strcmp(urldata->lud_scheme, "ldap") != 0)
+ if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+ strcmp(urldata->lud_scheme, "ldaps") != 0)
{
ereport(elevel,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1739,6 +1740,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
return false;
}
+ if (urldata->lud_scheme)
+ hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
if (urldata->lud_host)
hbaline->ldapserver = pstrdup(urldata->lud_host);
hbaline->ldapport = urldata->lud_port;
@@ -1766,6 +1769,17 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
else
hbaline->ldaptls = false;
}
+ else if (strcmp(name, "ldapscheme") == 0)
+ {
+ REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+ if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+ ereport(elevel,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid ldapscheme value: \"%s\"", val),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, HbaFileName)));
+ hbaline->ldapscheme = pstrdup(val);
+ }
else if (strcmp(name, "ldapserver") == 0)
{
REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index e711bee8bf..5f68f4c666 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef struct HbaLine
char *pamservice;
bool pam_use_hostname;
bool ldaptls;
+ char *ldapscheme;
char *ldapserver;
int ldapport;
char *ldapbinddn;
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 0aa6be4666..27b1368721 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -310,6 +310,9 @@
/* Define to 1 if you have the <ldap.h> header file. */
#undef HAVE_LDAP_H
+/* Define to 1 if you have the `ldap_initialize' function. */
+#undef HAVE_LDAP_INITIALIZE
+
/* Define to 1 if you have the `crypto' library (-lcrypto). */
#undef HAVE_LIBCRYPTO
diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl
index 38760ece61..0bf6ffa208 100644
--- a/src/test/ldap/t/001_auth.pl
+++ b/src/test/ldap/t/001_auth.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use TestLib;
use PostgresNode;
-use Test::More tests => 15;
+use Test::More tests => 18;
my ($slapd, $ldap_bin_dir, $ldap_schema_dir);
@@ -33,13 +33,16 @@ elsif ($^O eq 'freebsd')
$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir;
my $ldap_datadir = "${TestLib::tmp_check}/openldap-data";
+my $slapd_certs = "${TestLib::tmp_check}/slapd-certs";
my $slapd_conf = "${TestLib::tmp_check}/slapd.conf";
my $slapd_pidfile = "${TestLib::tmp_check}/slapd.pid";
my $slapd_logfile = "${TestLib::tmp_check}/slapd.log";
my $ldap_conf = "${TestLib::tmp_check}/ldap.conf";
my $ldap_server = 'localhost';
my $ldap_port = int(rand() * 16384) + 49152;
+my $ldaps_port = $ldap_port + 1;
my $ldap_url = "ldap://$ldap_server:$ldap_port";
+my $ldaps_url = "ldaps://$ldap_server:$ldaps_port";
my $ldap_basedn = 'dc=example,dc=net';
my $ldap_rootdn = 'cn=Manager,dc=example,dc=net';
my $ldap_rootpw = 'secret';
@@ -63,13 +66,27 @@ access to *
database ldif
directory $ldap_datadir
+TLSCACertificateFile $slapd_certs/ca.crt
+TLSCertificateFile $slapd_certs/server.crt
+TLSCertificateKeyFile $slapd_certs/server.key
+
suffix "dc=example,dc=net"
rootdn "$ldap_rootdn"
rootpw $ldap_rootpw});
+# don't bother to check the server's cert (though perhaps we should)
+append_to_file($ldap_conf,
+qq{TLS_REQCERT never
+});
+
mkdir $ldap_datadir or die;
+mkdir $slapd_certs or die;
+
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/ca.key", "-x509", "-out", "$slapd_certs/ca.crt", "-subj", "/cn=CA";
+system_or_bail "openssl", "req", "-new", "-nodes", "-keyout", "$slapd_certs/server.key", "-out", "$slapd_certs/server.csr", "-subj", "/cn=server";
+system_or_bail "openssl", "x509", "-req", "-in", "$slapd_certs/server.csr", "-CA", "$slapd_certs/ca.crt", "-CAkey", "$slapd_certs/ca.key", "-CAcreateserial", "-out", "$slapd_certs/server.crt";
-system_or_bail $slapd, '-f', $slapd_conf, '-h', $ldap_url;
+system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url";
END
{
@@ -81,6 +98,7 @@ chmod 0600, $ldap_pwfile or die;
$ENV{'LDAPURI'} = $ldap_url;
$ENV{'LDAPBINDDN'} = $ldap_rootdn;
+$ENV{'LDAPCONF'} = $ldap_conf;
note "loading LDAP data";
@@ -178,9 +196,36 @@ test_access($node, 'test1', 0, 'combined LDAP URL and search filter');
note "diagnostic message";
+# note bad ldapprefix with a question mark that triggers a diagnostic message
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="?uid=" ldapsuffix=""});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 2, 'any attempt fails due to bad search pattern');
+
+note "TLS";
+
+# request StartTLS with ldaptls=1
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)" ldaptls=1});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'StartTLS');
+
+# request LDAPS with ldapscheme=ldaps
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapscheme=ldaps ldapport=$ldaps_port ldapbasedn="$ldap_basedn" ldapsearchfilter="(uid=\$username)"});
+$node->reload;
+
+$ENV{"PGPASSWORD"} = 'secret1';
+test_access($node, 'test1', 0, 'LDAPS');
+
+# request LDAPS with ldapurl=ldaps://...
unlink($node->data_dir . '/pg_hba.conf');
-$node->append_conf('pg_hba.conf', qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapprefix="uid=" ldapsuffix=",dc=example,dc=net" ldaptls=1});
+$node->append_conf('pg_hba.conf', qq{local all all ldap ldapurl="$ldaps_url/$ldap_basedn??sub?(uid=\$username)"});
$node->reload;
$ENV{"PGPASSWORD"} = 'secret1';
-test_access($node, 'test1', 2, 'any attempt fails due to unsupported TLS');
+test_access($node, 'test1', 0, 'LDAPS with URL');
--
2.14.1
On 1/2/18 14:56, Thomas Munro wrote:
A small point on the test changes. You change the test under
"diagnostic message", but I'm not sure why. Do the changes invalidate
the existing test?Yeah. In master, I was relying on the server rejecting ldaptls=1
requests due to lack of configured certificate in order to generate a
diagnostic message. Now that there is a certificate, I needed to find
another way to get requests rejected with a diagnostic message. I
have added a brief note to the commit message about this.We should probably also add another "note" call to introduce the LDAPS
tests section.I realised that I should probably also include a new test for
ldaptls=1, so that we can see that both ways of doing TLS are working.
I added that test, and added a "note" to label the whole section as
"TLS". Please see attached.
Committed.
I added a test case for combining LDAPS with StartTLS. The OpenLDAP
library sensibly rejects that, so we don't need to do anything ourselves
to prevent that.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services