]> eyrie.org Git - kerberos/wallet.git/commitdiff
Add ad-keytab, update Wallet::Config
authorBill MacAllister <whm@dropbox.com>
Sun, 3 Apr 2016 18:40:00 +0000 (18:40 +0000)
committerRuss Allbery <eagle@eyrie.org>
Mon, 28 May 2018 00:33:31 +0000 (17:33 -0700)
* This ad-keytab is useful in the initial setup of AD as a keytab
  store for wallet.
* Change configuration variables to correctly reflect that some values
  are relative distinguished names.
* Add a configuration variable for the base distinguished name for
  ActiveDirectory.

contrib/ad-keytab [new file with mode: 0755]
perl/lib/Wallet/Config.pm
perl/lib/Wallet/Kadmin/AD.pm

diff --git a/contrib/ad-keytab b/contrib/ad-keytab
new file mode 100755 (executable)
index 0000000..2af9f85
--- /dev/null
@@ -0,0 +1,610 @@
+#!/usr/bin/perl -w
+#
+# Create, update, delete, and display keytabs stored in Active Directory.
+#
+# Written by Bill MacAllister <whm@dropbox.com>
+# Copyright 2016 Dropbox, Inc.
+#
+# See LICENSE for licensing terms.
+
+##############################################################################
+# Declarations
+##############################################################################
+
+use Authen::SASL;
+use Carp;
+use Getopt::Long;
+use IPC::Run qw( run timeout );
+use Net::LDAP;
+use Pod::Usage;
+use strict;
+
+my $opt_ad_server;
+my $opt_base_dn;
+my $opt_computer_rdn;
+my $opt_config;
+my $opt_debug;
+my $opt_dump;
+my $opt_help;
+my $opt_manual;
+my $opt_realm;
+my $opt_user_rdn;
+
+# Configuration variables
+our $AD_DEBUG;
+our $AD_SERVER;
+our $AD_COMPUTER_RDN;
+our $AD_USER_RDN;
+our $KEYTAB_REALM;
+our $AD_BASE_DN;
+
+##############################################################################
+# Subroutines
+##############################################################################
+
+# Write messages to standard output and check the return status
+sub msg {
+    my @msgs = @_;
+    for my $m (@msgs) {
+        print STDOUT $m . "\n" or croak("Problem printing to STDOUT");
+    }
+    return;
+}
+
+# Write debugging messages
+sub dbg {
+    my ($m) = @_;
+    msg("DEBUG:$m");
+    return;
+}
+
+# Decode Active Directory's userAccountControl attribute
+# Flags are powers of two starting at zero.
+sub list_userAccountControl {
+    my ($uac) = @_;
+    my @flags = (
+        'SCRIPT',
+        'ACCOUNTDISABLE',
+        'HOMEDIR_REQUIRED',
+        'LOCKOUT',
+        'PASSWD_NOTREQD',
+        'PASSWD_CANT_CHANGE',
+        'ENCRYPTED_TEXT_PWD_ALLOWED',
+        'TEMP_DUPLICATE_ACCOUNT',
+        'NORMAL_ACCOUNT',
+        'INTERDOMAIN_TRUST_ACCOUNT',
+        'WORKSTATION_TRUST_ACCOUNT',
+        'SERVER_TRUST_ACCOUNT',
+        'DONT_EXPIRE_PASSWORD',
+        'MNS_LOGON_ACCOUNT',
+        'SMARTCARD_REQUIRED',
+        'TRUSTED_FOR_DELEGATION',
+        'NOT_DELEGATED',
+        'USE_DES_KEY_ONLY',
+        'DONT_REQ_PREAUTH',
+        'PASSWORD_EXPIRED',
+        'TRUSTED_TO_AUTH_FOR_DELEGATION',
+        'PARTIAL_SECRETS_ACCOUNT'
+    );
+
+    my $flag_list;
+    my $comma = '';
+    for (my $i=0; $i<scalar(@flags); $i++) {
+        if ($uac & (2**$i)) {
+            $flag_list .= $comma . $flags[$i];
+            $comma = ', ';
+        }
+    }
+    return $flag_list;
+}
+
+# GSS-API bind to the active directory server
+sub ldap_connect {
+    my $ldap;
+    if ($AD_DEBUG) {
+        dbg('binding to ' . $AD_SERVER);
+    }
+    if (!$AD_SERVER) {
+        croak("Missing ldap host name, specify ad_server=\n");
+    }
+    eval {
+        my $sasl = Authen::SASL->new(mechanism => 'GSSAPI');
+        $ldap = Net::LDAP->new($AD_SERVER, onerror => 'die');
+        my $mesg = eval { $ldap->bind(undef, sasl => $sasl) };
+    };
+    if ($@) {
+        my $error = $@;
+        die "ldap bind to AD failed: $error\n";
+    }
+    return $ldap;
+}
+
+# Take a principal and split into parts.  The parts are keytab type,
+# keytab identifier, the base dn, an LDAP filter, and if the keytab
+# type is host the host name.
+sub kerberos_attrs {
+    my ($principal) = @_;
+
+    my %attr = ();
+    my $dn;
+    my $host;
+    my $k_type;
+    my $k_id;
+    if ($principal =~ m,^(host|service)/(\S+),xms) {
+        $attr{type} = $1;
+        $attr{id}   = $2;
+        if ($attr{type} eq 'host') {
+            $attr{base}   = $AD_COMPUTER_RDN . ',' . $AD_BASE_DN;
+            $attr{host}   = $attr{id};
+            $attr{host}   =~ s/[.].*//;
+            $attr{dn}     = "cn=$attr{host},$attr{base}";
+            $attr{filter} = "(samAccountName=$attr{host}\$)";
+        } elsif ($attr{'type'} eq 'service') {
+            $attr{base}   = $AD_USER_RDN  . ',' . $AD_BASE_DN;
+            $attr{dn}     = "cn=srv-$attr{id},$attr{base}";
+            $attr{filter} = "(servicePrincipalName=$attr{type}/$attr{id})";
+        }
+    }
+    if ($AD_DEBUG) {
+        for my $a (sort keys %attr) {
+            dbg("$a = $attr{$a}");
+        }
+    }
+    return %attr;
+}
+
+# Perform an LDAP search against AD and return information about
+# service and host accounts.
+sub ad_show {
+    my ($principal, $kattr_ref) = @_;
+
+    my $ldap = ldap_connect();
+    my %kattr = %{$kattr_ref};
+    my $base   = $kattr{base};
+    my $filter = $kattr{filter};
+    my @attrs = ();
+    if (!$opt_dump) {
+        @attrs = (
+            'distinguishedName',             'objectclass',
+            'dnsHostname',                   'msds-KeyVersionNumber',
+            'msds-SupportedEncryptionTypes', 'name',
+            'servicePrincipalName',          'samAccountName',
+            'userAccountControl',            'userPrincipalName',
+            'whenChanged',                   'whenCreated',
+            );
+    }
+
+    if ($AD_DEBUG) {
+        dbg("base:$base filter:$filter scope:subtree\n");
+    }
+
+    my $result;
+    eval {
+        $result = $ldap->search(
+            base   => $base,
+            scope  => 'subtree',
+            filter => $filter,
+            attrs  => \@attrs
+            );
+    };
+    if ($@) {
+        my $error = $@;
+        die "LDAP search error: $error\n";
+    }
+    if ($result->code) {
+        msg("INFO base:$base filter:$filter scope:subtree\n");
+        die $result->error;
+    }
+    if ($AD_DEBUG) {
+        dbg('returned: ' . $result->count);
+    }
+    if ($result->count > 0) {
+        for my $entry ($result->entries) {
+            for my $attr ( sort $entry->attributes ) {
+                my $out = '';
+                if ($attr =~ /userAccountControl/xmsi) {
+                    my $val = $entry->get_value($attr);
+                    $out = "$attr: $val";
+                    $out .= ' (' . list_userAccountControl($val) . ')';
+                    msg($out);
+                } else {
+                    my $val_ref = $entry->get_value($attr, asref => 1);
+                    my @vals = @{$val_ref};
+                    for my $val (@vals) {
+                        msg("$attr: $val");
+                    }
+                }
+            }
+        }
+    } else {
+        msg("$kattr{type}/$kattr{id} not found");
+    }
+    msg(' ');
+    return;
+}
+
+# Check to see if a keytab exists
+sub ad_exists {
+    my ($principal, $kattr_ref) = @_;
+
+    my $ldap = ldap_connect();
+    my %kattr = %{$kattr_ref};
+    my $base   = $kattr{base};
+    my $filter = $kattr{filter};
+    my @attrs = ('objectClass', 'msds-KeyVersionNumber');
+    if ($AD_DEBUG) {
+        dbg("base:$base filter:$filter scope:subtree\n");
+    }
+
+    my $result;
+    eval {
+        $result = $ldap->search(
+            base   => $base,
+            scope  => 'subtree',
+            filter => $filter,
+            attrs  => \@attrs
+            );
+    };
+    if ($@) {
+        my $error = $@;
+        die "LDAP search error: $error\n";
+    }
+    if ($result->code) {
+        msg("INFO base:$base filter:$filter scope:subtree\n");
+        die $result->error;
+    }
+    if ($AD_DEBUG) {
+        dbg('returned: ' . $result->count);
+    }
+    if ($result->count > 1) {
+        msg('ERROR: too many AD entries for this keytab');
+        for my $entry ($result->entries) {
+            msg('INFO: dn found ' . $entry->dn . "\n");
+        }
+        die("INFO: use show to examine the problem\n");
+    }
+    if ($result->count) {
+        for my $entry ($result->entries) {
+            return $entry->get_value('msds-KeyVersionNumber');
+        }
+    } else {
+        return 0;
+    }
+    return;
+}
+
+# Run a shell command.  In this case the command will always be msktutil.
+sub run_cmd {
+    my @cmd = @_;
+
+    if ($AD_DEBUG) {
+        dbg('running command:' . join(q{ }, @cmd));
+    }
+
+    my $in;
+    my $out;
+    my $err;
+    my $err_flag;
+    eval {
+        run(\@cmd, \$in, \$out, \$err, timeout(60));
+        if ($?) {
+            my $this_err = $?;
+            $err_flag = 1;
+            if ($this_err) {
+                msg('ERROR:' . $?);
+            }
+            if ($err) {
+                msg('ERROR (err):' . $err);
+            }
+        }
+    };
+    if ($@) {
+        msg('ERROR (status):' . $@);
+        $err_flag = 1;
+    }
+    if ($err_flag) {
+        msg('ERROR: Problem executing:' . join(q{ }, @cmd));
+        die "FATAL: Execution failed\n";
+    }
+
+    msg($out);
+    return;
+}
+
+# Either create or update a keytab for the principal.  Return the name
+# of the keytab file created.
+sub ad_create_update {
+    my ($principal, $file, $action) = @_;
+    my @cmd = ('/usr/sbin/msktutil');
+    push @cmd, '--' . $action;
+    push @cmd, '--server',   $AD_SERVER;
+    push @cmd, '--enctypes', '0x4';
+    push @cmd, '--enctypes', '0x8';
+    push @cmd, '--enctypes', '0x10';
+    push @cmd, '--keytab',   $file;
+    if ($KEYTAB_REALM) {
+        push @cmd, '--realm', $KEYTAB_REALM;
+    }
+    if ($principal =~ m,^host/(\S+),xms) {
+        my $fqdn = $1;
+        my $host = $fqdn;
+        $host =~ s/[.].*//xms;
+        push @cmd, '--base', $AD_COMPUTER_RDN;
+        push @cmd, '--dont-expire-password';
+        push @cmd, '--computer-name', $host;
+        push @cmd, '--upn',           "host/$fqdn";
+        push @cmd, '--hostname',      $fqdn;
+    } elsif ($principal =~ m,^service/(\S+),xms) {
+        my $service_id = $1;
+        push @cmd, '--base', $AD_USER_RDN;
+        push @cmd, '--use-service-account';
+        push @cmd, '--service',      "service/$service_id";
+        push @cmd, '--account-name', "srv-${service_id}";
+        push @cmd, '--no-pac';
+    }
+    run_cmd(@cmd);
+    return;
+}
+
+# Delete a principal from Kerberos.  For AD this means just delete the
+# object using LDAP.
+sub ad_delete {
+    my ($principal, $kattr_ref) = @_;
+
+    my %kattr = %{$kattr_ref};
+    if (!ad_exists($principal, $kattr_ref)) {
+        msg("WARN: the keytab for $principal does not appear to exist.");
+        msg("INFO: attempting the delete anyway.\n");
+    }
+
+    my $ldap = ldap_connect();
+    my $msgid = $ldap->delete($kattr{dn});
+    if ($msgid->code) {
+        my $m;
+        $m .= "ERROR: Problem deleting $kattr{dn}\n";
+        $m .= $msgid->error;
+        die $m;
+    }
+    return 1;
+}
+
+##############################################################################
+# Main Routine
+##############################################################################
+
+# Get options
+GetOptions(
+    'ad_server=s'    => \$opt_ad_server,
+    'base_dn=s'      => \$opt_base_dn,
+    'computer_rdn=s' => \$opt_computer_rdn,
+    'config=s'       => \$opt_config,
+    'debug'          => \$opt_debug,
+    'dump'           => \$opt_dump,
+    'help'           => \$opt_help,
+    'manual'         => \$opt_manual,
+    'realm'          => \$opt_realm,
+    'user_rdn=s'     => \$opt_user_rdn
+);
+
+# Help the user
+if ($opt_manual) {
+    pod2usage(-verbose => 2);
+}
+if ($opt_help || !$ARGV[0]) {
+    pod2usage(-verbose => 0);
+}
+
+# Make sure that we have kerberos credentials and that KRB5CCNAME
+# points to them.
+if (!$ENV{'KRB5CCNAME'}) {
+    msg('ERROR: Kerberos credentials are required ... try kinit');
+    pod2usage(-verbose => 0);
+}
+
+# Read the configuration file or croak
+my $conf_file;
+if ($opt_config) {
+    if (-e $opt_config) {
+        $conf_file = $opt_config;
+    } else {
+        msg("ERROR: Config file ($opt_config) not found");
+        pod2usage(-verbose => 0);
+    }
+} elsif ($ENV{'ADKEYTAB'}) {
+    $conf_file = $ENV{'ADKEYTAB'};
+} elsif (-e '.ad-keytab.conf') {
+    $conf_file = '.ad-keytab.conf';
+} else {
+    $conf_file = '/etc/wallet/wallet.conf';
+}
+do $conf_file or die (($@ || $!) . "\n");
+
+# Process command line options
+if ($opt_ad_server) {
+    $AD_SERVER = $opt_ad_server;
+}
+if ($opt_base_dn) {
+    $AD_BASE_DN = $opt_base_dn;
+}
+if ($opt_computer_rdn) {
+    $AD_COMPUTER_RDN = $opt_computer_rdn;
+}
+if ($opt_user_rdn) {
+    $AD_USER_RDN = $opt_user_rdn;
+}
+if ($opt_debug) {
+    $AD_DEBUG = 1;
+}
+
+# -- Get command line arguments
+my $action = shift;
+my $id     = shift;
+my $keytab;
+if ($ARGV[0]) {
+    $keytab = shift;
+} else {
+    $keytab = '/etc/krb5.keytab';
+}
+
+my %kattr = kerberos_attrs($id);
+# Validate that the keytab id makes sense for the keytab type
+if ($kattr{type} eq 'service') {
+    if ($kattr{id} =~ /[.]/xms) {
+        msg('ERROR: service principal names may not contain periods');
+        pod2usage(-verbose => 0);
+    }
+    if (length($kattr{id}) > 22) {
+        msg('ERROR: service principal name too long');
+        pod2usage(-verbose => 0);
+    }
+} elsif ($kattr{type} eq 'host') {
+    if ($kattr{id} !~ /[.]/xms) {
+        msg('ERROR: FQDN is required');
+        pod2usage(-verbose => 0);
+    }
+} else {
+    msg("ERROR: unknown keytab type $kattr{type}");
+    pod2usage(-verbose => 0);
+}
+
+if ($action =~ /^(create|update)/xms) {
+    ad_create_update($id, $keytab, $1);
+} elsif ($action =~ /^del/xms) {
+    ad_delete($id, \%kattr);
+} elsif ($action =~ /^sh/xms) {
+    ad_show($id, \%kattr);
+} else {
+    msg("ERROR: unknown action $action");
+    pod2usage(-verbose => 0);
+}
+
+exit;
+
+__END__
+
+=head1 NAME
+
+ad-keytab
+
+=head1 SYNOPSIS
+
+ad-keytab create|update|delete|show keytab-id [keytab-file]
+[--ad_server=hostname] [--computer_rdn=dn] [--user_rdn] [--dump]
+[--help] [--manual] [--debug]
+
+=head1 DESCRIPTION
+
+This script is a wrapper around msktutil and ldapsearch to simplify
+the creation of host and service keytabs.  The script is useful for
+boot strapping the kerberos credentials required to use Active
+Directory as a backend keytab store for wallet.  The script shares
+the wallet configuration file.
+
+Generally, two keytabs will need to be created to setup update.  One
+host keytab for the wallet server host and one service keytab for
+wallet to use when connecting to an Active Directory Domain
+Controller.
+
+Note, this script does not update the Wallet database which means
+any keytabs created by it will be invisible from wallet.
+
+=head1 ACTIONS
+
+=over 4
+
+=item create
+
+Add a keytab to AD and update the keytab file.  Fails if the keytab
+already exists.
+
+=item update
+
+Update an existing keytab in AD and update the keytab file.  Fails if
+the keytab does not exist.
+
+=item delete
+
+Delete a keytab from AD and remove it from the keytab file.
+
+=item show
+
+Show AD's view of the account corresponding to the keytab.  This action
+does not use msktutil and queries AD directly using LDAP.
+
+=back
+
+=head1 OPTIONS AND ARGUMENTS
+
+=over 4
+
+=item keytab-id
+
+This is either host principal name of the form host/<fqdn> or a
+service principal name of the form service/<id>.  Service keytab
+identifiers cannot be longer than 18 characters because of an
+ActiveDirectory restriction.
+
+=item keytab-filename
+
+The name of the keytab file.  Defaults to /etc/krb5.keytab.
+
+=item --conf=filename
+
+The configuration file to read.  The script searches for a configuration
+file in the following order.
+
+      * The command line switch --conf
+      * The environment variable ADKEYTAB
+      * The file .ad-keytab.conf
+      * The file /etc/ad-keytab.conf
+
+=item --ad_server=hostname
+
+The name of the Active Directory host to connect to.  It is important
+what the script contact only _one_ server due to the fact that
+propagation within an Active Directory domain can be quite slow.
+
+=item --base_dn=ou=org,dc=domain,dc=tld
+
+The base distinguished name holding both computer and user accounts.
+
+=item --computer_rdn=dn
+
+The relative distinguished name to use as the base DN for both the
+creation of host keytabs and searches of Active Directory.  The
+distinguished name formed will be computer_rdn,base_dn.
+
+=item --user_rdn=dn
+
+The relative distinguished name to use as the base DN for ldap
+searches of Active Directory for service keytabs.  The distinguished
+name formed will be user_rdn_rdn,base_dn.
+
+=item --dump
+
+When displaying keytab attributes show all of the attributes.
+
+=item --help
+
+Displays help text.
+
+=item --manual
+
+Displays more complete help text.
+
+=item --debug
+
+Turns on debugging displays.
+
+=back
+
+=head1 SEE ALSO
+
+Set the documentation for Wallet::Config for configuration information, i.e.
+perldoc Wallet::Config.
+
+=head1 AUTHOR
+
+Bill MacAllister <whm@dropbox.com>
+
+=cut
index 65157563a018d3c3190c7b10254be5c761a9de6d..2222aba5a9b203a706c56b8554dac173b44e1d72 100644 (file)
@@ -415,40 +415,39 @@ our $KEYTAB_TMP;
 
 =back
 
