Thursday, 1 August 2013

OpenLDAP ldap backend: SASL authentication

The problem

The OpenLDAP ldap backend acts as a reverse proxy to a LDAP server containing real data. The simple binds are simply verbatim transferred to the data server and users don't even feel there is a proxy in-between.
The problem is with SASL. With a SASL bind the proxy doesn't have a password to forward to the data server. Even if proxy authenticates user with GSSAPI or EXTERNAL, proxy obtains a dn but no password. So it has to perform an anonymous bind which is different from an authenticated one. This behaviour is warned by the log line:
ldap_back_dobind_int: DN="cn=admin,dc=example,dc=org" without creds, binding anonymously

Overview

The solution uses the id_assert directive in the ldap badabase section on the proxy in order to bind to the data server with a defined principal who then proxies to the original client identity thanks to the authzTo attribute.
The OpenLDAP need three modifies:
  • id_assert section in ldap database (proxy server)
  • creation of the id_assert principal with the authzTo attribute (data server)
  • enable authzPolicy to in cn=config (data server)

ldifs needed

To enable authzPolicy on the data server:
dn: cn=config
changetype: modify
replace: olcAuthzPolicy
olcAuthzPolicy: to
This is necessary because OpenLDAP disregards authzTo attribute in a entry unless the global olcAuthzPolicy is set.
To enable id_assert on ldap database on proxy server:
dn: olcDatabase={2}ldap,cn=config
changetype: modify
replace: olcDbIDAssertAuthzFrom
olcDbIDAssertAuthzFrom: "dn:*"
-
replace: olcDbIDAssertBind
olcDbIDAssertBind: mode=self bindmethod=simple binddn="cn=proxy,ou=agents,dc=example,dc=org" credentials=KHg7VvVD
The id_assert is in effect only for authenticated users to avoid using a privileged bind for anonymous users.
The proxy user is defined as follow:
dn: cn=proxy,ou=agents,dc=example,dc=org
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: proxy
description: principal for proxy ldap
userPassword: {SSHA}LCLFm/lhsZqr4ltuGoyrr/zKGZgJSZ3c
authzTo: {0}dn.regex:^uid=[^,]*,ou=people,dc=example,dc=org$
authzTo: {1}dn.regex:^cn=[^,]*,ou=agents,dc=example,dc=org$
view raw proxy.ldif hosted with ❤ by GitHub

Check log entries

This is a log entry for the user "a_user" bound with GSSAPI requesting his own entry:
Log on proxy:
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 fd=19 ACCEPT from PATH=/var/run/slapd/ldapi (PATH=/var/run/slapd/ldapi)
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=0 SRCH base="" scope=0 deref=0 filter="(objectClass=*)"
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=0 SRCH attr=supportedSASLMechanisms
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=0 ENTRY dn=""
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=0 SEARCH RESULT tag=101 err=0 nentries=1 text=
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=1 BIND dn="" method=163
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=1 RESULT tag=97 err=14 text=SASL(0): successful result: security flags do not match required
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=2 BIND dn="" method=163
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=2 RESULT tag=97 err=14 text=SASL(0): successful result: security flags do not match required
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=3 BIND dn="" method=163
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=3 BIND authcid="a_user" authzid="a_user"
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=3 BIND dn="uid=a_user,ou=people,dc=example,dc=org" mech=GSSAPI sasl_ssf=56 ssf=71
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=3 RESULT tag=97 err=0 text=
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=4 SRCH base="dc=example,dc=org" scope=2 deref=0 filter="(uid=a_user)"
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=4 ENTRY dn="uid=a_user,ou=people,dc=example,dc=org"
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=4 SEARCH RESULT tag=101 err=0 nentries=1 text=
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 op=5 UNBIND
Aug 1 10:36:59 proxy slapd[23946]: conn=1057 fd=19 closed
view raw proxy.log hosted with ❤ by GitHub

