Add support for RADIUS authentication.

This commit is contained in:
Magnus Hagander 2010-01-27 12:12:00 +00:00
parent 000416ac32
commit b3daac5a9c
7 changed files with 532 additions and 11 deletions

View File

@ -1,4 +1,4 @@
<!-- $PostgreSQL: pgsql/doc/src/sgml/client-auth.sgml,v 1.127 2010/01/26 06:45:31 petere Exp $ --> <!-- $PostgreSQL: pgsql/doc/src/sgml/client-auth.sgml,v 1.128 2010/01/27 12:11:59 mha Exp $ -->
<chapter id="client-authentication"> <chapter id="client-authentication">
<title>Client Authentication</title> <title>Client Authentication</title>
@ -394,6 +394,16 @@ hostnossl <replaceable>database</replaceable> <replaceable>user</replaceable>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><literal>radius</></term>
<listitem>
<para>
Authenticate using a RADIUS server. See <xref
linkend="auth-radius"> for detauls.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><literal>cert</></term> <term><literal>cert</></term>
<listitem> <listitem>
@ -1331,6 +1341,95 @@ ldapserver=ldap.example.net ldapprefix="cn=" ldapsuffix=", dc=example, dc=net"
</sect2> </sect2>
<sect2 id="auth-radius">
<title>RADIUS authentication</title>
<indexterm zone="auth-radius">
<primary>RADIUS</primary>
</indexterm>
<para>
This authentication method operates similarly to
<literal>password</literal> except that it uses RADIUS
as the password verification method. RADIUS is used only to validate
the user name/password pairs. Therefore the user must already
exist in the database before RADIUS can be used for
authentication.
</para>
<para>
When using RADIUS authentication, an Access Request message will be sent
to the configured RADIUS server. This request will be of type
<literal>Authenticate Only</literal>, and include parameters for
<literal>user name</>, <literal>password</> (encrypted) and
<literal>NAS Identifier</>. The request will be encrypted using
a secret shared with the server. The RADIUS server will respond to
this server with either <literal>Access Accept</> or
<literal>Access Reject</>. There is no support for RADIUS accounting.
</para>
<para>
The following configuration options are supported for RADIUS:
<variablelist>
<varlistentry>
<term><literal>radiusserver</literal></term>
<listitem>
<para>
The IP address of the RADIUS server to connect to. This must
be an IPV4 address and not a hostname. This parameter is required.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>radiussecret</literal></term>
<listitem>
<para>
The shared secret used when talking securely to the RADIUS
server. This must have exactly the same value on the PostgreSQL
and RADIUS servers. It is recommended that this is a string of
at least 16 characters. This parameter is required.
<note>
<para>
The encryption vector used will only be cryptographically
strong if <productname>PostgreSQL</> is built with support for
<productname>OpenSSL</>. In other cases, the transmission to the
RADIUS server should only be considered obfuscated, not secured, and
external security measures should be applied if necessary.
</para>
</note>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>radiusport</literal></term>
<listitem>
<para>
The port number on the RADIUS server to connect to. If no port
is specified, the default port <literal>1812</> will be used.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>radiusidentifier</literal></term>
<listitem>
<para>
The string used as <literal>NAS Identifier</> in the RADIUS
requests. This parameter can be used as a second parameter
identifying for example which database the user is attempting
to authenticate as, which can be used for policy matching on
the RADIUS server. If no identifier is specified, the default
<literal>postgresql</> will be used.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</sect2>
<sect2 id="auth-cert"> <sect2 id="auth-cert">
<title>Certificate authentication</title> <title>Certificate authentication</title>

View File