-The following parameters are specific to generating keytabs from Active
-Directory (KEYTAB_KRBTYPE is set to C<AD>).
+The following parameters are specific to generating keytabs from
+Active Directory (KEYTAB_KRBTYPE is set to C<AD>).
 
 =over 4
 
-=item AD_CACHE
-
-Specifies the ticket cache to use when manipulating Active Directory objects.
-The ticket cache must be for a principal able to bind to Active Directory and
-run B<msktutil>.
+=item AD_BASE_DN
 
-AD_CACHE must be set to use Active Directory support.
+The base distinguished name of the ActiveDirectory instance.  This is
+use when Wallet uses LDAP directly to examine objects in Active
+Directory.
 
 =cut
 
-our $AD_CACHE;
+our $AD_BASE_DN;
 
-=item AD_COMPUTER_DN
+=item AD_COMPUTER_RDN
 
-The LDAP base DN for computer objects inside Active Directory.  All keytabs of
-the form host/<hostname> will be mapped to objects with a C<samAccountName> of
-the <hostname> portion under this DN.
+The LDAP base DN for computer objects inside Active Directory.  All
+keytabs of the form host/<hostname> will be mapped to objects with a
+C<samAccountName> of the <hostname> portion under this DN.
 
-AD_COMPUTER_DN must be set if using Active Directory as the keytab backend.
+AD_COMPUTER_RDN must be set if using Active Directory as the keytab
+backend.
 
 =cut
 