Log on data server:
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 fd=22 ACCEPT from IP=10.0.0.1:43682 (IP=0.0.0.0:389)
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=0 EXT oid=1.3.6.1.4.1.1466.20037
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=0 STARTTLS
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=0 RESULT oid= err=0 text=
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 fd=22 TLS established tls_ssf=128 ssf=128
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=1 BIND dn="cn=proxy,ou=agents,dc=example,dc=org" method=128
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=1 BIND dn="cn=proxy,ou=agents,dc=example,dc=org" mech=SIMPLE ssf=0
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=1 RESULT tag=97 err=0 text=
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=2 PROXYAUTHZ dn="uid=a_user,ou=people,dc=example,dc=org"
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=2 SRCH base="dc=example,dc=org" scope=2 deref=0 filter="(uid=a_user)"
Aug 1 10:36:44 openldap slapd[19002]: conn=1662 op=2 SEARCH RESULT tag=101 err=0 nentries=1 text=
view raw openldap.log hosted with ❤ by GitHub
Note bind is performed by cn=proxy,ou=agents,dc=example,dc=org but then authorization is performed on behalf to "a_user".

Wednesday, 24 July 2013

OpenLDAP ldap backend as a proxy

Ldap backend works as a proxy: when a client searches data, proxy forwards request to ldap servers with real data, which are served to client. This is useful for:
  • high availability: ldap backend spots faulty servers and picks the first working in a list
  • firewalling: clients connect to a single IP no matter how many ldap server are involved.
Before using ldap backend, you have to enable it: create a ldif named 'add_module_ldap.ldif' and apply with: sudo ldapadd -H ldapi:/// -Y EXTERNAL -f add_module_ldap.ldif
dn: cn=module{1},cn=config
objectClass: olcModuleList
cn: module{1}
olcModulePath: /usr/lib/ldap
olcModuleLoad: back_ldap
Now you are ready to create to database by inserting the following ldif.
dn: olcDatabase={1}ldap,cn=config
objectClass: olcDatabaseConfig
objectClass: olcLdapConfig
olcDatabase: {1}ldap
olcSuffix: dc=example,dc=org
olcRootDN: cn=ldap-admin
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
,cn=auth manage by * break
olcAccess: {1}to * by * read
olcDbStartTLS: start
olcDbUri: "ldap://ldap1.example.org ldap://ldap2.example.org"
view raw ldap_db.ldif hosted with ❤ by GitHub
Now queries matching the basename "dc=example,dc=org" are forwarded to the first available server between ldap1.example.org or ldap2.example.org. The proxy might take some time to spot faulty server (maybe it has to wait for a timeout), but since the next call it forwards to the last used server, the first working one. Please note:
  • the "allow all" acl is required because ldap backend perform authorization. A request is fulfilled if both the proxy and the data server allow it. Serious acl are supposed to be on the data server only (it helps sanity);
  • remember to encrypt connection between proxy and data server with 'olcDbStartTLS: start'

Tuesday, 23 July 2013

pkcs11 ssh authentication

This post is about using ssh without password, with a certificate stored on a smartcard. I think it could be really useful with notebooks. You can securely connect to ssh servers without storing the ssh secret key on the notebook, which can be stolen, can be lost etc.

Setup

Of course you have to be able to read the certificate on the smartcard so:
  • get a smartcard reader; be sure it is supported. We lost a lot of time because ACR38UR didn't work (ACR38UC works fine);
  • install pcscd;
  • get the crypto api for yor smartcard. 'opensc' should work fine, sometimes the certificate issuer require other libraries (Italian CNS work with libbit4ipki.so -- you can find it with the software 'dike').

Get public keys

$ ssh-keygen -D /usr/lib/libbit4ipki.so
ssh-rsa AAAAB3NzaC1yc2[...]J6KIcjjROKtdJ2CHOftZExSkNyNNQ==
ssh-rsa AAAAB3NzaC1yc2[...]kRxbZfOVWb8X5C4X++iiXS4UDpWhQ==
Copy one of the line beginning with "ssh-rsa" to the '.ssh/authorized_keys' on the ssh server (chmod 600).

Load private keys

$ ssh-agent /bin/bash
$ ssh-add -s /usr/lib/libbit4ipki.so
Enter passphrase for PKCS#11:
Card added: /usr/lib/libbit4ipki.so
$ ssh-add -l
1024 f8:8a:e3:[...]:cb:ab:db:67:da:3e /usr/lib/libbit4ipki.so (RSA)
1024 bc:9f:e9:[...]:27:7a:13:55:81:bf /usr/lib/libbit4ipki.so (RSA)
Then you can happily login to ssh server with a simple ssh command.

