Commit a5c33b2b authored by ingo Voss's avatar ingo Voss
Browse files

ITS#8267 - unicodepw contrib module

parent 7df2a0f3
# $OpenLDAP$
# This work is part of OpenLDAP Software <http://www.openldap.org/>.
#
# Copyright 2021 The OpenLDAP Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted only as authorized by the OpenLDAP
# Public License.
#
# A copy of this license is available in the file LICENSE in the
# top-level directory of the distribution or, alternatively, at
# <http://www.OpenLDAP.org/license.html>.
LDAP_SRC = ../../..
LDAP_BUILD = $(LDAP_SRC)
LDAP_INC = -I$(LDAP_BUILD)/include -I$(LDAP_SRC)/include -I$(LDAP_SRC)/servers/slapd
LDAP_LIB = $(LDAP_BUILD)/libraries/libldap_r/libldap_r.la \
$(LDAP_BUILD)/libraries/liblber/liblber.la
LIBTOOL = $(LDAP_BUILD)/libtool
CC = gcc
OPT = -g -O2 -Wall -Wextra
DEFS = -DSLAPD_OVER_UNICODEPW=SLAPD_MOD_DYNAMIC
INCS = $(LDAP_INC)
LIBS = $(LDAP_LIB)
PROGRAMS = unicodepw.la
LTVER = 0:0:0
prefix=/usr/local
exec_prefix=$(prefix)
ldap_subdir=/openldap
libdir=$(exec_prefix)/lib
libexecdir=$(exec_prefix)/libexec
moduledir = $(libexecdir)$(ldap_subdir)
.SUFFIXES: .c .o .lo
.c.lo:
$(LIBTOOL) --mode=compile $(CC) $(OPT) $(DEFS) $(INCS) -c $<
all: $(PROGRAMS)
unicodepw.la: unicodepw.lo
$(LIBTOOL) --mode=link $(CC) $(OPT) -version-info $(LTVER) \
-rpath $(moduledir) -module -o $@ $? $(LIBS)
clean:
rm -rf *.o *.lo *.la .libs
install: $(PROGRAMS)
mkdir -p $(DESTDIR)$(moduledir)
for p in $(PROGRAMS) ; do \
$(LIBTOOL) --mode=install cp $$p $(DESTDIR)$(moduledir) ; \
done
This directory contains a slapd overlay, "unicodepw".
The overlay unicodepw restricts all LDAP modification requests, so that
only password changes for MS unicodePwd are possible. All other LDAP
requests will not be observed.
Usage:
man slapo-unicodepw (see slapo-unicodepw.5)
Build:
Use the Makefile to compile this plugin.
---
This work is part of OpenLDAP Software <http://www.openldap.org/>.
Copyright 2021 The OpenLDAP Foundation. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted only as authorized by the OpenLDAP
Public License.
Redistribution and use in source and binary forms, with or without
modification, are permitted only as authorized by the OpenLDAP
Public License.
A copy of this license is available in the file LICENSE in the
top-level directory of the distribution or, alternatively, at
<http://www.OpenLDAP.org/license.html>.
The attached patch file is derived from OpenLDAP Software.
All of the modifications to OpenLDAP Software represented in
the following patch(es) were developed by Ingo Voss ingo.voss@gmail.com.
I have not assigned rights and/or interest in this work to any party.
.TH UNICODEPW 5 "RELEASEDATE" "OpenLDAP LDVERSION"
.\" Copyright 2021 The OpenLDAP Foundation, All Rights Reserved.
.\" Copying restrictions apply. See the COPYRIGHT file.
.\" $OpenLDAP$
.SH NAME
unicodepw \- Overlay for openlap
.SH SYNOPSIS
The overlay
.B unicodepw
restricts all LDAP modification requests, so that only
password changes for MS unicodePwd are possible.
All other LDAP requests will not be observed.
.SH DESCRIPTION
Some remote access technologies for company networks (e.g. VPN gateways)
require a MS Active Directory Service (ADS) in the backend to log in with the
personal ADS account. In some cases (e.g. home office workers/max. password age),
it must be possible to allow password changes from remote.
But this requires "write access" (modify) from the gateway to the ADS.
A direct access from the gateway to the ADS is a bad idea, so using
OpenLDAP as proxy
.B (slapd-ldap)
in the DMZ can mitigate the security risks.
All LDAP search results, initiated by the gateway can be restricted by ACLs.
That will happen in the response from ADS. LDAP requests that only modify
(write, modify, ..) cannot be protecte with ACLs, because slapd-ldap does not
support restricting incoming request.
The backend slapd-ldap itself cannot be set to read-only, because changing the password requires "write" access.
The overlay
.B denyop
can only restrict LDAP requests, but it does not look
inside the request. Using overlay denyop would only restrict the access by
allowing ALL modifications to ADS, so that other manipulation are possible too.
That was the reason to write the extra overlay
.B unicodepw.
The
.B unicodepw
overlay to
.BR slapd (8)
services is checking the modification requests, if the modification request
is a password change for MS ADS. All other modifications are denied.
.LP
The conditions for changing the password in MS ADS are described in
https://msdn.microsoft.com/en-us/library/cc223248 and KB269190.
Microsoft stores the password in the attribute unicodePwd. It is not readable,
but writeable. The sequence requires ONE LDAP modification with TWO operations to
change the password:
.RS
.nf
dn: <userdn>
changetype: modify
delete: UnicodePwd
UnicodePwd::<old password>
-
add: UnicodePwd
UnicodePwd::<new password>
.fi
.RE
The old password is required, no matter what rights the changing context
(bind dn) in the ADS has.
The
.B unicodepw
checks
.RS
.nf
- The number of the operations: only TWO are allowed
- The type of operations: only DELETE and ADD are allowed
- The order of the operations: first DELETE, second ADD
- The attribute which is modified (configurable)
- The parent dn of the user who is changed (configurable)
.fi
.RE
If one of these checks fails, the overlay will deny the request BEFORE sending
the request to the ADS!
.SH CONFIGURATION
.LP
The
.B unicodepw
is configured in the ETCDIR/slapd.conf.
At the moment NO dynamic config support is available for the overlay unicodepw.
.TP
.B moduleload unicodepw
Load the module in the slapd context. Don't forget to set the
.B modulepath
option.
.TP
.B overlay unicodepw
This directive adds the unicodepw overlay to the current backend.
.TP
.B unicodepw pwattr <password attribute>
This directive configures the attribute which is checked in the modification. Usually "unicodePwd" should be
used here.
.TP
.B unicodepw userbase <DN, where the users in the ADS are located>
This directive configures the distinguished name of the user base.
With the userbase directive you can restrict password changes to a dedicated location in the
ADS. This location then should contain only users who use remote access.
For all other users in the ADS a password change is NOT possible from the VPN gateway (outside).
.TP
.B unicodepw subtree <yes|no>
If this directive is set to yes, password changes are allowed for all users under "userbase" and
all DNs below (subtree)
.TP
.B unicodepw logactivity <yes|no>
Enable the additional logging for all operations, checks and results from the unicodepw module.
The global parameter "log level" MUST be set to stats)!
This Parameter was intoduced to prevent the noise from the module during normal operation
of slapd (the default log level is already stats).
Since the module implements a security function, the admin likes to know and log, who is changing the password
and with witch result. Then set logactivity to "yes"!
.SH CONFIGURATION HINTS
Since the unicodepw only restricts the modify request, the module
.B denyop
must used too, to restrict all other unwanted LDAP requests.
Additionally, to configure slapd-ldap to proxy unicodePwd changes, at least the
unicodePwd must be defined in a private schema (see EXAMPLES).
All other LDAP operations comming from the access gateway should restict with ACLs!
For your own security, the connection between LDAP client (e.g. VPN gateway)
and OpenLDAP should be protected by SSL/TLS.
The connection between OpenLDAP and Microsoft ADS MUST be encrypted via SSL/TLS.
Microsoft does not allow password changes on a unencrypted session!
.SH EXAMPLES
Only the important parts for the slapd configuration:
.RS
.nf
#################################
# including private schema
#################################
include /etc/openldap/schema/myADschema.schema
#################################
# overlay configuration
#################################
modulepath /usr/lib/openldap/modules
moduleload back_ldap
moduleload denyop
moduleload unicodepw
overlay denyop
# possible denyops add,bind,compare,delete,
# extended,modify,modrdn,search,unbind
denyop add,compare,delete,modrdn
overlay unicodepw
unicodepw pwattr "UnicodePwd"
unicodepw userbase "ou=remoteUsers,dc=company,dc=com"
unicodepw logactivity "yes"
#################################
# proxy configuration
#################################
database ldap
rebind-as-user yes
suffix "dc=company,dc=com"
uri "ldap://ads1.company.com/ ldap://ads2.company.com"
chase-referrals no
protocol-version 3
.fi
.RE
Please don't forget to configure the ACLs in the proxy backend and the TLS configuration for slapd!
Parameters like:
.B TLSCACertificateFile, TLSCertificateFile, TLSCertificateKeyFile, TLSVerifyClient, TLSCipherSuite
should configured globally and
.B security, tls, access
in the database section!
For testing purposes, it can be useful to create the encoded MS unicode password.
The following commands should do it:
.TP
.nf
.B echo <password> | perl -ne 'chomp;print pack \*(lqv*\*(rq, unpack \*(lqC*\*(rq,\*(lq\e\*(rq$_\e\*(rq\*(rq' | base64
.fi
The output can be included in the modify request in the attribute unicodePwd.
Example:
.TP
.nf
.B echo password123_ | perl -ne 'chomp;print pack \*(lqv*\*(rq, unpack \*(lqC*\*(rq,\*(lq\e\*(rq$_\e\*(rq\*(rq' | base64
the resulting attribute is:
unicodePwd::IgBwAGEAcwBzAHcAbwByAGQAMQAyADMAXwAiAA==
.fi
.RE
In the section CONFIGURATION HINTS, we talked about a private schema. Here is an example for it:
.RS
.nf
attributetype ( 1.2.840.113556.1.4.750 NAME 'groupType'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )
attributetype ( 1.3.114.7.4.2.0.33 NAME 'memberOf'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.26' )
attributetype ( 1.2.840.113556.1.4.656 NAME 'userPrincipalName'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )
attributetype ( 1.2.840.113556.1.4.52 NAME 'lastLogon'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' )
attributetype ( 1.2.840.113556.1.4.159 NAME 'accountExpires'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' )
attributetype ( 1.2.840.113556.1.4.96 NAME 'pwdLastSet'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' )
attributetype ( 1.2.840.113556.1.4.221 NAME 'sAMAccountName'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )
attributetype ( 1.2.840.113556.1.4.8 NAME 'userAccountControl'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' )
attributetype ( 1.2.840.113556.1.4.90 NAME 'unicodePwd'
SYNTAX '1.3.6.1.4.1.1466.115.121.1.40' )
objectclass ( 1.2.840.113556.1.5.9 NAME 'user'
DESC 'a user'
SUP inetOrgPerson STRUCTURAL
MUST ( cn )
MAY ( userPassword $ memberOf $ userPrincipalName $
distinguishedName $ lastLogon $ accountExpires $
pwdLastSet $ sAMAccountName $ userAccountControl $
unicodePwd ) )
objectclass ( 1.2.840.113556.1.5.8 NAME 'group'
DESC 'a group of users'
SUP top STRUCTURAL
MUST ( groupType $ cn )
MAY ( member ) )
.fi
.RE
.SH LOGGING/DEBUG
If "stats" is enabled, each password change request and all checks are logged and if they fail, the reasons are logged too!
For informations about the values during modification, please refer to the log before unicodepw. Grep for the same
connection id and operation number (conn= and op=)!
.B Example for a logging output, every thing is ok:
.nf
conn=1005 op=2 unicodepw: INFO => configured UsersBase nDN => <ou=remoteUsers,dc=company,dc=com>
conn=1005 op=2 unicodepw: INFO => configured pwattr => <UnicodePwd>
conn=1005 op=2 unicodepw: INFO => configured logactivity => <1>
conn=1005 op=2 unicodepw: INFO => Parent DN from user, who is changed => <ou=remoteUsers,dc=company,dc=com>
conn=1005 op=2 unicodepw: INFO => User, who is changed <cn=remoteuser1,ou=remoteUsers,dc=company,dc=com>
conn=1005 op=2 unicodepw: OK => Attribute in Modification (DEL) is the configured pwattr!
conn=1005 op=2 unicodepw: OK => Attribute in Modification (ADD) is the configured pwattr!
conn=1005 op=2 unicodepw: ACCEPT => UnicodePwd changing is permitted!
.fi
.B Example for a logging output, user DN is wrong:
.nf
conn=1006 op=2 unicodepw: INFO => configured UsersBase nDN => <ou=remoteUsers,dc=company,dc=com>
conn=1006 op=2 unicodepw: INFO => configured pwattr => <UnicodePwd>
conn=1006 op=2 unicodepw: INFO => configured logactivity => <1>
conn=1006 op=2 unicodepw: INFO => Parent DN from user, who is changed => <ou=Users,dc=company,dc=com>
conn=1006 op=2 unicodepw: INFO => User, who is changed <cn=normaluser,ou=Users,dc=company,dc=com>
conn=1006 op=2 unicodepw: DENY => UserBase from <cn=normaluser,ou=Users,dc=company,dc=com> is not in configured UserBase!
conn=1006 op=2 unicodepw: OK => Attribute in Modification (DEL) is the configured pwattr!
conn=1006 op=2 unicodepw: OK => Attribute in Modification (ADD) is the configured pwattr!
.fi
.SH FILES
.TP
ETCDIR/slapd.conf
default slapd configuration file
.SH SEE ALSO
.BR slapd.conf (5),
.BR slapd\-config (5),
.BR slapd\-ldap (5),
.BR slapd (8),
overlay
.BR denyop
and https://msdn.microsoft.com/en-us/library/cc223248 and KB269190 for description of password change.
.SH AUTHOR
This module is written in 2016-2020 by Ingo Voss (ingo.voss@gmail.com)
/* unicodepw.c - acceppt only password change for MS Active Directory */
/* $OpenLDAP$ */
/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
*
* Copyright 2021 The OpenLDAP Foundation.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted only as authorized by the OpenLDAP
* Public License.
*
* A copy of this license is available in the file LICENSE in the
* top-level directory of the distribution or, alternatively, at
* <http://www.OpenLDAP.org/license.html>.
*/
/* ACKNOLEDGEDMENTS:
* This work was initially developed by Ingo Voss (ingo.voss@gmail.com)
* for inclusion in OpenLDAP Software.
*/
#include "portable.h"
#ifdef SLAPD_OVER_UNICODEPW
#include <stdio.h>
#include <ac/string.h>
#include <ac/socket.h>
#include "slap.h"
#include "lber.h"
typedef struct unicodepw_conf {
struct berval attr;
struct berval userbase;
int log_activity;
int subtree_search;
} unicodepw_conf;
static int
unicodepw_mod( Operation *op, SlapReply *rs ) {
slap_overinst *on = (slap_overinst *) op->o_bd->bd_info;
unicodepw_conf *u_conf = (unicodepw_conf *) on->on_bi.bi_private;
int deny = 0;
int i = 1;
Modifications *m;
/* from Config for container, where users are located */
struct berval user_base_ndn;
dnNormalize( 0, NULL, NULL, &u_conf->userbase, &user_base_ndn, op->o_tmpmemctx );
/* get parent DN from user, who is changed */
struct berval user_parent_ndn;
dnParent( &op->o_req_ndn, &user_parent_ndn );
if ( u_conf->log_activity == 1 ) {
/* logging config */
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => configured UsersBase nDN => <%s>\n",
op->o_connid, op->o_opid, user_base_ndn.bv_val );
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => configured pwattr => <%s>\n",
op->o_connid, op->o_opid, u_conf->attr.bv_val );
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => configured logactivity => <%d>\n",
op->o_connid, op->o_opid, u_conf->log_activity );
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => configured subtree => <%d>\n",
op->o_connid, op->o_opid, u_conf->subtree_search );
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => User, who is changed <%s>\n",
op->o_connid, op->o_opid, op->o_req_ndn.bv_val );
/* logging User and parent DN */
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO => Parent DN from user, who is changed => <%s>\n",
op->o_connid, op->o_opid, user_parent_ndn.bv_val );
}
/* check if user is in configured UserBase */
if ( u_conf->subtree_search == 1 ) {
int found = 0;
while (!BER_BVISEMPTY( &user_parent_ndn )) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DEBUG => <%s>\n",
op->o_connid, op->o_opid, user_parent_ndn.bv_val );
}
if ( strcmp( user_parent_ndn.bv_val, user_base_ndn.bv_val ) == 0 ) {
found = 1;
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: INFO: Found User under => <%s>\n",
op->o_connid, op->o_opid, user_parent_ndn.bv_val );
}
break;
}
dnParent( &user_parent_ndn , &user_parent_ndn );
}
if ( found == 0 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => UserBase from <%s> is not in configured UserBase\n",
op->o_connid, op->o_opid, op->o_req_dn.bv_val );
}
deny = 1;
}
} else {
if ( strcmp( user_parent_ndn.bv_val, user_base_ndn.bv_val ) != 0 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => UserBase from <%s> is not in configured UserBase\n",
op->o_connid, op->o_opid, op->o_req_dn.bv_val );
}
deny = 1;
}
}
/* load Modifications and process */
if ( !(m = op->orm_modlist) ) {
op->o_bd->bd_info = (BackendInfo *) on->on_info;
send_ldap_error( op, rs, LDAP_INVALID_SYNTAX, "unique_modify() got null op.orm_modlist" );
return rs->sr_err;
}
/* successful load */
for ( ; m; m = m->sml_next ) {
/* only attribute UnicodePwd allowed! */
if ( strcasecmp( m->sml_type.bv_val, u_conf->attr.bv_val ) != 0 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => Attribute in Modification (%s) is not the configured pwattr!\n",
op->o_connid, op->o_opid,
m->sml_op == LDAP_MOD_ADD ? "ADD" : (m->sml_op == LDAP_MOD_DELETE ? "DEL" : "other") );
}
deny = 1;
} else {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: OK => Attribute in Modification (%s) is the configured pwattr!\n",
op->o_connid, op->o_opid,
m->sml_op == LDAP_MOD_ADD ? "ADD" : (m->sml_op == LDAP_MOD_DELETE ? "DEL" : "other") );
}
}
/* only DEL and ADD allowed */
if ( m->sml_op == LDAP_MOD_DELETE || m->sml_op == LDAP_MOD_ADD ) {
if ( m->sml_op == LDAP_MOD_DELETE && i != 1 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => Modification DEL not in first place!%s\n",
op->o_connid, op->o_opid, "" );
}
deny = 1;
} else if ( m->sml_op == LDAP_MOD_ADD && i != 2 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => Modification ADD not in second place!%s\n",
op->o_connid, op->o_opid, "" );
}
deny = 1;
}
} else {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => Modification not in ADD or DEL!%s\n",
op->o_connid, op->o_opid, "" );
}
deny = 1;
}
i++;
}
if ( i != 3 ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: DENY => More or less than TWO modifications!%s\n",
op->o_connid, op->o_opid, "" );
}
deny = 1;
}
if ( !deny ) {
if ( u_conf->log_activity == 1 ) {
Debug( LDAP_DEBUG_STATS,
"conn=%lu op=%lu unicodepw: ACCEPT => UnicodePwd changing is permitted!%s\n",
op->o_connid, op->o_opid, "" );
}
return SLAP_CB_CONTINUE;
}
op->o_bd->bd_info = (BackendInfo *)on->on_info;
send_ldap_error( op, rs, LDAP_UNWILLING_TO_PERFORM, "operation not allowed by unicodepw!" );
return 0;
}
static int
unicodepw_config(
BackendDB *be,
const char *fname,
int lineno,
int argc,
char **argv )
{
slap_overinst *on = (slap_overinst *) be->bd_info;
unicodepw_conf *u_conf = (unicodepw_conf *) on->on_bi.bi_private;
Debug( LDAP_DEBUG_CONFIG,
"\tlline: %d,\t argv[1]: %s,\t argv[2]: %s\n",
lineno, argv[1], argv[2] );
if ( strcasecmp( argv[0], "unicodepw" ) == 0 ) {
if ( argc != 3 ) {
Debug( LDAP_DEBUG_ANY,
"%s: line %d: " "wrong configuration line, use: " "\"unicodepw <param> <value>\" line.%s\n",
fname, lineno, "" );
return 1;
}
Debug( LDAP_DEBUG_CONFIG,
"unicodepw: config => param: %s, value: %s%s\n",
argv[1] ,argv[2], "" );
if ( strcasecmp(argv[1], "pwattr" ) == 0 ) {
if ( u_conf->attr.bv_val ) {
/* if already defined! */
ch_free( u_conf->attr.bv_val );
}