-our $AD_COMPUTER_DN;
+our $AD_COMPUTER_RDN;
 
 =item AD_DEBUG
 
-If set to true, asks for some additional debugging information, such as the
-B<msktutil> command, to be logged to syslog.  These debugging messages will be
-logged to the C<local3> facility.
+If set to true, asks for some additional debugging information, such
+as the B<msktutil> command, to be logged to syslog.  These debugging
+messages will be logged to the C<local3> facility.
 
 =cut
 
@@ -464,17 +463,25 @@ default PATH.
 
 our $AD_MSKTUTIL = 'msktutil';
 
-=item AD_USER_DN
+=item AD_SERVER
+
+The hostname of the Active Directory Domain Controller.
+
+=cut
+
+our $AD_SERVER;
+
+=item AD_USER_RDN
 
 The LDAP base DN for user objects inside Active Directory.  All keytabs of the
 form service/<user> will be mapped to objects with a C<servicePrincipalName>
 matching the wallet object name under this DN.
 
-AD_USER_DN must be set if using Active Directory as the keytab backend.
+AD_USER_RDN must be set if using Active Directory as the keytab backend.
 
 =cut
 
-our $AD_USER_DN;
+our $AD_USER_RDN;
 
 =back
 
@@ -482,8 +489,9 @@ our $AD_USER_DN;
 
 Heimdal provides the choice, over the network protocol, of either
 downloading the existing keys for a principal or generating new random