Wednesday, 10 July 2013

OpenLDAP: force TLS on authentication only

Imagine your directory has public data which can be accessed anonymously. Suppose there are also confidential data whose access requires authentication.

You want authenticated access to be on the secure channel (to protect both password and data from sniffing) while you don't want to enforce TLS to anonymous access to public data (maybe some clients are hard to configure properly for TLS).

Setting:
olcSecurity: ssf=36
in cn=config would require all user to use TLS: otherwise OpenLDAP issues a "confidentiality required" error. This setting is maybe overkill.

TLS can be enforced with ACL as well.

Create a ldif file named "add_tls_for_auth.ldif" as following:
dn: olcDatabase={1}hdb,cn=config
changetype: modify
delete: olcAccess
olcAccess: {1}to attrs=userPassword,shadowLastChange by self write by anonymou
s auth by dn="cn=admin,dc=example,dc=org" write by * none
-
add: olcAccess
olcAccess: {1}to attrs=userPassword,shadowLastChange by ssf=128 break by pee
rname.ip="127.0.0.1" break by * none
olcAccess: {2}to attrs=userPassword,shadowLastChange by self write by anonymou
s auth by dn="cn=admin,dc=example,dc=org" write by * none
and apply to config with:
ldapmodify -H ldapi:/// -Y EXTERNAL -f add_tls_for_auth.ldif
(this code assumes the default acl setup by Debian).

Explanation

The break keyword means that if you match that rule you should check next rule for the same what. So, if your ssf is strong enought or your IP is 127.0.0.1 you are allowed to check next rule about access to attrs=userPassword,shadowLastChange. Otherwise the none means userPassword is not returned so no authentication can ever succeed.
In short the break keyword is a kind on logical AND between two rules.

Notes

There are two points to note:
  • Users are still allowed to try connection with clear text password on ldap://. Simply authntication never succeed so in a while they should stop;
  • To enable ldapi:/// authenticated connection you might need to set olcLocalSSF=128 in cn=config:
    dn: cn=config
    changetype: modify
    replace: olcLocalSSF
    olcLocalSSF: 128

Tuesday, 2 July 2013

Basic setup in Debian slapd package

After issuing apt-get install slapd a few steps are required in order to:
  • change basename suffix;
  • enable logging;
  • speed up admin authentication.
The Debian version is release 7 Wheezy.

Change basename suffix

Package creates a database with suffix aligned to domain name. Domain name is read from /etc/resolv.conf or the like. If you want to change it, the dpkg command can help you:
sudo dpkg-reconfigure slapd
The second time you can choose the domain name.

Enable logging

To enable logging, create a ldif modify file:
dn: cn=config
changetype: modify
replace: olcLogLevel
olcLogLevel: Stats
name it enable_log.ldif and apply to openLDAP with:
sudo ldapmodify -H ldapi:/// -Y EXTERNAL -f enable_log.ldif
Next ensure slapd sends log to a facility, for example local6. This is done in /etc/default/slapd:
# Additional options to pass to slapd
SLAPD_OPTIONS="-l local6"
(then restart slapd). By the way, to avoid filling the hard drive with openldap log, instruct logrotate to handle them: drop in /etc/logrotate.d/ a file called 'ldap':
/var/log/ldap.log
{
       rotate 90
       daily
       missingok
       notifempty
       delaycompress
       compress
}

Speed up admin authentication

In order to avoid typing admin password to populate directory, authorize SASL/EXTERNAL with root access to do that. Create a ldif file (enable_sasl_acl.ldif):
dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
 ,cn=auth manage by * break
and switch it on with:
sudo ldapmodify -H ldapi:/// -Y EXTERNAL -f enable_sasl_acl.ldif
Now you are able to add ou, users and groups without password as long you are root, with '-H ldapi:/// -Y EXTERNAL' switch.

Monday, 24 June 2013

Securing the ActiveMQ 5.8.0 web console using LDAP based authentication with Ldaptive

