Showing posts with label authentication. Show all posts
Showing posts with label authentication. Show all posts

Linux client authentication and Active Directory

Ubuntu Active Directory Authentication (an alternative solution)

Description

In order to authenticate a linux desktop client against Windows Directory services there are several configuration files and services which need to exist. The PAM authentication stack utilizing the pam_ldap and nss_ldap authentication modules. Network services such as NFS etc. Extensive configuration settings regarding the nsswitch.conf, pam.d/* configuration files, ldap.conf etc.

As a systems administrator these factors create a sharp learning curve, and time to not only configure all of these things, but to also maintain them. Having this many variables lead to errors costing time and money.

What if you could install one Pluggable Authentication Module, modify one configuration file to begin authenticating an existing Windows Directory Service or OpenLDAP directory containing existing users and groups?

Background

I work for a University. We have an existing Windows Directory full of students. Two hundred thousand plus accounts. To bring Linux into the destkop environment as an alternative to the Windows and OSX clients we needed a simple to maintain, simply to configure solution.

Kerberos Authentication was needed. Active Directory / OpenLDAP support was needed. Minimal configuration and minimal network services was also needed.

We have been utilizing Linux on the desktop in our student labs, public access terminals and some staff machines for close to 5 years now without the need for the pam_ldap, nss_ldap, nsswitch.conf, ldap.conf or NFS configurations necessary for this type of Linux desktop integration.

Alternative Solution

Because there is a perfectly viable solution existing regarding the necessary Kerberos Realm authentication a simple patch, or feature was added to dynamically query the existing Windows / OpenLDAP directory services in order to provide the pam_krb5 TGT to UID/GID verification.

The pam_krb5+ldap project page:

pam_krb5+ldap project page @ sourceforge.net

A patch has also been submitted to the original developer as a feature request to integrate this functionality to any new releases. Details can be found on bugzilla.

patch to integrate ldap uid/gid mapping to pam_krb5

Installation, configuration, usage

Installation HOWTO

Simple, get the package. You can download the latest release at the following URL: pam_krb5+ldap download

Next simple extract the package contents:

%> tar zxvf pam_krb5+ldap-version.tgz

Compile the package using the '--with-ldap' switch to enable the Widows Directory / OpenLDAP option (please note the libpam, libldap and libkrb5 libraries are required)

%> ./configure --with-ldap

Now install the compiled shared objects

%> make && make install

You may need to change the installation directory from /var/lib/security/pam_krb5 by issuing the following command:

%> cp -dfrv /var/lib/security/pam_krb5/* /lib/security/

If you receive errors during the compile or make commands please file bug reports at the following URL: pam_krb5+ldap support

Configuration

There are two areas to configure, one being the krb5.conf and the other being the pam.d/ file to ensure the Linux client uses the proper authentication method.

We will start with the krb5.conf file, below is an example. The following items will need to be changed to match your environment:

* default_realm (your kerberos realm) * kdc (kerberos realm server address) * default_domain (your domain name) * ldap_servs (list of ldap/active directory server addresses separated by a space) * ldap_port (connection port, default is 389, to use TLS/SSL specify 689) * binddn (location of user accounts used in bind process) * basedn (location of users used during search process) * ldapuser (default user account used during bind process) * ldappass (default password associated with bind user) * group_list (comma separated list of groups to ad user to)
[libdefaults]
        default_realm = EXAMPLE.EDU
        clockskew = 300

[realms]
UTAH.EDU = {
        kdc = KDC1.EXAMPLE.COM
        default_domain = EXAMPLE.COM
        admin_server = KDC1.EXAMPLE.COM
}

[logging]
        kdc = FILE:/var/log/krb5kdc.log
        admin_server = FILE:/var/log/kadmin.log
        default = FILE:/var/log/krb5lib.log
[domain_realm]
        .sub.example.com = EXAMPLE.COM

[appdefaults]
pam = {
        ticket_lifetime = 1d
        renew_lifetime = 1d
        forwardable = true
        proxiable = false
        retain_after_close = false
        minimum_uid = 2
        try_first_pass = true
        ignore_root = true

        schema = ad
        ldapservs = ldap1.example.com ldap2.example.com
        ldapport = 389
        binddn = uid=username,ou=Users,dc=example,dc=com
        basedn = ou=remoteusers,dc=example,dc=com
        ldapuser = [readonly-username]
        ldappass = [readonly-password]
        
        passwd = /etc/passwd
        shadow = /etc/shadow
        groups = /etc/group

        groups_list = audio,cdrom,cdrw,usb,plugdev,video,games

        # If you define these they will
        # over write anything obtained from
        # ldap/active directory
        homedir = /home
        defshell = /bin/bash
}

Now simply configure any service you wish to utilize this authentication method. For example if you wish to use this method to only authenticate the user at the terminal configure the /etc/pam.d/system-login file, if you wish to also enable ssh users access utilizing this authentication method you would also configure the /etc/pam.d/ssh configuration file.

Below is an example of the /etc/pam.d/system-login configuration

auth            required        pam_env.so
auth            sufficient      pam_krb5.so
auth            sufficient      pam_unix.so try_first_pass likeauth nullok
auth            required        pam_deny.so

account         required        pam_unix.so

password        required        pam_cracklib.so difok=2 minlen=8 dcredit=2 ocredit=2 retry=3
password        sufficient      pam_krb5.so
password        sufficient      pam_unix.so try_first_pass use_authtok nullok sha512 shadow
password        required        pam_deny.so

session         required        pam_limits.so
session         required        pam_env.so
session         optional        pam_krb5.so
session         required        pam_unix.so
session         required        pam_mkhomedir.so skel=/etc/skel/ umask=0022
session         optional        pam_permit.so

You may want to take note of the utilization of the pam_mkhome.so module as this is required for this authentication type.

Conclusion

Thats it. Hopefully linux on the desktop in educational, corporate environments will start becoming a bit easier to integrate.

Help & Support

Problems and questions can be directed to the support forums. Thanks.

pam_krb5+ldap support

Session management

Talking about sessions

Lets talk a minute about a common method of authentication tracking for web applications. Sessions. A session is a server side file, database and/or variable management which a developer can utilize to ensure a user has authenticated and remains a valid authenticated user. I am not going to talk about the dynamics or the best option to utilize here as it is beyond the scope of this document but I do recommend doing your research before starting a project as there are pro's and con's to each method of session management.

Here we are going to present an method of utilizing sessions and storage within a MySQL database. The reason this we decided to forgo the performance boosts of utilizing the mcache or default file based session management is due to our web hosting provider utilizing a shared directory where file based session management gets intermingled with other users, and they do not support the mcache daemon.

Below is the database structure for our database.

Session management table

CREATE TABLE `sessions` (
  `session_id` varchar(32) NOT NULL default '',
  `http_user_agent` varchar(32) NOT NULL default '',
  `session_data` blob NOT NULL,
  `session_expire` int(11) NOT NULL default '0',
  UNIQUE KEY `session_id` (`session_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Here I am going to provide the PHP class we utilize for our database connections, queries resulting query functions etc.

Database class

class database
{
 var $db;
 var $server;
 var $username;
 var $password;
 var $database;
 var $query;
 var $rows;
 var $array;
 var $num_rows;
 var $affected_rows;
 var $error;
 var $errno;

 function dbConnect( $server, $username, $password, $database )
 {
  $data->db = @mysql_pconnect( $server, $username, $password );
  if( !$data->db ) {
   $data->db = -1;
  } else {
   if( !@mysql_select_db( $database ) ) {
    $data->db = -1;
   }
  }
  return $data->db;
 }

 function dbConnectOnly( $server, $username, $password )
 {
  $data->db = @mysql_pconnect( $server, $username, $password );
  if( !$data->db ) {
   $data->db = -1;
  } else {
   $data->db = 0;
  }
  return $data->db;
 }

 function dbQuery( $query, $db )
 {
  $data->query = @mysql_query( $query, $db );
  if( !$data->query ) {
   $data->query = -1;
  }
  return $data->query;
 }

 function dbNumRows( $id )
 {
  $data->num_rows = @mysql_num_rows( $id );
  if( !$data->num_rows === 0 ) {
   $data->num_rows = -1;
  }
  return $data->num_rows;
 }

 function dbNumRowsAffected( $id )
 {
  $data->affected_rows = @mysql_affected_rows( $id )
  if( !$data->affected_rows === 0 ) {
   $data->affected_rows = -1;
  }
  return $data->affected_rows;
 }

 function dbArrayResults( $sql )
 {
  $data->array = array();
  while( $rows = @mysql_fetch_array( $sql, MYSQL_ASSOC ) ) {
   if( !$rows ) {
    $data->array = -1;
   }
   array_push( $data->array, $rows );
  }
  return $data->array;
 }

 function dbArrayResultsAssoc( $sql )
 {
  $data->array = array();
  while( $rows = @mysql_fetch_assoc( $sql ) ) {
   if( !$rows ) {
    $data->array = -1;
   }
   array_push( $data->array, $rows );
  }
  return $data->array;
 }

 function dbAffectedRows( $sql )
 {
  $rows = @mysql_affected_rows( $sql );
   if( $rows === 0 ) {
    $data->array = -1;
   }
   $data->array = 0;
  return $data->array;
 }

 function dbCatchErrno()
 {
  return @mysql_errno();
 }

 function dbCatchError()
 {
  return @mysql_error();
 }

 function dbFreeData( $sql )
 {
  return @mysql_free_result( $sql );
 }

 function dbCloseConn( $sql )
 {
  return @mysql_close( $sql );
 }

 function dbFixTable( $table, $db )
 {
  @mysql_query( "REPAIR TABLE `" . $table . "`", $db );
  @mysql_query( "OPTIMIZE TABLE `" . $table . "`", $db );
  @mysql_query( "FLUSH TABLE `" . $table . "`", $db );
 }
}

And finally our PHP session handler class. As you can see in the '__construct' function we utilize the 'session_set_save_handler' to modify the default PHP subroutine of using flat file's to store and retrieve our global session variables.

Session management class

class sessions
{
 var $id;
 var $data;
 var $max_time;
 var $dbconn;

 function __construct( $max_time )
 {
  global $defined;
  if( !empty( $max_time ) ) {
   @ini_set( 'session.gc_maxlifetime', $max_time );
  } else {
   @ini_set( 'session.gc_maxlifetime', 3600 );
  }
  session_set_save_handler(
   array( &$this, 'open' ),
   array( &$this, 'close' ),
   array( &$this, 'read' ),
   array( &$this, 'write' ),
   array( &$this, 'destroy' ),
   array( &$this, 'gc' )
  );
  @ini_set( 'cache_limiter', 'private' );
  @ini_set( 'cache_expire', $max_time );
  @ini_set( 'use_cookies', "1" );
  @register_shutdown_function( 'session_write_close' );
  @session_start();
 }

 function register( $name, $data )
 {
  return $_SESSION[$name] = $data;
 }

 function regen( $flag = false )
 {
  if( $flag !== false ) {
   $this->register( 'id', session_id() );
   @session_regenerate_id( $flag );
   $this->id = session_id();
   $this->destroy( $_SESSION['id'] );
  }
  return;
 }

 function reindex()
 {
  global $handles;
  return $handles['db']->dbFixTable( "sessions", $this->dbconn );
 }

 function open( $path, $name )
 {
  global $defined;
  global $handles;
  if( ( $this->dbconn = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ) ) !== -1 ) {
   return true;
  } else {
   return false;
  }
 }

 function close()
 {
  global $defined;
  global $handles;
  $this->reindex();
  $handles['db']->dbFreeData( $this->dbconn );
  $handles['db']->dbCloseConn( $this->dbconn );
  return true;
 }

 function read( $id )
 {
  global $handles;
  $query = "SELECT * FROM `sessions` WHERE `session_id` = \"" . $id . "\" AND `http_user_agent` = \"" . md5( $_SERVER["HTTP_USER_AGENT"] ) . "\" LIMIT 1";
  $result = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn );
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $fields = $handles['db']->dbArrayResultsAssoc( $result );
   return stripslashes( unserialize( $fields[0]['session_data'] ) );
  }
  return "";
 }

 function write( $id, $data )
 {
  global $handles;
  $query = "INSERT INTO `sessions` ( `session_id`, `http_user_agent`, `session_data`, `session_expire` ) VALUES ( \"" . $id . "\", \"" . md5( $_SERVER["HTTP_USER_AGENT"] ) . "\", \"" . mysql_real_escape_string( serialize( $data ) ) . "\", \"" . time()  . "\" ) ON DUPLICATE KEY UPDATE `session_id` = \"" . $id . "\", `session_data` = \"" . mysql_real_escape_string( serialize( $data ) ) . "\", `session_expire` = \"" . time() . "\"";
  $result = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn );
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $this->reindex();
   return true;
  } else {
   return false;
  }
 }

 function destroy( $id )
 {
  global $handles;
  $query = "DELETE FROM `sessions` WHERE `session_id` = \"" . $id . "\" LIMIT 1";
  $result = $handles['db']->dbQuery($handles['val']->ValidateSQL( $query, $this->dbconn ), $this->dbconn);
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) > 0 ) ) {
   $this->reindex();
   return true;
  } else {
   return false;
  }
 }

 function gc( $max_time )
 {
  global $handles;
  $query = "DELETE FROM `sessions` WHERE `session_expire` > \"" . time() - $this->max_time . "\"";
  $result = $handles['db']->dbQuery($handles['val']->ValidateSQL($query, $this->dbconn), $this->dbconn);
  if( ( is_resource( $result ) ) && ( $handles['db']->dbNumRowsAffected( $this->dbconn ) !== -1 ) ) {
   $this->reindex();
   return true;
  }
  return false;
 }
}

Some reasons to utilize a global session variables between your web pages could be to keep a persistent theme, or a specified region code or even to register an authenticated token which will allow your users access to register only data.

Here is a bit of code you can include on your pages to utilize the above classes.

// include the two classes we gave above
include 'class.database.php';
include 'class.sessions.php';

// register a variable as a handle of our database class
if(!is_resource($handles['dbconn'])) { $handles['dbconn'] = new database; }

// check to see if we have an existing session
if((!is_resource($handles['session'])&&(!isset($_SESSION))) { $handles['session'] = new sessions; }

// now register any variable you wish to persist between pages
$_SESSION['variable'] = 'we want this to follow our user from page to page';

And that is it for being able to change PHP's default session save handler from a flat file database to a MySQL database. I also want to mention one thing about the security of using sessions for sensitive data. Having worked with various companies one thing has become blazingly apparent in regards to an attack that first started back in the late 1990's regarding PHP's 'session_id()'. Change it every page your user visits to prevent a common attack known as 'session fixation'. This is very important if you are using server side session variables (whether it be a flat file session handler, a MySQL session handler or even an mcache session handler.

Here is an example to get you started:

// include the two classes we gave above
include 'class.database.php';
include 'class.sessions.php';

// register a variable as a handle of our database class
if(!is_resource($handles['dbconn'])) { $handles['dbconn'] = new database; }

// check to see if we have an existing session
if((!is_resource($handles['session'])&&(!isset($_SESSION))) { $handles['session'] = new sessions; }

// now register any variable you wish to persist between pages
$_SESSION['variable'] = 'we want this to follow our user from page to page';

// prevent session fixation attacks by changing the session_id()
$handles['session']->regen();
Return top