System administrators who log onto a number of machines often don’t do so as normal user (as they maybe ought to), then switching to root, but instead directly log on as root. In an environment thus controlled by different administrators, they commonly share a single user environment which is usually set up by the system via a combination of /etc/profile and ~/.profile, ~/.bashrc or similar. One difficulty in using a common environment is that administrators desiring different settings need to either manually set up their environment each time they log on, or otherwise make profiles which are littered with _if_s. Even this is difficult to do if there is no exact method of determining where the person logged in from (i.e. which client IP address) or who the human being logging on actually is (i.e. what is the guy’s name?).

A sample /etc/profile on one of my machines illustrates the point. Here the IP address from where I log on is determined by looking for the tty name in the list of currently logged on users.

# Is JP logging on ? ;-)
ttyname=`tty | sed -e 's|/dev/||'`
ip="`/usr/bin/w | grep "$ttyname" | awk '{print $3}'`"
case "$ip" in
        10.0.1.22|10.0.12.14|10.0.24.14|pc.my*)
                source /etc/jp.profile
                ;;
esac

If the workstations I log on from didn’t have a static IP address, such as when arriving via a dynamically set up VPN connection, I wouldn’t be able to determine this at all. Even so, it is a PITA and it is error-prone and requires maintenance.

I use ssh whenever I log onto a Linux or Unix system, telnet being too insecure. And since I’m quite lazy, I use RSA keys to log on to the systems, which avoids me having to enter my password every time I connect to a system to quickly check a mail queue, or the current disk space on the machine.

This program was was created because I was sick and tired of always finding myself in a shell which was in emacs mode and which didn’t have my favorite aliases set ;-)

ldp relies on the fact that SSH can be configured to set an environment variable whenever I connect to it.

For the next step, I want to have access to a central profile whichever system I log on to. It is important to me to have certain environment variables set up, programs that I need launched, and most important, I want vi-style editing in both the shell and programs that support GNU readline (e.g. the mysql program, etc.).

LDAP to the rescue. Anybody who knows me also knows that I swear by LDAP, specifically OpenLDAP’s implementation of the LDAP directory.

Even though I’m going to store my profile in my LDAP directory entry, there is no reason for not doing it differently. A similar service could easily be created to retrieve profile and settings from an HTTP server such as the excellent Apache HTTP server, or even simply from a file server. My reason for using an LDAP directory server is that I have a number of slave replica servers which ensure high service availability (and the fact that LDAP simply rocks).

Modus Operandi

Invoked from /etc/profile upon login, ldp checks for the presence of an environment variable set by the SSH daemon and uses that to determine the username of the person loggin on. It then searches the LDAP directory for an object of class ldpObject, and if it finds it, decodes the profile it contains and stores it in a file for the user. If the username also exists locally (i.e. can be found in either /etc/passwd or whichever is defined by the system’s name service switch NSS) the file ownership is changed accordingly. Just before exiting, ldp simply issues the path of that file to stdout, allowing the invoking shell to source that in to its own environment.

Installation

Installation of ldp is quite simple:

  • Download the distribution
  • Unpack the distribution and edit ldp.h, adjusting the defines to your needs.
  • Check the Makefile, possibly adding paths to the location of your LDAP libs
  • make and make test. The latter will print out the values of the settings you defined in ldp.h. Review those to ensure they are correct.
  • Create the profile directory LDP_PDIR and set appropriate permissions
  • Install the binary into a convenient location
  • Set up the LDAP directory, ensuring the schema for ldpObject is loaded
  • Optionally set up ACLs for the directory server
  • If required, restart your directory server
  • Add a profile for yourself
  • Test ldp withLDP_USER=myname ./ldp ls -l /var/spool/ldp
  • If all works, set up sshd to permit user environment strings
  • On the target machines, set up root’s ~/.ssh/authorized_keys to enable $LDP_USER
  • Edit /etc/profile or equivalent to source the result of ldp

Download

Download ldp

Unpack & Install

Retrieve and unpack the tarball with the usual commands.

configure ldp.h

After changing into the ldp distribution directory, edit the file ldp.h and adjust the definitions according to the instructions.