-keys.  MIT Kerberos does not; downloading a keytab over the kadmin
-protocol always rekeys the principal.
+keys.  Neither MIT Kerberos or ActiveDirectory support retrieving an
+existing keytab; downloading a keytab over the kadmin protocol or
+using msktutil always rekeys the principal.
 
 For MIT Kerberos, the keytab object backend therefore optionally supports
 retrieving existing keys, and hence keytabs, for Kerberos principals by
@@ -491,6 +499,11 @@ contacting the KDC via remctl and talking to B<keytab-backend>.  This is
 enabled by setting the C<unchanging> flag on keytab objects.  To configure
 that support, set the following variables.
 
+For ActiveDirectory Kerberos, the keytab object backend supports
+storing the keytabs on the wallet server.  This functionality is
+enabled by setting the configuration variable AD_KEYTAB_BUCKET.  (This
+had not been implemented yet.)
+
 This is not required for Heimdal; for Heimdal, setting the C<unchanging>
 flag is all that's needed.
 
@@ -542,6 +555,25 @@ will be used.
 
 our $KEYTAB_REMCTL_PORT;
 
+=item AD_CACHE
+
+The ticket cache that hold credentials used to access the
+ActiveDirectory KDC.  This must be created and maintained externally.
+
+=cut
+
+our $AD_CACHE;
+
+=item AD_KEYTAB_BUCKET
+
+The path to store a copy of keytabs created.  This is required for the
+support of unchanging keytabs with an ActiveDirectory KDC.  (This has
+not been implemented yet.)
+
+=cut
+
+our $AD_KEYTAB_BUCKET = '/var/lib/wallet/keytabs';
+
 =back
 
 =head1 WEBAUTH KEYRING OBJECT CONFIGURATION
