LDAP & NFS

LDAP Description

LDAP stands for Lightweight Directory Access Protocol. It is a protocol for accessing directory services on the network. LDAP is at the basis of Active Directory. Linux systems commonly use the OpenLDAP variant. The term directory services can translate into virtually any information services such as telephone directory, account information, address book data used by mail clients, etc. This scheme is found virtually everywhere for network-based information services and validation. In particular, LDAP is the basis of the very common Active Directory (user at WCU itself). A good discussion can be found in the wikipedia article:
http://en.wikipedia.org/wiki/LDAP
Our OpenLDAP implementation is based on the online Ubuntu LDAP documentation:
OpenLDAP Server Docs
Unfortunately, the Ubuntu document is overly complicated, having you access and create very low-level features of the LDAP database setup.

As an LDAP novice, you'll find LDAP to be a very bewildering system. However, it has been around a very long time and is quite stable for setting up network-based user account information.

LDAP makes extensive use of LDIF (LDAP Data Interchange Format) configuration files to create entities with the LDAP database. Some common terminology used in LDIF is this:
dn:           distinguished name, a unique identifier for the record
dc:           domain component
cn:           common name, often a simple way to refer to the record
o:            organization
ou:           organizational unit
objectClass:  a class to which this entity belongs
The objectClass key signifies that LDAP is an object-oriented system whereby records belong to classes which specify properties of the record attributes (required, optional, etc). The classes are described in so-called schema files which reside in the directory /etc/ldap/schema. The multiple occurrences of objectClass which you'll see suggests that LDAP employs multiple inheritance.

The domain components (dc) are key constituents of the dn values of records. These dc values are often suggestive of a DNS domain components, which is often the case. Our fully qualified domain name MACHINE.cs.wcupa.edu would correspond to the a record whose base dn would be this:
dc=MACHINE,dc=cs,dc=wcupa,dc=edu
This entry is often used as the so-called base distinguished name, identifying the record which corresponds to our machine which provides the LDAP service. To simplify things, our example uses the base dn of:
dc=MACHINE
In a real-world situation, the former value would be preferable; however, LDAP doesn't really require any relationship between its names and DNS names, so you can do whatever you like.

Preparation for LDAP Server installation

We're going to rely on Python modules (scripts) to populate, present, and modify the LDAP database. At the basis is a module which will provide all the necessary variable definitions, which you will create as:
/usr/local/share/ldap/myldap.py
in a directory accessible only to you (and root of course).

Create LDAP Support Library

$ sudo mkdir /usr/local/share/ldap
$ sudo chgrp LOGIN /usr/local/share/ldap
$ sudo chmod 770 /usr/local/share/ldap
In order to make this directory accessible to Python, we need set the PYTHONPATH environment variable, so:
$ nano ~/.profile
append the line:
export PYTHONPATH=/usr/local/share/ldap
You must log out / log in to have this variable be in effect. Verify when you reenter:
$ printenv PYTHONPATH
/usr/local/share/ldap

Install Python packages

$ sudo apt install ldap-utils python3-ldap python3-ldap3

Additional modifications to PyDev

To pick up the PYTHONPATH setting and other installed packages we need to reconstruct the Python Interpreter in Eclipse. Go through Window ⇾ Preferences and select
PyDev ⇾ Interpreters ⇾ Python Interpreter
The python interpreter entry you created originally should be selected. We want to recreate it.

Click the button, then the button.

Proceed as before. Add this information:
Interpreter Name:       python3
Interpreter Executable: /usr/bin/python3
Click Apply to activate. You should now see in the bottom window that
/usr/local/share/ldap
is one of the directory entries.

LDAP Server installation

Install the necessary packages. The installation requires you to create an LDAP administrator password. Like the MySQL root password, we will record this into a file, so you don't have to remember it. Install the server-side LDAP like this:
$ sudo apt install slapd
The configuration is two steps, the first to enter the password:
┌─────────────────────────┤ Configuring slapd ├──────────────────────────┐   
│ Please enter the password for the admin entry in your LDAP directory.  │   
│ Administrator password:                                                │   
│                                                                        │   
│  LDAP_ADMIN_PASS                                                       │   
│                                                                        │   
│                                 <Ok>                                   │   
│                                                                        |   
└────────────────────────────────────────────────────────────────────────┘   
The second step confirms the password.