/*
 * Define LDP_LDAPURI to be the URL to connect to, or define LDP_LDAPHOST
 * with the host:port combination (deprecated). Don't use both.
 * If $LDP_LDAPURI is set in the environment, that will take preference over
 * this definition.
 */

#define LDP_LDAPURI		"ldap://ldap.example.com:389/"
//#define LDP_LDAPHOST	"localhost:389"


/* The credentials with which ldp will bind to the directory
 * to search for and read the profile. If you allow anonymous
 * access to ldpObject in your DSA, you can set both of these
 * to NULL, otherwise they must be set to the DN and the password
 * of the DN allowed to retrieve the object.
 */
 
#define LDP_BINDDN		"cn=ldpMan,dc=example,dc=com"
// #define LDP_BINDDN	NULL
#define LDP_BINDPW		"secret"
// #define LDP_BINDPW	NULL


/* Set LDP_BASE to the search base upon which to start searching
 * for ldpObjects. If you keep your ldpObjects in the person-
 * entries, set it to something like ou=People,dc=example,dc=com.
 * Otherwise, if you keep all your ldpObjects under one branch of
 * the DIT, set LDP_BASE to the top of that branch (e.g.
 * ou=ldp-Profiles,dc=example,dc=com.
 */

#define LDP_BASE	"ou=People,dc=example,dc=com"

/* Define LDP_ATTRIB to the name of the attribute type which holds
 * the username. Ldp will construct a filter of the form
 * "(&(objectClass=ldpObject)(%s=%s))", with the first %s being set
 * to LDP_ATTRIB and the second %s being set to the username found
 * in $LDP_USER. Typically, LDP_ATTRIB will be either 'cn' or 'uid'.
 */

#define LDP_ATTRIB	"userid"

/* The name of the attribute type which contains the base-64 encoded
 * profile. There shouldn't really be a need to change this, but if
 * you do, you'll have to change the schema definition in ldpobject.schema
 */

#define LDP_SHPROFILE	"ldpSHprofile"	// Attribute type containing profile text


/* Define a comma or white-space-separated list of attribute types
 * that ldp should export into the environment for each user. The
 * types are read from the directory, and if found, their first
 * value will be written into the profile as LDP_<typename>. If you
 * define LDP_TYPES as "CN mail TelePhoneNumber", ldp will create
 *	LDP_CN="Jan-Piet Mens"
 *	LDP_mail="jpmens@gmail.com"
 *	LDP_TelePhoneNumber="999"
 * (note the capitalization which is done the way in which you 
 * specified the attribute type)
 */

#define LDP_ETYPES	"CN, Mail, telephoneNumber"

/* Set LDP_USERENV to the name of the environment variable that ldp
 * will use to determine the username of the user logging in. This
 * is the name of the environment variable which is set through
 * sshd, and must match that given in the "environment=" option
 * of ~/.ssh/authorized_jeys
 */

#define LDP_USERENV	"LDP_USER"


/* Define LDP_PDIR to be the directory in which ldp writes the 
 * retrieved profiles. This directory should be owned by root
 * and be set to mode 1777.
 */

#define LDP_PDIR	"/var/spool/ldp"

/* Set to the attribute type name you want displayed when ldp is
 * invoked. If NULL then none is displayed
 */

#define LDP_DISPLAYNAME	"CN"

Makefile

You may need to adjust the paths to your LDAP libraries in the Makefile. Then type make to build the program, followed by make test to ensure that ldp has correct settings.

$ make
$ make test

Create profile directory

ldp stores profiles retrieved from the LDAP directory server in a so-called profile directory, the name of which you specified as LDP_PDIR in ldp.h. You have to create this directory and set appropriate permissions for it. Do a make spooldir, alternatively issuing the following commands if you prefer to do so manually:

$ mkdir `./ldp -p`
$ chmod 1777 `ldp -p`

Install ldp

Install the binary ldp into a conventient location, such as /usr/local/bin. A make install does that for you.

$ sudo make install

Setting up the LDAP Directory Server

In the examples below, I’m supposing you are using a newver version of OpenLDAP, otherwise the instructions will have to be modified according to the requirements of your directory server.

Schema