index ec60af9fd5d733a12063f40256599081c7cc3671..1c13ab611bc6214670d41006b33fd91bb5f10b55 100644 (file)
@@ -1,8 +1,8 @@
 # Wallet::Kadmin::AD -- Wallet Kerberos administration API for AD
 #
-# Written by Bill MacAllister <bill@ca-zephyr.org>
+# Written by Bill MacAllister <whm@dropbox.com>
 # Copyright 2016 Russ Allbery <eagle@eyrie.org>
-# Copyright 2015 Dropbox, Inc.
+# Copyright 2015,2016 Dropbox, Inc.
 # Copyright 2007, 2008, 2009, 2010, 2014
 #     The Board of Trustees of the Leland Stanford Junior University
 #
@@ -100,17 +100,19 @@ sub ldap_base_filter {
         my $fqdn = $1;
         my $host = $fqdn;
         $host =~ s/[.].*//xms;
-        $base   = $Wallet::Config::AD_COMPUTER_DN;
         $filter = "(samAccountName=${host}\$)";
+        $base   = $Wallet::Config::AD_COMPUTER_RDN . ','
+          . $Wallet::Config::AD_BASE_DN;
     } elsif ($principal =~ m,^service/(\S+),xms) {
         my $id = $1;
-        $base   = $Wallet::Config::AD_USER_DN;
         $filter = "(servicePrincipalName=service/${id})";
+        $base
+          = $Wallet::Config::AD_USER_RDN . ',' . $Wallet::Config::AD_BASE_DN;
     }
     return ($base, $filter);
 }
 