This is a follow-up of the great post by Torsten Mielke, updated for the 5.8.0 activemq release with the great ldaptive ldap java library.
First of all, drop the ldaptive.jar in the ${activemq-home}/lib directory.
Next you need to edit two files:
  • login.config
  • jetty.xml
In jetty.xml, the key task is to substitute to the default securityLoginService based on a org.eclipse.jetty.security.HashLoginService a section like:
<bean id="defaultIdentityService" class="org.eclipse.jetty.security.DefaultIdentityService" />
<bean id="securityLDAPLoginService" class="org.eclipse.jetty.plus.jaas.JAASLoginService">
<property name="name" value="ActiveMQLdapRealm" />
<property name="LoginModuleName" value="jetty-ldap" />
<property name="identityService" ref="defaultIdentityService" />
<property name="roleClassNames" value="org.ldaptive.jaas.LdapRole" />
</bean>
<bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint">
<property name="name" value="BASIC" />
<property name="roles" value="admins" />
<property name="authenticate" value="true" />
</bean>
[...]
<bean id="securityHandler" class="org.eclipse.jetty.security.ConstraintSecurityHandler">
<property name="realmName" value="ActiveMQLdapRealm" />
<property name="loginService" ref="securityLDAPLoginService" />
<property name="authenticator">
<bean class="org.eclipse.jetty.security.authentication.BasicAuthenticator" />
</property>
[...]
</bean>
where jetty-ldap has to match the label in login.config, and roleClassNames match the class role exported by ldaptive.
The property 'roles' should include the group names from ldap which define if a member is allowed to access web console. In this example, a user should be a member of the admins group.
Please note identityService has to be present even if it is default.
The login.config is:
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
activemq-local {
org.apache.activemq.jaas.PropertiesLoginModule required
debug=true
org.apache.activemq.jaas.properties.user="org/apache/activemq/security/users.properties"
org.apache.activemq.jaas.properties.group="org/apache/activemq/security/groups.properties";
};
jetty-ldap {
org.ldaptive.jaas.LdapLoginModule required
debug=true
storePass="true"
ldapUrl="ldap://ldap1.test.com:389 ldap://ldap2.test.com:389"
connectionStrategy="ACTIVE_PASSIVE"
baseDn="ou=people,dc=test,dc=com"
useStartTLS="true"
credentialConfig="{trustCertificates=file:/etc/ssl/certs/ca-chain.pem}"
userFilter="(uid={user})";
org.ldaptive.jaas.LdapRoleAuthorizationModule required
useFirstPass="true"
ldapUrl="ldap://ldap1.test.com:389 ldap://ldap2.test.com:389"
connectionStrategy="ACTIVE_PASSIVE"
bindDn="cn=jetty-jaas,ou=agents,dc=test,dc=com"
baseDn="ou=groups,dc=test,dc=com"
bindCredential="secret"
roleFilter="(memberUid={user})"
useStartTLS="true"
credentialConfig="{trustCertificates=file:/etc/ssl/certs/ca-chain.pem}"
roleAttribute="cn";
};
view raw login.config hosted with ❤ by GitHub
ldap1 and ldap2 are contacted via start_tls, with a ACTIVE_PASSIVE strategy, the groups are in the ou=groups,dc=test,dc=com and a authentication principal is required to browse them. "storepass=true" in the LdapLoginModule is required in order to allow to the filter 'memberUid={user}' to resolve the {user} placeholder.

The hardest part of this setup is the role mapping from group membership. These errors show up with the following error in web console following a successful login:
Problem accessing /. Reason:      !role
In this example groups/role mappings are in web.xml. They could be done in login.config as well with the following lines:
            roleFilter="(&(cn=admin_group)(memberUid={user}))"
     defaultRole="admin" 