For storing the shell-profile, I’ve created an auxilliary object class called ldpObject with an attribute type ldpSHprofile which holds the octets of the profile proper, but you are of course welcome to modify that to suit your needs.

#(@) ldpobject.schema by Jan-Piet Mens
#
# The OID base is
#  iso(1) org(3) dod(6) internet(1) private(4) enterprise(1) jpmens(7637)
#     1.3.6.1.4.1.7637
#					  .10 pub
# ldp uses the object id prefix (OID arc) from our own enterprise
# number. the object ids used herein may be freely used for ldp.

attributetype ( 1.3.6.1.4.1.7637.10.1.1.1 NAME 'ldpSHprofile'
	DESC 'Shell code sourced in via /etc/profile; this string is Base64-encoded.'
	EQUALITY octetStringMatch
	SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
	SINGLE-VALUE
	)

objectclass ( 1.3.6.1.4.1.7637.10.2.1.1 NAME 'ldpObject'
	DESC 'Auxiliary object to be used with person, inetorgperson or account for storing user profile'
	SUP top AUXILIARY
	MUST ( cn  ) 
	MAY ( description $ ldpSHprofile $ owner )  
   ) 

As defined here, ldpObject is an auxiliary class which is fine for adding it to an existing person or inetOrgPerson object. You might want to modify the schema definition if you want ldpObject to be kept in their own container.

Container

I have forseen ldpObject objects to be added to an existing user’s entry which already has a structural object class such as person or inetOrgPerson. Alternatively, it could be added to an account object as defined in the Cosine schema. Still another method is to simply change the AUXILIARY definition into STRUCTURAL and create objects in their own container.

Some samples:

dc=example,dc=com
   ou=People
      cn=JP Mens,ou=People,dc=example,dc=com
         objectclass: person
  	   objectclass: inetOrgPerson
	   objectclass: ldpObject
	   uid: jpm
	   cn: JP Mens
	   ldpSHprofile:: ...

   ou=ldp-Profiles
	cn=jpm,ou=ldp-Profiles,dc=example,dc=com
	   objectclass: account
	   objectclass: ldpobject
	   uid: jpm
	   ldpSHprofile:: ...

Access Control

A profile might contain sensitive information, and as such it ought to be protected from casual view.

If you keep the ldpObject attached to the people classes in the directory, you can set up an ACL in OpenLDAP’s slapd.conf to allow reading of these objects only by the DN defined in ldp.h, in which case the DN must match the definition of LDP_BINDDN. Furthermore, you’ll then need an entry in the directory named as LDP_BINDDN with the password defined in LDP_BINDPW. The following ACL snippet will allow the attribute type ldpSHprofile to be read and modified by the user (self) and by the ldp-manager; all others have no access to it.

access to attrs=ldpSHprofile
  by self write
  by dn.exact="cn=ldpMan,dc=example,dc=com"	write
  by * none

If, on the other hand, you’ve chosen to put all ldpObjects into their own container, use an ACL similar to the following, which allows ldp to read all objects in the given container and, if the entry has an owner, will allow the entrie’s owner to modify the profile.

access to dn.regex=".*,ou=ldp-Profiles,dc=example,dc=com$"
  by dnattr=owner write
  by dn.exact="cn=ldpMan,dc=example,dc=com" read
  by * none

.profile into LDAP

My profile is a text file which I create and modify with an editor of course. A profile doesn’t usually contain secrets, but never the less it need not be easily readable to others. For this reason I decided to obfuscate it by encoding it in BASE-64 when storing it in the LDAP directory. Environments that require more security can easily add an ACL to the directory server to protect it from prying eyes (see below).

I note that the profile I’m editing here will be run on multiple machines, so care is needed to make it as generic as possible. For example always use references to $HOME instead of hardcoding the path to a home directory.

$ vi myprofile.sh
$ encode.sh < myprofile.sh > myprofile.64

After editing your profile, it needs to be converted to Base64 encoding. The supplied encode.sh script uses OpenSSL’s enc utility to do so. If you don’t have OpenSSL, you might try using the supplied encode.pl which attempts to do the same with Perl’s MIME::Base64 module.