-# TODO: Get a keytab from the keytab cache.
+# TODO: Get a keytab from the keytab bucket.
 sub get_ad_keytab {
     my ($self, $principal) = @_;
     return;
@@ -125,13 +127,16 @@ sub get_ad_keytab {
 sub msktutil {
     my ($self, $args_ref) = @_;
     unless (defined($Wallet::Config::KEYTAB_HOST)
+        and defined($Wallet::Config::KEYTAB_PRINCIPAL)
+        and defined($Wallet::Config::KEYTAB_FILE)
         and defined($Wallet::Config::KEYTAB_REALM))
     {
         die "keytab object implementation not configured\n";
     }
-    unless (defined($Wallet::Config::AD_CACHE)
-        and defined($Wallet::Config::AD_COMPUTER_DN)
-        and defined($Wallet::Config::AD_USER_DN))
+    unless (-e $Wallet::Config::AD_MSKTUTIL
+        and defined($Wallet::Config::AD_BASE_DN)
+        and defined($Wallet::Config::AD_COMPUTER_RDN)
+        and defined($Wallet::Config::AD_USER_RDN))
     {
         die "Active Directory support not configured\n";
     }
@@ -192,14 +197,16 @@ sub ad_create_update {
         my $fqdn = $1;
         my $host = $fqdn;
         $host =~ s/[.].*//xms;
+        push @cmd, '--base',          $Wallet::Config::COMPUTER_RDN;
         push @cmd, '--dont-expire-password';
         push @cmd, '--computer-name', $host;
-        push @cmd, '--upn', "host/$fqdn";
-        push @cmd, '--hostname', $fqdn;
+        push @cmd, '--upn',           "host/$fqdn";
+        push @cmd, '--hostname',      $fqdn;
     } elsif ($principal =~ m,^service/(\S+),xms) {
         my $service_id = $1;
+        push @cmd, '--base',         $Wallet::Config::USER_RDN;
         push @cmd, '--use-service-account';
-        push @cmd, '--service', "service/$service_id";
+        push @cmd, '--service',      "service/$service_id";
         push @cmd, '--account-name', "srv-${service_id}";
         push @cmd, '--no-pac';
     }
@@ -365,9 +372,15 @@ sub ad_delete {
         if ($k_type eq 'host') {
             my $host = $k_id;
             $host =~ s/[.].*//;
-            $dn = "cn=${host}," . $Wallet::Config::AD_COMPUTER_DN;
+            $dn
+              = "cn=${host},"
+              . $Wallet::Config::AD_COMPUTER_RDN . ','
+              . $Wallet::Config::AD_BASE_DN;
         } elsif ($k_type eq 'service') {
-            $dn = "cn=srv-${k_id}," . $Wallet::Config::AD_USER_DN;
+            $dn
+              = "cn=srv-${k_id},"
+              . $Wallet::Config::AD_USER_RDN . ','
+              . $Wallet::Config::AD_BASE_DN;
         }
     }
 
@@ -435,18 +448,6 @@ using a local keytab cache.
 To use this class, several configuration parameters must be set.  See
 L<Wallet::Config/"KEYTAB OBJECT CONFIGURATION"> for details.
 
-=head1 FILES
-
-=over 4
-
-=item KEYTAB_TMP/keytab.<pid>
-
-The keytab is created in this file and then read into memory.  KEYTAB_TMP
-is set in the wallet configuration, and <pid> is the process ID of the
-current process.  The file is unlinked after being read.
-
-=back
-
 =head1 LIMITATIONS
 
 Currently, this implementation calls an external B<msktutil> program rather