The next phase is to create the initial LDAP setup:
$ sudo dpkg-reconfigure slapd
Follow these steps:
  1. Omit OpenLDAP server configuration?
    Take the No default.
  2. DNS domain name (this is very important):
    MACHINE
  3. Organization name (this probably doesn't matter much):
    MACHINE
  4. Administrator password:
    LDAP_ADMIN_PASS
  5. Confirm password:
    LDAP_ADMIN_PASS
  6. Database backend to use.
    Take the MDB default.
  7. Do you want the database to be removed when slapd is purged?
    For pedagogical sake, set this to Yes.
  8. Move old database?
    Take the Yes default.
Saying Yes in step 7 allows you to "completely start over" if something goes wrong by doing:
$ sudo apt purge slapd
$ sudo apt install slapd

Now create the myldap.py module. Create it as normal user:
$ nano /usr/local/share/ldap/myldap.py
Make this be the content:

/usr/local/share/myldap.py
ldap_host = "localhost"
base_dc   = "MACHINE"
base_dn   = "dc={}".format(base_dc)
admin_dn  = "cn=admin,{}".format(base_dn)
people_dn = "ou=People,{}".format(base_dn)
group_dn  = "ou=Group,{}".format(base_dn)
admin_pwd = "LDAP_ADMIN_PASS"
select

Set the default base for LDAP utils

The file /etc/ldap/ldap.conf provides defaults for the ldap utility commands. We only need the base dn as the value of BASE (it's commented out with a sample setting):

/etc/ldap/ldap.conf (modification)
BASE    dc=MACHINE

See what you have so far

In order to illustrate some basic behavior of LDAP client access and to prove the effectiveness of the admin dn, run the ldapsearch utility in two ways:
$ ldapsearch -x

$ ldapsearch -xW -D "cn=admin,dc=MACHINE"
Enter LDAP Password: LDAP_ADMIN_PASS
The output is not very informative, but the access methods are. LDAP access, say, in contrast to MySQL, has separate phases: The first ldapsearch call uses anonymous binding on the connection. The ubiquitous "-x" option means to use simple authentication, i.e., not using SASL (don't worry if this makes no sense). With anonymous binding, it is often the case that most of the information in the records can be read, but not written.

When the "-D" option is used it means that the connection is bound to a specific entity specified by the distinguished name provided. In the above example, the distinguished name is that of the admin user which has read/write privileges on all the records. The "-W" option means that the password needed to authenticate the administrator will be provided through standard input.

It is an interesting exploration into complexity to look at the backend configuration directory, /etc/ldap/slapd.d/:
$ sudo ls -RF /etc/ldap/slapd.d/
You can now "forget" the LDAP admin password with respect to this document.
LDAP admin password:

Populate the LDAP Database

We're going to use a few Python files to populate and modify the LDAP database. These are contained in the Python ldap project. Download the source archive
ldap.zip
Extract the archive into the ~/eclipse-workspace/ directory:
~/eclipse-workspace/ldap
Create a PyDev project, from this folder.

LDIF reader

Our scripts will create LDIF files, and it's useful to the view these files in Eclipse. So do another Eclipse MarketPlace installation. Enter this into the Find box:
Find:
You should see one choice:
Apache Directory Studio.
Click Install and then follow through by the usual "say yes" steps.

Python LDAP populate script

We will employ a home-grown script, ldif_add.py, to add LDIF record entries to the LDAP database.

ldif_add.py
#!/usr/bin/env python3
 
from ldap3 import Server, Connection
from ldif import LDIFParser
import os
import argparse
 
from myldap import ldap_host, admin_dn, admin_pwd
 
parser = argparse.ArgumentParser()
parser.add_argument('ldif', help='ldif file')
args = parser.parse_args()
 
server = Server(ldap_host)
 
conn = Connection(server, admin_dn, admin_pwd, auto_bind = True)
 
class MyLDIF(LDIFParser):
    def __init__(self, infile):
        LDIFParser.__init__(self, infile)
 
    def handle(self, dn, attrs):
        found = conn.search( dn, '(objectClass=*)' )
        if found:
            print('entry exists for {}'.format(dn))
        else:
            print('entry added for {}'.format(dn))
            conn.add(dn,attributes=attrs)
 
ldif_file = args.ldif
 
if not os.path.exists(ldif_file):
    print('**** no such file: {}'.format(ldif_file))
    exit(1)
 
parser = MyLDIF(open(ldif_file, 'r'))
parser.parse()
The ldif_add.py script exhibits some features common to any LDAP program in that we must establish a connection to the LDAP server and then "bind" credentials to it:
conn = Connection(server, admin_dn, admin_pwd, auto_bind = True)

Creating LDIF files

It's best to see the ldif_add.py script in action. The idea is that you're going to generate an LDIF file and then run this scrip to enter the entities into the database. Every LDIF file you use must contain information specific to your LDAP site, for example the initial entities for People and Group contain these starting "dn" lines, respectively:
dn: ou=People,dc=MACHINE 

dn: ou=Group,dc=MACHINE 
We are going to write Python modules which reference the site-specific information in myldap.py and make dynamic substitutions in order to create the static LDIF output files.

People entity

Start by getting a shell to the right place:
$ cd ~/eclipse-workspace/ldap
Run all the Python scripts from this directory.

Start with the module:

people_ldif.py
from string import Template
from myldap import base_dn
 
__tmpl = Template('''\
dn: ou=People,$BASE_DN
ou: People
objectClass: organizationalUnit
objectClass: top
'''
)
 
print(__tmpl.substitute( BASE_DN = base_dn ))
Execute it
$ python3 people_ldif.py
getting the LDIF output:
dn: ou=People,dc=MACHINE
ou: People
objectClass: organizationalUnit
objectClass: top
 
To add this entry to the LDAP database, redirect the standard output into an LDIF file and then call ldif_add.py on that file.
$ python3 people_ldif.py > people.ldif

$ ./ldif_add.py people.ldif
adding new entry "ou=People,dc=MACHINE"

Group entity

Same goes for the group_ldif.py module:

group_ldif.py
from string import Template
from myldap import base_dn
 
__tmpl = Template('''\
dn: ou=Group,$BASE_DN
ou: Group
objectClass: organizationalUnit
objectClass: top
'''
)
 
print(__tmpl.substitute( BASE_DN = base_dn ))
$ python3 group_ldif.py > group.ldif

$ ./ldif_add.py group.ldif
adding new entry "ou=Group,dc=MACHINE"
Check what you've got so far:
$ ldapsearch -x

Why the string.Template.substitute and not format?

The key idea in all the substitutions made is to identify a substitute variable:
$BASE_DN
and replace it by the value of base_dn found in myldap.py. So why not use our usual format with "{}" instead. There are several reasons:

Add entries for two users

The users on our system, like a Linux system, have LDIF entries making them a "People" entity as well as a "Group" entity. You can put more than one LDIF entry into the same file. They must be separated by one or more empty line (implying that a single LDIF entity cannot have empty lines).

Here are our two sample persons. Each defines a People entry and a Group entry.

asmith_ldif.py
from string import Template
from myldap import base_dn
 
__tmpl = Template('''\
dn: uid=asmith,ou=People,$BASE_DN
uid: asmith
cn: Alice Smith
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword:
shadowLastChange: 0
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 2001
gidNumber: 2001
homeDirectory: /home/asmith
gecos: Alice Smith
 
dn: cn=asmith,ou=Group,$BASE_DN
cn: asmith
objectClass: posixGroup
objectClass: top
userPassword: {crypt}x
gidNumber: 2001
'''
)
 
print(__tmpl.substitute( BASE_DN = base_dn ))

bjones_ldif.py
from string import Template
from myldap import base_dn
 
__tmpl = Template('''\
dn: uid=bjones,ou=People,$BASE_DN
uid: bjones
cn: Bob Jones
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword:
shadowLastChange: 0
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 2002
gidNumber: 2002
homeDirectory: /home/bjones
gecos: Bob Jones
 
dn: cn=bjones,ou=Group,$BASE_DN
cn: bjones
objectClass: posixGroup
objectClass: top
userPassword: {crypt}x
gidNumber: 2002
'''
)
 
print(__tmpl.substitute( BASE_DN = base_dn ))
$ python3 asmith_ldif.py > asmith.ldif
$ python3 bjones_ldif.py > bjones.ldif

$ ./ldif_add.py asmith.ldif
added entry for dn:  "uid=asmith,ou=People,dc=MACHINE"
added entry for dn:  "cn=asmith,ou=Group,dc=MACHINE"

$ ./ldif_add.py bjones.ldif
added entry for dn:  "uid=bjones,ou=People,dc=MACHINE"
added entry for dn:  "cn=bjones,ou=Group,dc=MACHINE"
After this, see what all you've got:
$ ldapsearch -x

Relationship of people entries to UNIX login information

The information in the python LDIF files corresponds to what would appear in these three system files. Take a look at the LDIF file asmith.ldif.

If asmith were a normal user on a UNIX system, there would information in these files:
  1. /etc/passwd: specifies uid, uidNumber, gidNumber, gecos, homeDirectory, and loginShell:
    asmith:x:2001:100:Alice Smith:/home/asmith:/bin/bash
  2. /etc/group: identifies the gidNumber:
    asmith:x:2001:
  3. /etc/shadow: specifies these entries (see "man 5 shadow"):
    asmith:F2:F3:F4:F5:F6:F7:F8:
    Association with LDAP fields are as follows. Dates are days since 1/1/1970.
    F2: encrypted password (userPassword)
    F3: date of last password change (shadowLastChange)
    F4: min. password age = # days before password can be changed (0 = no minimum)
    F5: max. password age = # days before password expires (shadowMax)
    F6: password warning period = expiration warning period (shadowWarning)
    F7: password inactivity period = # days after expiration before must change
    F8: account expiration = date when account expires
    	

Manipulate LDAP Entities

LDAP is an odd object-oriented database, but just like a database, we want to be able to read, modify and delete entries as well as add them.

List user entries

The next program to study is this:

ldif_userDump.py
#!/usr/bin/env python3
 
from ldap3 import Server, Connection, ALL_ATTRIBUTES
import argparse
from myldap import ldap_host, admin_dn, admin_pwd, people_dn, group_dn
 
parser = argparse.ArgumentParser()
parser.add_argument('login', help='user login', nargs='?')
args = parser.parse_args()
 
login = args.login
 
server = Server(ldap_host)
 
if not login:
    conn = Connection(server, auto_bind = True) 
 
    print("==> People entries\n")
 
    conn.search(people_dn, '(cn=*)', attributes = ALL_ATTRIBUTES)
 
    for entry in conn.entries:
        print(entry.entry_to_ldif())
 
    print("==> Group entries\n")
 
    conn.search(group_dn, '(cn=*)', attributes = ALL_ATTRIBUTES)
 
    for entry in conn.entries:
        print(entry.entry_to_ldif())
else:
    conn = Connection(server, admin_dn, admin_pwd, auto_bind = True)
 
    search_filter = '(uid={})'.format(login)
    found = conn.search(people_dn, search_filter, attributes = ALL_ATTRIBUTES)
 
    if found:
        user = conn.entries[0]
        print(user.entry_to_ldif())
    else:
        print("\n**** No user entry for {}\n".format(login))
The two usages are to get all user/group entries in LDIF format:
$ ./ldif_userDump.py
and to get a single one, e.g.,
$ ./ldif_userDump.py asmith
Using the first call, you can easily get the key "dn" field entries for every user by filtering through grep:
$ ./ldif_userDump.py | grep ^dn
A pedagogical point is that finding all entities is done with anonymous binding (none) whereas finding a single one is done with admin binding.

The initial visual difference between the "all" and "single" dumps is the level on information given. The "single" dump gives the all important field (initially empty)
userPassword
whereas the the "all" dump does not.

Remove Entities

The next script is one which removes an LDAP entity. The way to do so is by the "dn" entry.

removeDN.py
#!/usr/bin/env python3
 
from ldap3 import Server, Connection
from myldap import ldap_host, admin_dn, admin_pwd
import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument('dn', help='dn entry')
args = parser.parse_args()
 
dn = args.dn
 
server = Server(ldap_host)
 
conn = Connection(server, admin_dn, admin_pwd, auto_bind = True)
 
deleted = conn.delete(dn)
 
if deleted:
    print("entry deleted")
else:
    print("entry not deleted")
Say I want to get rid of user bjones, do this to identify the two "dn" entities you want to remove:
$ ./ldif_userDump.py  | grep ^dn | grep bjones
Then get rid of them like this (quoting the dn values is necessary):
$ ./removeDN.py "uid=bjones,ou=People,dc=MACHINE"
$ ./removeDN.py "cn=bjones,ou=Group,dc=MACHINE"
Confirm by running the previous ./ldif_userDump.py call to see that there's no output.

Modify: change user password

The final script to look at is one which changes a user password.

changePwd.py
#!/usr/bin/env python3
 
from ldap3 import Server, Connection, MODIFY_REPLACE
import argparse
import string
from getpass import getpass
import random
import crypt
from time import time
from myldap import ldap_host, admin_dn, admin_pwd, people_dn
 
parser = argparse.ArgumentParser()
parser.add_argument('uid', help='user login')
args = parser.parse_args()
 
login = args.uid
 
server = Server(ldap_host)
 
conn = Connection(server, admin_dn, admin_pwd, auto_bind = True)
 
found = conn.search(people_dn, '(uid={})'.format(login))
 
if not found:
    print( "no such login: {}".format(login))
    exit(1)
 
dn = conn.entries[0].entry_dn
 
password = getpass("Password: ")
confirm  = getpass(" Confirm: ")
 
if password != confirm:
    print("\n**** Confirmation not same as Password\n")
    exit(1)
 
chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '/.'
salt = ''.join(random.SystemRandom().choice(chars) for c in range(8))
salt = '$6$' + salt + '$'
encrypted = crypt.crypt(password,salt)
userPassword = '{crypt}' + encrypted
 
seconds_per_day = 24 * 60 * 60
shadowLastChange = int( time() / seconds_per_day )
 
userPassword = userPassword.encode('utf-8')
shadowLastChange = str(shadowLastChange).encode('utf-8')
 
conn.modify(dn, {
    'shadowLastChange': [( MODIFY_REPLACE, [shadowLastChange] )],
    'userPassword'    : [( MODIFY_REPLACE, [userPassword]     )],
})
Set the password of asmith:
$ ./changePwd.py asmith
Password: asmith-password
 Confirm: asmith-password
Verify that this password works.
$ ldapsearch -xW -D "uid=asmith,ou=People,dc=MACHINE"
Enter LDAP Password: asmith-password
and take a look at the new dump entry:
$ ./ldif_userDump.py asmith
The changePwd.py script exhibits a number of novel features.
  1. We obtain the password (and confirmation) without echoing to terminal with this code:
    password = getpass("Password: ")
  2. We obtain the correct encryption of the password as follows:
    1. Obtain a string of all characters usable for the "salt" of as SSHA512 encrpytion:
      chars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '/.'
    2. Construct the salt part by taking 8 random choices from the chars string and surrounding them by the SSHA512 salt designation:
      salt = ''.join(random.SystemRandom().choice(chars) for c in range(8))
      salt = '$6$' + salt + '$'
    3. Create the 512-bit encryption based on the password and the salt:
      encrypted = crypt.crypt(password,salt)
    4. Put the whole thing together, salt + encrypted password, plus a designation of the general type of encryption created:
      userPassword = '{crypt}' + encrypted
      userPassword = userPassword.encode('utf-8')
      
      conn.modify(dn, {
          'shadowLastChange': [( MODIFY_REPLACE, [shadowLastChange] )],
          'userPassword'    : [( MODIFY_REPLACE, [userPassword]     )],
      })
  3. Use the connection to make the necessary modifications on the users' LDAP entry:
    conn.modify(dn, {
        'shadowLastChange': [( MODIFY_REPLACE, [shadowLastChange] )],
        'userPassword'    : [( MODIFY_REPLACE, [userPassword]     )],
    })

Become an LDAP client

The LDAP server-side is in place. We now want our machine to be an LDAP client. Install this:
$ sudo apt install libnss-ldap
A configurator for the auth-client-config package runs automatically. Make these choices:
LDAP server: ldapi:/// (the default)
Distinguished name base: dc=MACHINE (not the default)
LDAP version: 3 (the default)
Make local root Database admin? No (not the default)
Does the LDAP database
require login?
No (the default)

The effects are to add and start a new service, libnss-ldap, and configure these files to use ldap for all authentication forms:
/etc/pam.d/
   common-account
   common-auth
   common-password
   common-session
   common-session-noninteractive
Most of the information entered creates the key control file: /etc/ldap.conf. Most of the file consists of commented lines, but you can find the un-commented lines by:
$ grep -v ^# /etc/ldap.conf | grep -P "\S"

base dc=MACHINE
uri  ldapi:///
ldap_version 3
pam_password md5
If you need to change this LDAP client configuration, Ubuntu suggests doing this:
$ sudo dpkg-reconfigure ldap-auth-config

Next, create the setup that allows system clients to authenticate against LDAP:
$ sudo auth-client-config -t nss -p lac_ldap
The affected file is /etc/nsswitch.conf which is used to determine choices for obtaining user (and other) information. You can cat this file and observe the content:
$ cat /etc/nsswitch.conf
...
# pre_auth-client-config # passwd:         compat
passwd: files ldap
# pre_auth-client-config # group:          compat
group: files ldap
# pre_auth-client-config # shadow:         compat
shadow: files ldap
...
We need to make configuration changes in the appropriate PAM modules so that LDAP authentication is used. Run this:
$ sudo pam-auth-update
This will ask you to indicate which authentication profiles to enable (the list includes the newly added LDAP Authentication). All are probably be checked by default. Just tab to OK and Enter.

To pick up these low-level changes, you must reboot your system:
$ sudo reboot

Verify account status for asmith

Coming back from reboot, the observation is the presence of asmith as a system user by:
$ getent passwd
$ getent group
$ sudo getent shadow
Secondly, trying switching user to asmith:
$ su - asmith
Password: asmith-password
No directory, logging in with HOME=/
asmith@MACHINE:/$ exit
Give asmith a home directory by this simple one-liner:
$ sudo mkhomedir_helper asmith
Then try again:
$ su - asmith
Finally, assuming you have installed SASL from Web Authentication and HTTPS, you can verify that asmith is a "valid user" for SASL authentication by doing:
$ testsaslauthd -u asmith -p asmith-password
The user "asmith" is not in any of the standard password files, but SASL authentication, by default, uses the PAM mechanism, as prescribed in the configuration file /etc/default/saslauthd. In particular LDAP users can be recognized as valid users for Apache Mod_Authn_SASL authentication. Same goes with External authentication which you can test by:
$ pwauth
asmith
asmith-password
$ echo $?
0

NFS

NFS (Network File System) is about accessing the file system of a server from a remote client machine. We are going to make our machine an NFS server and serve the /home directory to the virtual machine.

Server setup

The host will be the NFS server. We'll set IP up so that any VM client can mount the "/home" directory from the host.
  1. Install the server package:
    [MACHINE] $ sudo apt install nfs-kernel-server
    
  2. Export the entire /home file system to any virtual machine. Edit the file /etc/exports on MACHINE, adding this line:
    /home 192.168.122.0/24(rw,async,no_subtree_check)
    
  3. Restart the NFS service and verify exports:
    [MACHINE]$ sudo systemctl restart nfs-kernel-server
    
    [MACHINE]$ sudo exportfs
    /home         	192.168.122.0/24
    

VM Client

We want a new VM with the name nfsvm. This is where the current bug appears if I attempt to use the newest cloud machine (bionic).
  1. Create and access (must wait a bit for the second one):
    [MACHINE]$ uvt-kvm create nfsvm release=xenial --memory 256
    [MACHINE]$ uvt-kvm ssh nfsvm --insecure
    
  2. Make it have the static IP address 192.168.122.21 and reboot. See Virtualization with KVM for examples.
  3. Create an /etc/hosts entry and an ~/.ssh/config entry for the new VM nfsvm. Again, see Virtualization with KVM for examples.
  4. Access it from the host and update:
    [MACHINE]$ ssh nfsvm
    

Make the VM be an LDAP client

Install the basic LDAP client-side login access package on the guest. The procedure is almost the same as what we did for the host machine.
[nfsvm]$ sudo apt update
[nfsvm]$ sudo apt install libnss-ldap ldap-utils
Make these choices in the configurator (the difference from the host machine setup is highlighted):

LDAP server: ldap://192.168.122.1 (not the default)
Distinguished name base: dc=MACHINE (not the default)
LDAP version: 3 (the default)
Make local root Database admin? No (not the default)
Does the LDAP database
require login?
No (the default)

Create the setup that allows system clients to authenticate against LDAP:
[nfsvm]$ sudo auth-client-config -t nss -p lac_ldap
Finally, run this command to establish LDAP authentication within PAM:
[nfsvm]$ sudo pam-auth-update
Tab to OK and Enter. Then reboot the virtual machine:
[nfsvm]$ sudo reboot
Go back in and confirm that the user "asmith" is there:
[nfsvm]$ getent passwd

VM as NFS Client

Now we want the guest VM to be an NFS client of the host. In particular we want the user "asmith" to access her home directory on the host as /home/asmith on the VM guest.

Install the NFS client software:
[nfsvm]$ sudo apt install nfs-common
Then mount the /home directory, exported from the host onto the client:
[nfsvm]$ sudo mount 192.168.122.1:/home /home
[nfsvm]$ ls /home
Doing so has now made the ubuntu home hidden. If we were to log out we could not get back into the nfsvm!

Make "asmith" a VM admin in addition to ubuntu

We need to do some prep work before continuing. First, on the client side, create a sudoers entry for asmith:
[nfsvm]$ sudo su
[nfsvm]# nano /etc/sudoers.d/90-cloud-init-users
Add a line for "asmith" same as that for "ubuntu" (be careful to get the syntax EXACTLY correct):
asmith ALL=(ALL) NOPASSWD:ALL
Then on the host side, create a public key for "asmith" and authorize access to localhost:
[MACHINE]$ sudo su
[MACHINE]$ su - asmith
[asmith@MACHINE]$ ssh-keygen
[asmith@MACHINE]]$ cd .ssh/
[asmith@MACHINE]]$ cp id_rsa.pub authorized_keys
Test it for asmith. It should go through without a password.
[asmith@MACHINE]$ ssh nfsvm 
[asmith@nfsvm]$ sudo su
[root@nfsvm]# 
Also add your public key (i.e. for LOGIN) onto the authorized keys of "asmith" so that you can get it, too. On the host, as root:
[MACHINE]$ sudo su
[MACHINE]# cat /home/LOGIN/.ssh/id_rsa.pub >> /home/asmith/.ssh/authorized_keys
Then check it:
[MACHINE]$ ssh asmith@nfsvm
So "asmith" is now an admin of nfsvm when /home is mounted from the client.

Make the mount permanent

Using the root shell from "asmith", edit the file /etc/fstab on the client, adding a line to the end:

/etc/fstab (on nfsvm)
192.168.122.1:/home   /home   nfs  defaults   0  0
Then reboot nfsvm.

Test asmith admin access

When nfsvm comes back, you should be able to get in as "asmith" per se, or better, as you using the "asmith" login:
[LOGIN@MACHINE]$ ssh asmith@nfsvm
[asmith@nfsvm]$ exit
This is now the way in for you, so change your ~/.ssh/config file entry to:
Host nfsvm
  user asmith
and then confirm access:
[LOGIN@MACHINE]$ ssh nfsvm


© Robert M. Kline