Use the included utility ldp-encode which will read your profile file, and create an LDIF which can be directly given to ldapmodify. ldp-encode uses the values of LDP_ATTRIB and LDP_BASE to create the relative distinguised name (RDN) and distiniguished names (DN) respectively.

$ ldp-encode myprofile.sh | ldapmodify -D ... -W 

Using either ldapmodify or a graphical utility for managing your LDAP server, you now have to add or modify the entry in which the profile will be stored, adding the ldpObject object class as well as loading the base64 encoded profile into the ldpSHprofile attribute type.

ldapmodify

I can load this LDIF

dn: uid=jpmens,ou=people,dc=example,dc=com
changetype: modify
add: objectclass
objectclass: ldpObject
-
replace: ldpSHprofile
ldpSHprofile: <file://./myprofile.b64

into the directory with an

ldapmodify -x -h localhost -D cn=manager,dc=example,dc=com -W < me.ldif

LDAP Administrator

In Softerra’s LDAP Administrator, search and select the entry you want to add the profile to. Then choose “New Attribute”, click on ldpSHprofile and press “Next”. When prompted for the attribute value, click on the “Load…” button and search for and load the Base64 file, alternatively pasting it into the dialog box. Softerra

GQ

Jarek Gawor’s LDAPbrowser/editor

Jarek Gawor’s LDAP Browser/Editor is a fine Java-based tool which works on both sundry Linux/Unix derivatives as well as on Windows.

In order to create the new attribute type, ensure you’ve copied the base64-encoded string of the profile into your clipboard. You can then add a new attribute of type ‘string’ and load that in the LDAP Browser/Editor

LDAP browser

Configure ssh

ldp is designed to work with SSH in conjunction with RSA keys. There are a number of locations on the web that give detailed instructions on setting up ssh & generating keys for yourself.

sshd

SSH needs to allow an environment variable set upon logon. OpenSSH is usually shipped with the option disabled, but it has to be enabled for ldp to work. This option specifies whether the file ~/.ssh/environment and environment= options in ~/.ssh/authorized_keys are processed by sshd. The manual warns you, that enabling environment processing may enable users to bypass access restrictions in some configurations using mechanisms such as LD_PRELOAD. If you want to ignore that warning, locate the file sshd_config (often located in /etc/ssh) and add the line

PermitUserEnvironment yes

to the bottom of your /etc/ssh/sshd_config and restart the SSH daemon (on Redhat-based systems with service sshd restart). Note that this may open some security hole, so doing it is entirely your responsability: if your coffee machine burns out, or your home or office explode, it will be your fault. Check the manual for ssh(1), sshd(8) and sshd_config(5).

.ssh/authorized_keys

To ensure that a string containing my username is added to the environment whenever I log connect via SSH, I’ve set an environment option in the authorized_keys file for the root user in ~/.ssh/authorized_keys.

environment="LDP_USER=jpm" ssh-rsa AAAAB3.....Sas= rsa-key-20031128 SSH-2.0 SECn(JPmens)

Test

Test it by connecting to SSH. I’ll try with putty. If everything works, I should see something like this:

Using username "root".
Authenticating with public key "rsa-key-20031128 SSH-2.0 SECn" from agent
Last login: Sun Feb 19 22:30:21 2006 from 10.0.33.222
#  echo $LDP_USER
jpm
#

So, what have I gained so far? I am able to determine that I myself am logging on to the target system, irrespective of the client operating system, the ssh client and the source address of the computer I’m connecting from. Whenever I access the server with an ssh client and an RSA key, the environment will be populated with a variable LDP_USER containing my username. Of course any other administrator having access to root’s ~/.ssh/authorized_keys can set her own key up to contain an identical username. It is therefore worth noting that this isn’t terribly secure. On the other hand, there is never much that can be hidden from a root user, is there?

Set up /etc/profile

The ldp program (LDAP profile) is added to the system’s /etc/profile by getting the shell to source the result of ldp:

. `/usr/local/bin/ldp`

Note the backticks; upon logging in, the shell (any of standard Unix shell (sh), the Bash shell (bash) or Korn shell (ksh)) will read in or source (note the leading period in the line) the result of the ldp program. If you look up the syntax of the source command, you’ll see that it expects a filename to read and execute commands from. That is exactly what ldp will do: it will print a filename from which the login shell will read further commands from.