@ -8,7 +8,7 @@
* *
* *
* IDENTIFICATION * IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.191 2010/01/10 14:16:07 mha Exp $ * $PostgreSQL: pgsql/src/backend/libpq/auth.c,v 1.192 2010/01/27 12:11:59 mha Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
@ -27,12 +27,14 @@
#include <netinet/in.h> #include <netinet/in.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <unistd.h> #include <unistd.h>
#include <stddef.h>
#include "libpq/auth.h" #include "libpq/auth.h"
#include "libpq/crypt.h" #include "libpq/crypt.h"
#include "libpq/ip.h" #include "libpq/ip.h"
#include "libpq/libpq.h" #include "libpq/libpq.h"
#include "libpq/pqformat.h" #include "libpq/pqformat.h"
#include "libpq/md5.h"
#include "miscadmin.h" #include "miscadmin.h"
#include "storage/ipc.h" #include "storage/ipc.h"
@ -182,6 +184,15 @@ typedef SECURITY_STATUS
static int pg_SSPI_recvauth(Port *port); static int pg_SSPI_recvauth(Port *port);
#endif #endif
/*----------------------------------------------------------------
* RADIUS Authentication
*----------------------------------------------------------------
*/
#ifdef USE_SSL
#include <openssl/rand.h>
#endif
static int CheckRADIUSAuth(Port *port);
/* /*
* Maximum accepted size of GSS and SSPI authentication tokens. * Maximum accepted size of GSS and SSPI authentication tokens.
@ -265,6 +276,9 @@ auth_failed(Port *port, int status)
case uaLDAP: case uaLDAP:
errstr = gettext_noop("LDAP authentication failed for user \"%s\""); errstr = gettext_noop("LDAP authentication failed for user \"%s\"");
break; break;
case uaRADIUS:
errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
break;
default: default:
errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method"); errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method");
break; break;
@ -473,7 +487,9 @@ ClientAuthentication(Port *port)
Assert(false); Assert(false);
#endif #endif
break; break;
case uaRADIUS:
status = CheckRADIUSAuth(port);
break;
case uaTrust: case uaTrust:
status = STATUS_OK; status = STATUS_OK;
break; break;
@ -2415,3 +2431,350 @@ CheckCertAuth(Port *port)
} }
#endif #endif
/*----------------------------------------------------------------
* RADIUS authentication
*----------------------------------------------------------------
*/
/*
* RADIUS authentication is described in RFC2865 (and several
* others).
*/
#define RADIUS_VECTOR_LENGTH 16
#define RADIUS_HEADER_LENGTH 20
typedef struct
{
uint8 attribute;
uint8 length;
uint8 data[1];
} radius_attribute;
typedef struct
{
uint8 code;
uint8 id;
uint16 length;
uint8 vector[RADIUS_VECTOR_LENGTH];
} radius_packet;
/* RADIUS packet types */
#define RADIUS_ACCESS_REQUEST 1
#define RADIUS_ACCESS_ACCEPT 2
#define RADIUS_ACCESS_REJECT 3
/* RAIDUS attributes */
#define RADIUS_USER_NAME 1
#define RADIUS_PASSWORD 2
#define RADIUS_SERVICE_TYPE 6
#define RADIUS_NAS_IDENTIFIER 32
/* RADIUS service types */
#define RADIUS_AUTHENTICATE_ONLY 8
/* Maximum size of a RADIUS packet we will create or accept */
#define RADIUS_BUFFER_SIZE 1024
/* Seconds to wait - XXX: should be in a config variable! */
#define RADIUS_TIMEOUT 3
static void
radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len)
{
radius_attribute *attr;
if (packet->length + len > RADIUS_BUFFER_SIZE)
{
/*
* With remotely realistic data, this can never happen. But catch it just to make
* sure we don't overrun a buffer. We'll just skip adding the broken attribute,
* which will in the end cause authentication to fail.
*/
elog(WARNING,
"Adding attribute code %i with length %i to radius packet would create oversize packet, ignoring",
type, len);
return;
}
attr = (radius_attribute *) ((unsigned char *)packet + packet->length);
attr->attribute = type;
attr->length = len + 2; /* total size includes type and length */
memcpy(attr->data, data, len);
packet->length += attr->length;
}
static int
CheckRADIUSAuth(Port *port)
{
char *passwd;
char *identifier = "postgresql";
char radius_buffer[RADIUS_BUFFER_SIZE];
char receive_buffer[RADIUS_BUFFER_SIZE];
radius_packet *packet = (radius_packet *)radius_buffer;
radius_packet *receivepacket = (radius_packet *)receive_buffer;
int32 service = htonl(RADIUS_AUTHENTICATE_ONLY);
uint8 *cryptvector;
uint8 encryptedpassword[RADIUS_VECTOR_LENGTH];
int packetlength;
pgsocket sock;
struct sockaddr_in localaddr;
struct sockaddr_in remoteaddr;
socklen_t addrsize;
fd_set fdset;
struct timeval timeout;
int i,r;
/* Make sure struct alignment is correct */
Assert(offsetof(radius_packet, vector) == 4);
/* Verify parameters */
if (!port->hba->radiusserver || port->hba->radiusserver[0] == '\0')
{
ereport(LOG,
(errmsg("RADIUS server not specified")));
return STATUS_ERROR;
}
if (!port->hba->radiussecret || port->hba->radiussecret[0] == '\0')
{
ereport(LOG,
(errmsg("RADIUS secret not specified")));
return STATUS_ERROR;
}
if (port->hba->radiusport == 0)
port->hba->radiusport = 1812;
memset(&remoteaddr, 0, sizeof(remoteaddr));
remoteaddr.sin_family = AF_INET;
remoteaddr.sin_addr.s_addr = inet_addr(port->hba->radiusserver);
if (remoteaddr.sin_addr.s_addr == INADDR_NONE)
{
ereport(LOG,
(errmsg("RADIUS server '%s' is not a valid IP address",
port->hba->radiusserver)));
return STATUS_ERROR;
}
remoteaddr.sin_port = htons(port->hba->radiusport);
if (port->hba->radiusidentifier && port->hba->radiusidentifier[0])
identifier = port->hba->radiusidentifier;
/* Send regular password request to client, and get the response */
sendAuthRequest(port, AUTH_REQ_PASSWORD);
passwd = recv_password_packet(port);
if (passwd == NULL)
return STATUS_EOF; /* client wouldn't send password */
if (strlen(passwd) == 0)
{
ereport(LOG,
(errmsg("empty password returned by client")));
return STATUS_ERROR;
}
if (strlen(passwd) > RADIUS_VECTOR_LENGTH)
{
ereport(LOG,
(errmsg("RADIUS authentication does not support passwords longer than 16 characters")));
return STATUS_ERROR;
}
/* Construct RADIUS packet */
packet->code = RADIUS_ACCESS_REQUEST;
packet->length = RADIUS_HEADER_LENGTH;
#ifdef USE_SSL
if (RAND_bytes(packet->vector, RADIUS_VECTOR_LENGTH) != 1)
{
ereport(LOG,
(errmsg("could not generate random encryption vector")));
return STATUS_ERROR;
}
#else
for (i = 0; i < RADIUS_VECTOR_LENGTH; i++)
/* Use a lower strengh random number of OpenSSL is not available */
packet->vector[i] = random() % 255;
#endif
packet->id = packet->vector[0];
radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (unsigned char *) &service, sizeof(service));
radius_add_attribute(packet, RADIUS_USER_NAME, (unsigned char *) port->user_name, strlen(port->user_name));
radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (unsigned char *) identifier, strlen(identifier));
/*
* RADIUS password attributes are calculated as:
* e[0] = p[0] XOR MD5(secret + vector)
*/
cryptvector = palloc(RADIUS_VECTOR_LENGTH + strlen(port->hba->radiussecret));
memcpy(cryptvector, port->hba->radiussecret, strlen(port->hba->radiussecret));
memcpy(cryptvector + strlen(port->hba->radiussecret), packet->vector, RADIUS_VECTOR_LENGTH);
if (!pg_md5_binary(cryptvector, RADIUS_VECTOR_LENGTH + strlen(port->hba->radiussecret), encryptedpassword))
{
ereport(LOG,
(errmsg("could not perform md5 encryption of password")));
pfree(cryptvector);
return STATUS_ERROR;
}
pfree(cryptvector);
for (i = 0; i < RADIUS_VECTOR_LENGTH; i++)
{
if (i < strlen(passwd))
encryptedpassword[i] = passwd[i] ^ encryptedpassword[i];
else
encryptedpassword[i] = '\0' ^ encryptedpassword[i];
}
radius_add_attribute(packet, RADIUS_PASSWORD, encryptedpassword, RADIUS_VECTOR_LENGTH);
/* Length need to be in network order on the wire */
packetlength = packet->length;
packet->length = htons(packet->length);
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
ereport(LOG,
(errmsg("could not create RADIUS socket: %m")));
return STATUS_ERROR;
}
memset(&localaddr, 0, sizeof(localaddr));
localaddr.sin_family = AF_INET;
localaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *) &localaddr, sizeof(localaddr)))
{
ereport(LOG,
(errmsg("could not bind local RADIUS socket: %m")));
closesocket(sock);
return STATUS_ERROR;
}
if (sendto(sock, radius_buffer, packetlength, 0,
(struct sockaddr *) &remoteaddr, sizeof(remoteaddr)) < 0)
{
ereport(LOG,
(errmsg("could not send RADIUS packet: %m")));
closesocket(sock);
return STATUS_ERROR;
}
timeout.tv_sec = RADIUS_TIMEOUT;
timeout.tv_usec = 0;
FD_ZERO(&fdset);
FD_SET(sock, &fdset);
while (true)
{
r = select(sock + 1, &fdset, NULL, NULL, &timeout);
if (r < 0)
{
if (errno == EINTR)
continue;
/* Anything else is an actual error */
ereport(LOG,
(errmsg("could not check status on RADIUS socket: %m")));
closesocket(sock);
return STATUS_ERROR;
}
if (r == 0)
{
ereport(LOG,
(errmsg("timeout waiting for RADIUS response")));
closesocket(sock);
return STATUS_ERROR;
}
/* else we actually have a packet ready to read */
break;
}
/* Read the response packet */
addrsize = sizeof(remoteaddr);
packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
(struct sockaddr *) &remoteaddr, &addrsize);
if (packetlength < 0)
{
ereport(LOG,
(errmsg("could not read RADIUS response: %m")));
closesocket(sock);
return STATUS_ERROR;
}
closesocket(sock);
if (remoteaddr.sin_port != htons(port->hba->radiusport))
{
ereport(LOG,
(errmsg("RADIUS response was sent from incorrect port: %i",
ntohs(remoteaddr.sin_port))));
return STATUS_ERROR;
}
if (packetlength < RADIUS_HEADER_LENGTH)
{
ereport(LOG,
(errmsg("RADIUS response too short: %i", packetlength)));
return STATUS_ERROR;
}
if (packetlength != ntohs(receivepacket->length))
{
ereport(LOG,
(errmsg("RADIUS response has corrupt length: %i (actual length %i)",
ntohs(receivepacket->length), packetlength)));
return STATUS_ERROR;
}
if (packet->id != receivepacket->id)
{
ereport(LOG,
(errmsg("RADIUS response is to a different request: %i (should be %i)",
receivepacket->id, packet->id)));
return STATUS_ERROR;
}
/*
* Verify the response authenticator, which is calculated as
* MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret)
*/
cryptvector = palloc(packetlength + strlen(port->hba->radiussecret));
memcpy(cryptvector, receivepacket, 4); /* code+id+length */
memcpy(cryptvector+4, packet->vector, RADIUS_VECTOR_LENGTH); /* request authenticator, from original packet */
if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no attributes at all */
memcpy(cryptvector+RADIUS_HEADER_LENGTH, receive_buffer + RADIUS_HEADER_LENGTH, packetlength-RADIUS_HEADER_LENGTH);
memcpy(cryptvector+packetlength, port->hba->radiussecret, strlen(port->hba->radiussecret));
if (!pg_md5_binary(cryptvector,
packetlength + strlen(port->hba->radiussecret),
encryptedpassword))
{
ereport(LOG,
(errmsg("could not perform md5 encryption of received packet")));
pfree(cryptvector);
return STATUS_ERROR;
}
pfree(cryptvector);
if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0)
{
ereport(LOG,
(errmsg("RADIUS response has incorrect MD5 signature")));
return STATUS_ERROR;
}
if (receivepacket->code == RADIUS_ACCESS_ACCEPT)
return STATUS_OK;
else if (receivepacket->code == RADIUS_ACCESS_REJECT)
return STATUS_ERROR;
else
{
ereport(LOG,
(errmsg("RADIUS response has invalid code (%i) for user '%s'",
receivepacket->code, port->user_name)));
return STATUS_ERROR;
}
}

