As part of my OpenLDAP under Ubuntu Linux project, this post documents configuring Samba to use LDAP - as a storage back-end, as well as for authentication and authorization. (Samba is a free software re-implemenation of the SMB networking protocol, and is useful for providing network file shares that are recognized by Microsoft Windows.) As with my previous posts, this post was written against Ubuntu Linux's latest release, 10.10 ("Maverick Meerkat").
This post is focused on integrating SMBD (Samba's SMB/CIFS server) with LDAP, not with Samba or SMB itself. A few resources for Samba include:
- http://www.samba.org/
- "Samba File Server" in the Ubuntu Server Guide
- "Samba" in the Ubuntu Community Documentation
Additionally, there are already a few references specific to combining Samba and LDAP, which proved useful when I first started working on this:
- "Samba & LDAP" (wiki.samba.org)
- The Linux Samba-OpenLDAP Howto (Jérôme Tournier & Olivier Lemaire, 2007-07-12, gna.org)
- This is titled as revision 1.10. I also found references to a revision 1.21, but it actually appears older. There is no "print date", but I've only found it as a PDF on mirrors with a created metadata date of 2006-06-26.
As documented by Samba, Samba supports a few different account / password backends - including Ubuntu's default tdbsam (a "trivial database"), and for the purposes of this post, ldapsam.
First, we need to extend the LDAP schema to include Samba's object classes and their dependencies.
Following my notes in OpenLDAP under Ubuntu Linux, the LDAP configuration, by default, including the schema, is stored in LDAP itself (a.k.a. "cn=config").
Unfortunately, documentation and examples using cn=config are still quite rare.
This is complicated by the fact that schema additions or changes, just as with data, are now handled through LDIF files.
Previously, schema files were available in ".schema
" files, which used a custom format.
When using cn=config, LDIF files need to be used.
At least in Ubuntu, OpenLDAP comes with both versions (.schema
and .ldif
) for many of the "core" schemas, including "core", "cosine", "inetorgperson", "misc", "nis", and "openldap".
However, this list doesn't include the "samba" schema, or other schemas that may be necessary in the future - so they need to be converted.
As discussed in Debian bug # 190162, finding even the samba.schema
source file is a bit difficult.
The easiest (but somewhat inefficient) way is to install the samba-doc
package, then uncompress "/usr/share/doc/samba-doc/examples/LDAP/samba.schema.gz
".
(Copy the file to a workspace directory, then run "gunzip samba.schema.gz
".)
The best process I found for converting individual schema files to LDIF format is as follows (similar to the steps documented in OpenLDAP Server in the Ubuntu Server Guide):
- In a new "workspace" directory, create a "
schemaConvert.conf
" file. Add "include" lines for each schema to be converted, including any dependencies:include /etc/ldap/schema/core.schema include /etc/ldap/schema/cosine.schema include /etc/ldap/schema/nis.schema include /etc/ldap/schema/inetorgperson.schema include samba.schema
- Use
slaptest
(part of OpenLDAP), which includes functionality to convert to "config directory" (cn=config) format:$ mkdir config && slaptest -f schemaConvert.conf -F config configuration file testing succeeded
- Find the generated
.ldif
files in 'config/cn=config/cn=schema
'. - Copy the necessary file(s) to the root of the workspace directory for convenience and reference:
cp diff 'config/cn=config/cn=schema/cn={4}samba.ldif' samba.ldif samba.ldif
- For each needed file, some edits are necessary:
- On the 1st line (beginning with "
dn:
"), remove the index surrounded by curly braces, and append ",cn=schema,cn=config
" to the end of the line. - On the 3rd line (beginning with "
cn:
"), remove the index surrounded by curly braces. - At the end of the file, remove the line beginning with "
structrualObjectClass
", and any following lines - there should be 7 total.
$ diff 'config/cn=config/cn=schema/cn={4}samba.ldif' samba.ldif 1c1 < dn: cn={4}samba --- > dn: cn=samba,cn=schema,cn=config 3c3 < cn: {4}samba --- > cn: samba 186,192d185 < structuralObjectClass: olcSchemaConfig < entryUUID: a0d0349c-a737-102f-97c0-cf017cbccf86 < creatorsName: cn=config < createTimestamp: 20101229013514Z < entryCSN: 20101229013514.341084Z#000000#000#000000 < modifiersName: cn=config < modifyTimestamp: 20101229013514Z
- On the 1st line (beginning with "
Add the converted and modified samba.ldif
using ldapadd
, or your favorite LDAP editor e.g. phpLDAPadmin.
(This assumes that the "core", "cosine", "inetorgperson", and "nis" schemas were already included in the default OpenLDAP configuration.)
sudo ldapadd -Y EXTERNAL -H ldapi:/// -f samba.ldif
Add some appropriate security controls.
This is what my "olcAccess
" looked like on my "olcDatabase={1}hdb,cn=config" after the initial setup of OpenLDAP:
{0}to dn.subtree="ou=people,dc=example,dc=com" attrs=userPassword,shadowLastChange by group.exact="cn=ldap.admins,ou=groups,dc=example,dc=com" write by anonymous auth by self write by * none {1}to attrs=userPassword,shadowLastChange by group.exact="cn=ldap.admins,ou=groups,dc=example,dc=com" write by anonymous auth by * none {2}to * by group.exact="cn=ldap.admins,ou=groups,dc=example,dc=com" write by users read
I recommend making the following additions and updates, partially inspired by my previous password permissions configuration example:
{0}to * by group.exact="cn=ldap.admins,ou=groups,dc=example,dc=com" write by * break {1}to dn.one="dc=example,dc=com" filter=(objectClass=sambaDomain) by group.exact="cn=samba.admins,ou=groups,dc=example,dc=com" write by * break {2}to attrs=@sambaSamAccount,userPassword by group.exact="cn=samba.admins,ou=groups,dc=example,dc=com" write by * break {3}to dn.subtree="ou=people,dc=example,dc=com" attrs=userPassword by self write by * break {4}to attrs=userPassword,shadowLastChange,sambaNTPassword,sambaLMPassword,sambaPwdLastSet,sambaPwdMustChange by self read by anonymous auth by * none {5}to * by users read
The "ldap.admins
" and "samba.admins
" groups need to be created using the "groupOfNames
" objectClass.
Add a "samba.proxy
" user (use the "applicationProcess
" and "simpleSecurityObject
" objectClasses), and make it a member of the "samba.admins
" group, as well as "ldap.admins
" temporarily.
(Unfortunately, the above {1}
access rule only allows for modification of the sambaDomain
, not its creation.
For only a one-time use, I haven't yet found a proper access rule that allows for its restricted creation.)
Modify "/etc/samba/smb.conf
" to use the "ldapsam" backend.
Find the existing "passdb backend = tdbsam
", comment it out, then add the following lines:
passdb backend = ldapsam:ldap:/// ldap ssl = Off ldap suffix = dc=example,dc=com ldap admin dn = cn=samba.proxy,ou=serviceAccounts,dc=example,dc=com ldap passwd sync = only #ldap debug level = 1
For any serious usage, or any usage with real user accounts, "ldap ssl
" needs to be properly configured to protect user credentials between the SMBD and LDAP servers.
The above "ldap passwd sync = only
", per the ldapsam
documentation, means:
Only update the LDAP password and let the LDAP server worry about the other fields.
This option is only available on some LDAP servers and only when the LDAP server supports LDAP_EXOP_X_MODIFY_PASSWD
.
OpenLDAP supports this, but the SambaNTPasword
and SambaLMPassword
fields will only be updated with an OpenLDAP overlay such as smbk5pwd
.
Unfortunately, this overlay is not currently installed with Ubuntu's distribution - see Ubuntu bug # 82853 for details.
A few additional notes if building this overlay from source yourself:
- By default, this overlay is configured to support both Kerberos and Samba, both at compile-time and run-time.
The dependencies for Kerberos are complex and numerous, at least under Ubuntu.
If you don't have a need for Kerberos, I'd recommend compiling without Kerberos support.
This can be done by running
make
with aDEFS
argument of only "-DDO_SAMBA
" from thesmbk5pwd
source directory. Unfortunately, this currently does not exclude the Kerberos includes or libraries dependencies from the make file. To remedy this, I also set "HEIMDAL_INC
" and "HEIMDAL_LIB
" to empty strings.- If dependencies are met at compile-time, and not at run-time - even if not enabled,
slapd
will fail to load with "lt_dlopenext failed: (smbk5pwd) file not found
" visible in "/var/log/syslog
". This is a bit misleading as it isn't "/usr/lib/ldap/smbk5pwd.so
" that is missing, but one of its dynamically linked libraries, as viewable withldd
.
- If dependencies are met at compile-time, and not at run-time - even if not enabled,
- While unrelated to Samba, part of the
shadowAccount"
objectClass is a "shadowLastChange
" attribute. While it is apparently seldom or no longer used, if it exists, and ifsmbk5pwd
needs to be compiled anyway, I thought it would be beneficial to havesmbk5pwd
also reset this attribute along with any password changes. I submitted this request to OpenLDAP, along with a completed patch, a ITS 6550. While this seemed to receive some support from Michael Michael Ströder (one of the developers), it was abruptly closed by Howard Chu. In case anyone else is looking for a similar solution, and in case the patch becomes unavailable from the OpenLDAP tracker for some reason, it is included below:Index: contrib/slapd-modules/smbk5pwd/Makefile =================================================================== RCS file: /repo/OpenLDAP/pkg/ldap/contrib/slapd-modules/smbk5pwd/Makefile,v retrieving revision 1.7 diff -u -r1.7 Makefile --- contrib/slapd-modules/smbk5pwd/Makefile 13 Apr 2010 20:17:37 -0000 1.7 +++ contrib/slapd-modules/smbk5pwd/Makefile 14 May 2010 00:50:46 -0000 @@ -16,8 +16,8 @@ OPT=-g -O2 CC=gcc -# Omit DO_KRB5 or DO_SAMBA if you don't want to support it. -DEFS=-DDO_KRB5 -DDO_SAMBA +# Omit DO_KRB5, DO_SAMBA, or DO_SHADOW if you don't want to support it. +DEFS=-DDO_KRB5 -DDO_SAMBA -DDO_SHADOW HEIMDAL_INC=-I/usr/heimdal/include SSL_INC= Index: contrib/slapd-modules/smbk5pwd/README =================================================================== RCS file: /repo/OpenLDAP/pkg/ldap/contrib/slapd-modules/smbk5pwd/README,v retrieving revision 1.6 diff -u -r1.6 README --- contrib/slapd-modules/smbk5pwd/README 13 Apr 2010 20:17:37 -0000 1.6 +++ contrib/slapd-modules/smbk5pwd/README 14 May 2010 00:50:46 -0000 @@ -1,6 +1,6 @@ This directory contains a slapd overlay, smbk5pwd, that extends the -PasswordModify Extended Operation to update Kerberos keys and Samba -password hashes for an LDAP user. +PasswordModify Extended Operation to update Kerberos keys, Samba +password hashes, and the shadowLastChange attribute for an LDAP user. The Kerberos support is written for Heimdal using its hdb-ldap backend. If a PasswordModify is performed on an entry that has the krb5KDCEntry @@ -17,6 +17,10 @@ objectclass, then the sambaLMPassword, sambaNTPassword, and sambaPwdLastSet attributes will be updated accordingly. +The Shadow support updates the shadowLastChange attribute to the current +date if a PasswordModify is performed on an entry that has the +shadowAccount objectclass. + To use the overlay, add: include <path to>/krb5-kdc.schema @@ -40,8 +44,8 @@ smbk5pwd-enable <module> can be used to enable only the desired one(s); legal values for <module> -are "krb5" and "samba", if they are respectively enabled by defining -DO_KRB5 and DO_SAMBA. +are "krb5", "samba", and "shadow", if they are respectively enabled by defining +DO_KRB5, DO_SAMBA, and DO_SHADOW. The samba module also supports the @@ -60,15 +64,16 @@ olcOverlay: {0}smbk5pwd olcSmbK5PwdEnable: krb5 olcSmbK5PwdEnable: samba + olcSmbK5PwdEnable: shadow olcSmbK5PwdMustChange: 2592000 -which enables both krb5 and samba modules with a password expiry time -of 30 days. +which enables all the krb5, samba, and shadow modules with a password +expiry time of 30 days. -The provided Makefile builds both Kerberos and Samba support by default. -You must edit the Makefile to insure that the correct include and library -paths are used. You can change the DEFS macro if you only want one or the -other of Kerberos or Samba support. +The provided Makefile builds all of Kerberos, Samba, and Shadow support by +default. You must edit the Makefile to insure that the correct include and +library paths are used. You can change the DEFS macro if you only want partial +support. This overlay is only set up to be built as a dynamically loaded module. On most platforms, in order for the module to be usable, all of the Index: contrib/slapd-modules/smbk5pwd/smbk5pwd.c =================================================================== RCS file: /repo/OpenLDAP/pkg/ldap/contrib/slapd-modules/smbk5pwd/smbk5pwd.c,v retrieving revision 1.34 diff -u -r1.34 smbk5pwd.c --- contrib/slapd-modules/smbk5pwd/smbk5pwd.c 13 Apr 2010 20:17:37 -0000 1.34 +++ contrib/slapd-modules/smbk5pwd/smbk5pwd.c 14 May 2010 00:50:47 -0000 @@ -17,6 +17,7 @@ /* ACKNOWLEDGEMENTS: * Support for table-driven configuration added by Pierangelo Masarati. * Support for sambaPwdMustChange and sambaPwdCanChange added by Marco D'Ettorre. + * Support for shadowLastChange added by Mark A. Ziesemer <www.ziesemer.com>. */ #include <portable.h> @@ -81,14 +82,21 @@ static ObjectClass *oc_sambaSamAccount; #endif +#ifdef DO_SAMBA +static AttributeDescription *ad_shadowLastChange; +static ObjectClass *oc_shadowAccount; +#endif + /* Per-instance configuration information */ typedef struct smbk5pwd_t { unsigned mode; #define SMBK5PWD_F_KRB5 (0x1U) #define SMBK5PWD_F_SAMBA (0x2U) +#define SMBK5PWD_F_SHADOW (0x4U) #define SMBK5PWD_DO_KRB5(pi) ((pi)->mode & SMBK5PWD_F_KRB5) #define SMBK5PWD_DO_SAMBA(pi) ((pi)->mode & SMBK5PWD_F_SAMBA) +#define SMBK5PWD_DO_SHADOW(pi) ((pi)->mode & SMBK5PWD_F_SHADOW) #ifdef DO_KRB5 /* nothing yet */ @@ -100,6 +108,10 @@ /* How many seconds after allowing a password change? */ time_t smb_can_change; #endif + +#ifdef DO_SHADOW + /* nothing yet */ +#endif } smbk5pwd_t; static const unsigned SMBK5PWD_F_ALL = @@ -110,6 +122,9 @@ #ifdef DO_SAMBA | SMBK5PWD_F_SAMBA #endif +#ifdef DO_SHADOW + | SMBK5PWD_F_SHADOW +#endif ; static int smbk5pwd_modules_init( smbk5pwd_t *pi ); @@ -653,6 +668,34 @@ } } #endif /* DO_SAMBA */ + +#ifdef DO_SHADOW + /* Shadow stuff */ + if ( SMBK5PWD_DO_SHADOW( pi ) && is_entry_objectclass(e, oc_shadowAccount, 0 ) ) { + struct berval *keys; + + ml = ch_malloc(sizeof(Modifications)); + ml->sml_next = qpw->rs_mods; + qpw->rs_mods = ml; + + keys = ch_malloc( 2 * sizeof(struct berval) ); + keys[0].bv_val = ch_malloc( LDAP_PVT_INTTYPE_CHARS(long) ); + keys[0].bv_len = snprintf(keys[0].bv_val, + LDAP_PVT_INTTYPE_CHARS(long), + "%ld", slap_get_time() / 60 / 60 / 24 ); + BER_BVZERO( &keys[1] ); + + ml->sml_desc = ad_shadowLastChange; + ml->sml_op = LDAP_MOD_REPLACE; +#ifdef SLAP_MOD_INTERNAL + ml->sml_flags = SLAP_MOD_INTERNAL; +#endif + ml->sml_numvals = 1; + ml->sml_values = keys; + ml->sml_nvalues = NULL; + } +#endif /* DO_SHADOW */ + be_entry_release_r( op, e ); qpw->rs_new.bv_val[qpw->rs_new.bv_len] = term; @@ -715,6 +758,7 @@ static slap_verbmasks smbk5pwd_modules[] = { { BER_BVC( "krb5" ), SMBK5PWD_F_KRB5 }, { BER_BVC( "samba" ), SMBK5PWD_F_SAMBA }, + { BER_BVC( "shadow" ), SMBK5PWD_F_SHADOW }, { BER_BVNULL, -1 } }; @@ -860,6 +904,16 @@ } #endif /* ! DO_SAMBA */ +#ifndef DO_SHADOW + if ( SMBK5PWD_DO_SHADOW( pi ) ) { + Debug( LDAP_DEBUG_ANY, "%s: smbk5pwd: " + "<%s> module \"%s\" only allowed when compiled with -DDO_SHADOW.\n", + c->log, c->argv[ 0 ], c->argv[ rc ] ); + pi->mode = mode; + return 1; + } +#endif /* ! DO_SHADOW */ + { BackendDB db = *c->be; @@ -882,66 +936,78 @@ return rc; } +typedef struct smbk5pwd_verify_schema_t { + const char *name; + AttributeDescription **adp; +} smbk5pwd_verify_schema_t; + static int -smbk5pwd_modules_init( smbk5pwd_t *pi ) +smbk5pwd_modules_verify_schema(const char *ocName, ObjectClass **oc, smbk5pwd_verify_schema_t *ad) { - static struct { - const char *name; - AttributeDescription **adp; + int i, rc; + + *oc = oc_find( ocName ); + if ( !*oc ) { + Debug( LDAP_DEBUG_ANY, "smbk5pwd: " + "unable to find \"%s\" objectClass.\n", + ocName, 0, 0 ); + return -1; } + + for ( i = 0; ad[ i ].name != NULL; i++ ) { + const char *text; + + *(ad[ i ].adp) = NULL; + + rc = slap_str2ad( ad[ i ].name, ad[ i ].adp, &text ); + if ( rc != LDAP_SUCCESS ) { + Debug( LDAP_DEBUG_ANY, "smbk5pwd: " + "unable to find \"%s\" attributeType: %s (%d).\n", + ad[ i ].name, text, rc ); + *oc = NULL; + return rc; + } + } +} + +static int +smbk5pwd_modules_init( smbk5pwd_t *pi ) +{ + #ifdef DO_KRB5 - krb5_ad[] = { + smbk5pwd_verify_schema_t krb5_ad[] = { { "krb5Key", &ad_krb5Key }, { "krb5KeyVersionNumber", &ad_krb5KeyVersionNumber }, { "krb5PrincipalName", &ad_krb5PrincipalName }, { "krb5ValidEnd", &ad_krb5ValidEnd }, { NULL } - }, + }; #endif /* DO_KRB5 */ #ifdef DO_SAMBA - samba_ad[] = { + smbk5pwd_verify_schema_t samba_ad[] = { { "sambaLMPassword", &ad_sambaLMPassword }, { "sambaNTPassword", &ad_sambaNTPassword }, { "sambaPwdLastSet", &ad_sambaPwdLastSet }, { "sambaPwdMustChange", &ad_sambaPwdMustChange }, { "sambaPwdCanChange", &ad_sambaPwdCanChange }, { NULL } - }, + }; #endif /* DO_SAMBA */ - dummy_ad; - - /* this is to silence the unused var warning */ - dummy_ad.name = NULL; +#ifdef DO_SHADOW + smbk5pwd_verify_schema_t shadow_ad[] = { + { "shadowLastChange", &ad_shadowLastChange }, + { NULL } + }; +#endif /* DO_SHADOW */ #ifdef DO_KRB5 if ( SMBK5PWD_DO_KRB5( pi ) && oc_krb5KDCEntry == NULL ) { krb5_error_code ret; extern HDB *_kadm5_s_get_db(void *); - int i, rc; - - /* Make sure all of our necessary schema items are loaded */ - oc_krb5KDCEntry = oc_find( "krb5KDCEntry" ); - if ( !oc_krb5KDCEntry ) { - Debug( LDAP_DEBUG_ANY, "smbk5pwd: " - "unable to find \"krb5KDCEntry\" objectClass.\n", - 0, 0, 0 ); - return -1; - } - - for ( i = 0; krb5_ad[ i ].name != NULL; i++ ) { - const char *text; - - *(krb5_ad[ i ].adp) = NULL; - - rc = slap_str2ad( krb5_ad[ i ].name, krb5_ad[ i ].adp, &text ); - if ( rc != LDAP_SUCCESS ) { - Debug( LDAP_DEBUG_ANY, "smbk5pwd: " - "unable to find \"%s\" attributeType: %s (%d).\n", - krb5_ad[ i ].name, text, rc ); - oc_krb5KDCEntry = NULL; - return rc; - } + int rc = smbk5pwd_modules_verify_schema("krb5KDCEntry", &oc_krb5KDCEntry, krb5_ad); + if ( rc != LDAP_SUCCES ) { + return rc; } /* Initialize Kerberos context */ @@ -980,32 +1046,21 @@ #ifdef DO_SAMBA if ( SMBK5PWD_DO_SAMBA( pi ) && oc_sambaSamAccount == NULL ) { - int i, rc; - - oc_sambaSamAccount = oc_find( "sambaSamAccount" ); - if ( !oc_sambaSamAccount ) { - Debug( LDAP_DEBUG_ANY, "smbk5pwd: " - "unable to find \"sambaSamAccount\" objectClass.\n", - 0, 0, 0 ); - return -1; + int rc = smbk5pwd_modules_verify_schema("sambaSamAccount", &oc_sambaSamAccount, samba_ad); + if ( rc != LDAP_SUCCESS ) { + return rc; } + } +#endif /* DO_SAMBA */ - for ( i = 0; samba_ad[ i ].name != NULL; i++ ) { - const char *text; - - *(samba_ad[ i ].adp) = NULL; - - rc = slap_str2ad( samba_ad[ i ].name, samba_ad[ i ].adp, &text ); - if ( rc != LDAP_SUCCESS ) { - Debug( LDAP_DEBUG_ANY, "smbk5pwd: " - "unable to find \"%s\" attributeType: %s (%d).\n", - samba_ad[ i ].name, text, rc ); - oc_sambaSamAccount = NULL; - return rc; - } +#ifdef DO_SHADOW + if ( SMBK5PWD_DO_SHADOW( pi ) && oc_shadowAccount == NULL ) { + int rc = smbk5pwd_modules_verify_schema("shadowAccount", &oc_shadowAccount, shadow_ad); + if ( rc != LDAP_SUCCESS ) { + return rc; } } -#endif /* DO_SAMBA */ +#endif /* DO_SHADOW */ return 0; } Index: contrib/slapd-modules/README =================================================================== RCS file: /repo/OpenLDAP/pkg/ldap/contrib/slapd-modules/README,v retrieving revision 1.6 diff -u -r1.6 README --- contrib/slapd-modules/README 13 Apr 2010 20:17:33 -0000 1.6 +++ contrib/slapd-modules/README 14 May 2010 00:50:46 -0000 @@ -49,8 +49,8 @@ Proxy Authorization compatibility with obsolete internet-draft. smbk5pwd (overlay) - Make the PasswordModify Extended Operation update Kerberos - keys and Samba password hashes as well as userPassword. + Make the PasswordModify Extended Operation update Kerberos keys, + Samba password hashes, and shadowLastChange, as well as userPassword. trace (overlay) Trace overlay invocation.
To compile with the "shadow" patch, and without Kerberos, I used the following command:make DEFS="-DDO_SAMBA -DDO_SHADOW" HEIMDAL_INC="" HEIMDAL_LIB=""
To provide Samba with the password for the admin DN, use the smbpasswd
tool:
$ sudo smbpasswd -W Setting stored password for "cn=samba.proxy,ou=serviceAccounts,dc=example,dc=com" in secrets.tdb New SMB password: Retype new SMB password:
Now, enable SMB for the first user.
This will also create a new "sambaDomain
" child under the configured suffix:
$ sudo smbpasswd -a mark New SMB password: Retype new SMB password: Added user mark.
Once complete, remove the "samba.proxy
" user from the "ldap.admins
" group, as the elevated permissions are no longer necessary.
Users and the Samba domain can continue to be modified with the remaining permissions.
Finally, for some additional and advised security, add something along the following to "/etc/samba/samba.conf
":
security = user valid users = @samba.users
Per the Samba documentation:
"security = user" is always a good idea. This will require a Unix account in this server for every user accessing the server.
Finally, "valid users
" points to a valid Unix group name.
This can be anything that is resolved locally, and visible through "getent groups
".
This group may be declared locally to the same server running Samba, or a posixGroup
declared in LDAP, as covered previously in Linux client authentication with LDAP, PAM, and NSS.
1 comment:
awesome summary
Post a Comment