Actually ldp will connect to an LDAP directory server, search for a profile entry for the user named in the environment variable $LDP_USER and will store the content of that user’s profile into a temporary file, then printing the name of the file it wrote into to the standard output (stdout), thereby instructing the calling shell where to read commands from. Look at what happens when I execute ldp interactively on my system:

# /usr/local/bin/ldp
/var/spool/ldp/jpm

Notice the filename? That is the file into which ldp has written the content of the profile read from the directory. In actual fact therefore, sourcing the result of ldp is equivalent, for my user, to

. /var/spool/ldp/jpm

which is a perfectly valid shell command, and by the way, is exactly what I want!

Security

The only really secure method of using ldp is to not use it at all. Apart from that, ldp should be owned by root and be mode 0100. Unfortunately, since ldp is run in the user’s context from /etc/profile, it will probably require execute permission for user, group and others.

It is quite easy to trick ldp into returning another user’s profile. This as yet cannot easily be avoided. I wrote ldp for it to be used exclusively on trusted systems by the root user, and this is the way it has been tested. If you want to set up ldp for non-_root_ users, that is certainly possible, but first evaluate the risks.

Advanced Topics

.inputrc

One of the raisons d’être of ldp is to have the inputrc for any readline(3) enabled programs set up as well. This can be done in the profile retrieved via ldp, by creating the inputrc file on the fly.

ldp sets up two environment variables which can be used to determine the profile directory and the profile name used by it: they are $LDP_PDIR and $LDP_PNAME respectively. Using this information, my profile carries the following shell script as a here script in it:

# Create my INPUTRC (for readline programs) on the fly.
# LDP_PDIR is set by `ldp' to the directory into which
# a profile is written. LDP_PNAME is set to the filename
# into which `ldp' wrote my profile.

#-- begin INPUTRC

# I'm writing my inputrc on the fly.
export INPUTRC="${LDP_PNAME:-/tmp/$USER}.inputrc"
cat <<!EndINPUTRC > ${INPUTRC}
set editing-mode vi
!EndINPUTRC
chmod 400 ${INPUTRC}

While I’m logging on and the file created by ldp is being sourced in, this bit of shell script creates an inputrc for me and sets up $INPUTRC to point to that file. Voilá!

Bypassing hardcoded LDAP URI

If you are deploying the ldp binary to a number of machines, the name of the LDAP server may differ depending on where it is located (i.e. in a DMZ for example). ldp uses the LDAP URI which is compiled into it (from ldp.h), but that may be overriden by setting $LDP_LDAPURI before its invocation.

. `LDP_LDAPURI=ldap://other.example.com:389/  /usr/local/bin/ldp`

That may help on occasion.

The idea, implementation and code are mine and are copyright (C)2006 by Jan-Piet Mens. This software may be used freely.

ldp uses base64 encoding-decoding functions which includes software developed by the Kungliga Tekniska Hgskolan and its contributors. Please see base64.[ch] for full details.

Changelog

  • 0.3 2006-02-25
    • Initial version
  • 0.4 2006-02-27
    • Added ldp-encode utility to create an LDIF for importing profile into LDAP directory.
    • Added man-pages for ldp and ldp-encode
  • 0.5 2006-02-27
    • ldp-encode tries to determine whether to use Gecos or username for RDN
    • ldp-encode has option -n to add objectClass and option -r to set RDN on the command line, overriding any guesswork
    • ldp & ldp-encode show version with -v
    • man-pages brought up to date
    • removed warnings on MAX OS/X 10.4
  • 0.6 2006-02-28
    • ldp-encode printed Base64 data until size of input file instead of length of Base64 string.
    • ldp sets $LDP_USERHOME to the home directory which belongs to $LDP_USER; this can be used in scripts to find the user’s real directory, as $HOME always belongs to the logged in user. To clarify: if root logs in with $LDP_USER=joe, $HOME will be something like ‘/’ whereas $LDP_USERHOME will be ‘/home/joe’.
  • 0.7 2006-02-28
    • sundry small fixes