View File

@ -10,7 +10,7 @@
* *
* *
* IDENTIFICATION * IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.195 2010/01/15 09:19:02 heikki Exp $ * $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.196 2010/01/27 12:11:59 mha Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
@ -952,6 +952,8 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline)
#else #else
unsupauth = "cert"; unsupauth = "cert";
#endif #endif
else if (strcmp(token, "radius")== 0)
parsedline->auth_method = uaRADIUS;
else else
{ {
ereport(LOG, ereport(LOG,
@ -1162,6 +1164,45 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline)
else else
parsedline->include_realm = false; parsedline->include_realm = false;
} }
else if (strcmp(token, "radiusserver") == 0)
{
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusserver", "radius");
if (inet_addr(c) == INADDR_NONE)
{
ereport(LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("invalid RADIUS server IP address: \"%s\"", c),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
return false;
}
parsedline->radiusserver = pstrdup(c);
}
else if (strcmp(token, "radiusport") == 0)
{
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusport", "radius");
parsedline->radiusport = atoi(c);
if (parsedline->radiusport == 0)
{
ereport(LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("invalid RADIUS port number: \"%s\"", c),
errcontext("line %d of configuration file \"%s\"",
line_num, HbaFileName)));
return false;
}
}
else if (strcmp(token, "radiussecret") == 0)
{
REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecret", "radius");
parsedline->radiussecret = pstrdup(c);
}
else if (strcmp(token, "radiusidentifier") == 0)
{
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifier", "radius");
parsedline->radiusidentifier = pstrdup(c);
}
else else
{ {
ereport(LOG, ereport(LOG,
@ -1214,6 +1255,12 @@ parse_hba_line(List *line, int line_num, HbaLine *parsedline)
} }
} }
if (parsedline->auth_method == uaRADIUS)
{
MANDATORY_AUTH_ARG(parsedline->radiusserver, "radiusserver", "radius");
MANDATORY_AUTH_ARG(parsedline->radiussecret, "radiussecret", "radius");
}
/* /*
* Enforce any parameters implied by other settings. * Enforce any parameters implied by other settings.
*/ */

View File

@ -14,7 +14,7 @@
* Portions Copyright (c) 1994, Regents of the University of California * Portions Copyright (c) 1994, Regents of the University of California
* *
* IDENTIFICATION * IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/libpq/md5.c,v 1.38 2010/01/02 16:57:45 momjian Exp $ * $PostgreSQL: pgsql/src/backend/libpq/md5.c,v 1.39 2010/01/27 12:11:59 mha Exp $
*/ */
/* This is intended to be used in both frontend and backend, so use c.h */ /* This is intended to be used in both frontend and backend, so use c.h */
@ -298,6 +298,12 @@ pg_md5_hash(const void *buff, size_t len, char *hexsum)
return true; return true;
} }
bool pg_md5_binary(const void *buff, size_t len, void *outbuf)
{
if (!calculateDigestFromBuffer((uint8 *) buff, len, outbuf))
return false;
return true;
}
/* /*

View File

@ -39,9 +39,9 @@
# any subnet that the server is directly connected to. # any subnet that the server is directly connected to.
# #
# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi",
# "krb5", "ident", "pam", "ldap" or "cert". Note that "password" # "krb5", "ident", "pam", "ldap", "radius" or "cert". Note that
# sends passwords in clear text; "md5" is preferred since it sends # "password" sends passwords in clear text; "md5" is preferred since
# encrypted passwords. # it sends encrypted passwords.
# #
# OPTIONS are a set of options for the authentication in the format # OPTIONS are a set of options for the authentication in the format
# NAME=VALUE. The available options depend on the different # NAME=VALUE. The available options depend on the different

View File

@ -4,7 +4,7 @@
* Interface to hba.c * Interface to hba.c
* *
* *
* $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.60 2009/12/12 21:35:21 mha Exp $ * $PostgreSQL: pgsql/src/include/libpq/hba.h,v 1.61 2010/01/27 12:12:00 mha Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
@ -27,7 +27,8 @@ typedef enum UserAuth
uaSSPI, uaSSPI,
uaPAM, uaPAM,
uaLDAP, uaLDAP,
uaCert uaCert,
uaRADIUS
} UserAuth; } UserAuth;
typedef enum IPCompareMethod typedef enum IPCompareMethod
@ -71,6 +72,10 @@ typedef struct
char *krb_server_hostname; char *krb_server_hostname;
char *krb_realm; char *krb_realm;
bool include_realm; bool include_realm;
char *radiusserver;
char *radiussecret;
char *radiusidentifier;
int radiusport;
} HbaLine; } HbaLine;
/* kluge to avoid including libpq/libpq-be.h here */ /* kluge to avoid including libpq/libpq-be.h here */

View File

@ -9,7 +9,7 @@
* Portions Copyright (c) 1996-2010, PostgreSQL Global Development Group * Portions Copyright (c) 1996-2010, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California * Portions Copyright (c) 1994, Regents of the University of California
* *
* $PostgreSQL: pgsql/src/include/libpq/md5.h,v 1.7 2010/01/02 16:58:04 momjian Exp $ * $PostgreSQL: pgsql/src/include/libpq/md5.h,v 1.8 2010/01/27 12:12:00 mha Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
@ -23,6 +23,7 @@
extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum); extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum);
extern bool pg_md5_binary(const void *buff, size_t len, void *outbuf);
extern bool pg_md5_encrypt(const char *passwd, const char *salt, extern bool pg_md5_encrypt(const char *passwd, const char *salt,
size_t salt_len, char *buf); size_t salt_len, char *buf);