If a user matches the filter (she's member of the admin_group group) is added to the admin role which in turn should match the roles property in securityConstraints.

The following lines in your logback.xml can help you a lot to spot errors:
<logger name="org.ldaptive" additivity="false">
<level value="DEBUG"/>
<appender-ref ref="R" />
<appender-ref ref="stdout" />
</logger>

Link to the complete jetty.xml.
Ldaptive jaas guide.

Thursday, 23 May 2013

ldaptive 1.0 test

ldaptive is a java ldap library which replaces vt-ldap shipped with shibboleth and grouper.

There are some nice surprises, like the handling of the ldap extended operation of Password Modify and Password Policy, used by openldap to check if a user's password is locked or need to be changed.

I have tried a ldapsearch authenticated with SASL TLS/EXTERNAL:

require 'java'
require 'logback-core-1.0.9.jar'
require 'logback-classic-1.0.9.jar'
require 'slf4j-api-1.7.4.jar'
require 'ldaptive-1.0.jar'
import 'org.slf4j.Logger'
import 'org.slf4j.LoggerFactory'
import 'ch.qos.logback.classic.LoggerContext'
import 'ch.qos.logback.core.util.StatusPrinter'
java_import "org.ldaptive.DefaultConnectionFactory"
java_import "org.ldaptive.ConnectionConfig"
java_import "org.ldaptive.SearchOperation"
java_import "org.ldaptive.SearchRequest"
java_import "org.ldaptive.SearchResult"
java_import "org.ldaptive.LdapEntry"
java_import "org.ldaptive.LdapAttribute"
java_import "org.ldaptive.io.ValueTranscoder"
java_import "org.ldaptive.ssl.KeyStoreCredentialConfig"
java_import "org.ldaptive.ssl.SslConfig"
java_import "org.ldaptive.sasl.ExternalConfig"
java_import "org.ldaptive.BindOperation"
java_import "org.ldaptive.BindRequest"
class NoOpStringValueTranscoder
include org.ldaptive.io.ValueTranscoder
def decodeStringValue(string)
string
end
def getType
"a_string".class
end
end
logger = LoggerFactory.getLogger("lsearch")
conn_config = ConnectionConfig.new("ldap://ldap.test.com")
conn_config.setUseStartTLS(true)
cred_config = KeyStoreCredentialConfig.new
cred_config.setKeyStore("file:cluster.keystore")
cred_config.setKeyStorePassword("secret")
conn_config.setSslConfig(SslConfig.new(cred_config))
conn = DefaultConnectionFactory.getConnection(conn_config)
conn.open
bind = BindOperation.new conn
bind.execute(BindRequest.new(ExternalConfig.new))
search = SearchOperation.new(conn)
result = search.execute(
SearchRequest.new("dc=test,dc=com","uid=a_user", "mail", "isMemberOf")
).result
ldap_entry = result.entry
p ldap_entry
p ldap_entry.getDn
ldap_entry.attributes.each do |attr|
puts "#{attr.name} => #{attr.getValues(NoOpStringValueTranscoder.new).join(", ")}"
end
view raw lsearch.rb hosted with ❤ by GitHub

Please note that the private key password (the one you set to create the pkcs12) and the keystore password have to be the same.

Friday, 15 February 2013

Convert a openssl PEM RSA key to java keystore

Starting from jdk6, keytool allows importing pkcs12 bundles. The pkcs12 bundle includes key, certificate and CA chain so it is possible to require a certificate with openssl than using it with activemq, or other java projects.

The procedure is outlined the jetty ssl page, but there is a important point to note.

The keystore is password protected with the password set during keystore creation. The private key is password protected with the export password typed in openssl pkc12 command.

If a tool doesn't allow to specify both password, they must be the same.

The case is the activemq sslContext, which reads:

<sslContext>
                <sslContext 
                        keyStore="ateneo.store" keyStorePassword="bluehorror"
                        trustStore="client.ks" trustStorePassword="password"/>
        </sslContext>
The trick is to issue a keytool -keypasswd to align password. Failing to do that results in a:
Exception in thread "main" java.security.UnrecoverableKeyException: Cannot recover key
 at sun.security.provider.KeyProtector.recover(KeyProtector.java:311)
 at sun.security.provider.JavaKeyStore.engineGetKey(JavaKeyStore.java:121)
 at sun.security.provider.JavaKeyStore$JKS.engineGetKey(JavaKeyStore.java:38)
 at java.security.KeyStore.getKey(KeyStore.java:763)
Thanks to http://www.xinotes.org/notes/note/1395/

In case you want to follow the single steps, have a look at the puppet module to automatize certs export to keystore: https://github.com/francescm/certs