diff options
author | Bill MacAllister <whm@dropbox.com> | 2016-05-02 20:44:19 +0000 |
---|---|---|
committer | Russ Allbery <eagle@eyrie.org> | 2018-05-27 17:33:31 -0700 |
commit | 8bfba28196485236125ad363ed3b96c461025d94 (patch) | |
tree | d77d871704cb400d5ca80e4a69bf22f28a789daa /perl/lib/Wallet/Kadmin/AD.pm | |
parent | 720b9492c14ce2f814549502c013e2cfdf7130ae (diff) |
Update AD keytab policies
* Make sure userPrincipalName is created for all keytabs and use it to
search for entries in AD.
* Allow the creation of any service principal. This requires making
sure that the cn used to create AD entries for service accounts not
be any longer than 20 characters.
Diffstat (limited to 'perl/lib/Wallet/Kadmin/AD.pm')
-rw-r--r-- | perl/lib/Wallet/Kadmin/AD.pm | 304 |
1 files changed, 178 insertions, 126 deletions
diff --git a/perl/lib/Wallet/Kadmin/AD.pm b/perl/lib/Wallet/Kadmin/AD.pm index 0ffd7d9..83912dd 100644 --- a/perl/lib/Wallet/Kadmin/AD.pm +++ b/perl/lib/Wallet/Kadmin/AD.pm @@ -28,6 +28,8 @@ use Wallet::Kadmin; our @ISA = qw(Wallet::Kadmin); our $VERSION = '1.04'; +my $LDAP; + ############################################################################## # kadmin Interaction ############################################################################## @@ -71,17 +73,7 @@ sub ad_cmd_string { # Note that we do not permit realm information here. sub valid_principal { my ($self, $principal) = @_; - my $valid = 0; - if ($principal =~ m,^(host|service)(/[\w_.-]+)?\z,) { - my $k_type = $1; - my $k_id = $2; - if ($k_type eq 'host') { - $valid = 1 if $k_id =~ m/[.]/xms; - } elsif ($k_type eq 'service') { - $valid = 1 if length($k_id) < 19; - } - } - return $valid; + return scalar ($principal =~ m,^[\w-]+(/[\w_.-]+)?\z,); } # Connect to the Active Directory server using LDAP. The connection is @@ -90,49 +82,110 @@ sub valid_principal { sub ldap_connect { my ($self) = @_; - if (!-e $Wallet::Config::AD_CACHE) { - die 'Missing kerberos ticket cache ' . $Wallet::Config::AD_CACHE; - } - - my $ldap; - eval { - local $ENV{KRB5CCNAME} = $Wallet::Config::AD_CACHE; - my $sasl = Authen::SASL->new(mechanism => 'GSSAPI'); - $ldap = Net::LDAP->new($Wallet::Config::KEYTAB_HOST, onerror => 'die'); - my $mesg = eval { $ldap->bind(undef, sasl => $sasl) }; - }; - if ($@) { - my $error = $@; - chomp $error; - 1 while ($error =~ s/ at \S+ line \d+\.?\z//); - die "LDAP bind to AD failed: $error\n"; + if (!$LDAP) { + eval { + local $ENV{KRB5CCNAME} = $Wallet::Config::AD_CACHE; + my $sasl = Authen::SASL->new(mechanism => 'GSSAPI'); + $LDAP = Net::LDAP->new($Wallet::Config::KEYTAB_HOST, + onerror => 'die'); + my $mesg = eval { $LDAP->bind(undef, sasl => $sasl) }; + }; + if ($@) { + my $error = $@; + chomp $error; + 1 while ($error =~ s/ at \S+ line \d+\.?\z//); + die "LDAP bind to AD failed: $error\n"; + } } - - return $ldap; + return $LDAP; } # Construct a base filter for searching Active Directory. sub ldap_base_filter { my ($self, $principal) = @_; + my $base; my $filter; - if ($principal =~ m,^host/(\S+),xms) { - my $fqdn = $1; - my $host = $fqdn; - $host =~ s/[.].*//xms; - $filter = "(samAccountName=${host}\$)"; - $base = $Wallet::Config::AD_COMPUTER_RDN . ',' - . $Wallet::Config::AD_BASE_DN; - } elsif ($principal =~ m,^service/(\S+),xms) { - my $id = $1; - $filter = "(servicePrincipalName=service/${id})"; - $base - = $Wallet::Config::AD_USER_RDN . ',' . $Wallet::Config::AD_BASE_DN; + my $this_type; + my $this_id; + + if ($principal =~ m,^(.*?)/(\S+),xms) { + $this_type = $1; + $this_id = $2; + } else { + $this_id = $principal; + } + + # Create a filter to find the objects we create + if ($this_id =~ s/@(.*)//xms) { + $filter = "(userPrincipalName=${principal})"; + } elsif ($Wallet::Config::KEYTAB_REALM) { + $filter = '(userPrincipalName=' . $principal + . '@' . $Wallet::Config::KEYTAB_REALM . ')'; + } else { + $filter = "(userPrincipalName=${principal}\@*)"; } + + # Set the base distinguished name + if ($this_type && $this_type eq 'host') { + $base = $Wallet::Config::AD_COMPUTER_RDN; + } else { + $base = $Wallet::Config::AD_USER_RDN; + } + $base .= ',' . $Wallet::Config::AD_BASE_DN; + return ($base, $filter); } +# Take in a base and a filter and return the assoicated DN or return +# null if there is no matching entry. +sub ldap_get_dn { + my ($self, $base, $filter) = @_; + my $dn; + + if ($Wallet::Config::AD_DEBUG) { + $self->ad_debug('debug', "base:$base filter:$filter scope:subtree\n"); + } + + $self->ldap_connect(); + my @attrs = ('objectclass'); + 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 ($Wallet::Config::AD_DEBUG) { + $self->ad_debug('debug', 'returned: ' . $result->count); + } + + if ($result->count == 1) { + for my $entry ($result->entries) { + $dn = $entry->dn; + } + } elsif ($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"); + } + + return $dn; +} + # TODO: Get a keytab from the keytab bucket. sub get_ad_keytab { my ($self, $principal) = @_; @@ -200,10 +253,53 @@ sub msktutil { return $out; } +# The unique identifier that Active Directory used to store keytabs +# has a maximum length of 20 characters. This routine takes a +# principal name an generates a unique ID based on the principal name. +sub get_service_id { + my ($self, $this_princ) = @_; + + my $this_id; + my ($this_base, $this_filter) = $self->ldap_base_filter($this_princ); + my $real_dn = $self->ldap_get_dn($this_base, $this_filter); + if ($real_dn) { + $this_id = $real_dn; + $this_id =~ s/,.*//xms; + $this_id =~ s/.*?=//xms; + } else { + my $this_cn = $this_princ; + $this_cn =~ s{.*?/}{}xms; + if ($Wallet::Config::AD_SERVICE_PREFIX) { + $this_cn = $Wallet::Config::AD_SERVICE_PREFIX . $this_cn; + } + my $loop_limit = $Wallet::Config::AD_SERVICE_LIMIT; + if (length($this_cn)>20) { + my $cnt = 0; + my $this_dn; + my $suffix_size = length("$loop_limit"); + my $this_prefix = substr($this_cn, 0, 20-$suffix_size); + my $this_format = "%0${suffix_size}i"; + while ($cnt<$loop_limit) { + my $this_cn = $this_prefix . sprintf($this_format, $cnt); + $this_dn = ldap_get_dn($this_base, "cn=$this_cn"); + if (!$this_dn) { + $this_id = $this_cn; + last; + } + $cnt++; + } + } else { + $this_id = $this_cn; + } + } + return $this_id; +} + # Either create or update a keytab for the principal. Return the # name of the keytab file created. sub ad_create_update { my ($self, $principal, $action) = @_; + return unless $self->valid_principal($principal); my $keytab = $Wallet::Config::KEYTAB_TMP . "/keytab.$$"; if (-e $keytab) { unlink $keytab or die "Problem deleting $keytab\n"; @@ -213,31 +309,41 @@ sub ad_create_update { push @cmd, '--enctypes', '0x1C'; push @cmd, '--keytab', $keytab; push @cmd, '--realm', $Wallet::Config::KEYTAB_REALM; - - if ($principal =~ m,^host/(\S+),xms) { - my $fqdn = $1; - my $host = $fqdn; - $host =~ s/[.].*//xms; - push @cmd, '--base', $Wallet::Config::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', $Wallet::Config::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'; - } - my $out = $self->msktutil(\@cmd); - if ($out =~ /Error:\s+\S+\s+failed/xms) { - $self->ad_delete($principal); - my $m = "ERROR: problem creating keytab:\n" . $out; - $m .= 'INFO: the keytab used to by wallet probably has' - . " insufficient access to AD\n"; - die $m; + push @cmd, '--upn', $principal; + + my $this_type; + my $this_id; + if ($principal =~ m,^(.*?)/(\S+),xms) { + $this_type = $1; + $this_id = $2; + if ($this_type eq 'host') { + my $host = $this_id; + $host =~ s/[.].*//xms; + push @cmd, '--base', $Wallet::Config::AD_COMPUTER_RDN; + push @cmd, '--dont-expire-password'; + push @cmd, '--computer-name', $host; + push @cmd, '--hostname', $this_id; + } else { + my $service_id = $self->get_service_id($this_id); + push @cmd, '--base', $Wallet::Config::AD_USER_RDN; + push @cmd, '--use-service-account'; + push @cmd, '--service', $principal; + push @cmd, '--account-name', $service_id; + push @cmd, '--no-pac'; + } + my $out = $self->msktutil(\@cmd); + if ($out =~ /Error:\s+\S+\s+failed/xms + || !$self->exists($principal)) + { + $self->ad_delete($principal); + my $m = "ERROR: problem creating keytab for $principal"; + $self->ad_debug('error', $m); + $self->ad_debug('error', + 'Problem command:' . ad_cmd_string(\@cmd)); + die "$m\n"; + } + } else { + die "ERROR: Invalid principal format ($principal)\n"; } return $keytab; @@ -260,45 +366,9 @@ sub exists { my ($self, $principal) = @_; return unless $self->valid_principal($principal); - my $ldap = $self->ldap_connect(); my ($base, $filter) = $self->ldap_base_filter($principal); - my @attrs = ('objectClass', 'msds-KeyVersionNumber'); - - 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) { - my $m; - $m .= "INFO base:$base filter:$filter scope:subtree\n"; - $m .= 'ERROR:' . $result->error . "\n"; - die $m; - } - if ($result->count > 1) { - my $m = "ERROR: too many AD entries for this keytab\n"; - for my $entry ($result->entries) { - $m .= 'INFO: dn found ' . $entry->dn . "\n"; - } - die $m; - } - if ($result->count) { - for my $entry ($result->entries) { - return $entry->get_value('msds-KeyVersionNumber'); - } - } else { - return 0; - } - return; + return $self->ldap_get_dn($base, $filter); } # Call msktutil to Create a principal in Kerberos. Sets the error and @@ -371,7 +441,7 @@ sub destroy { } my $exists = $self->exists($principal); if (!defined $exists) { - return; + return 1; } elsif (not $exists) { return 1; } @@ -384,29 +454,11 @@ sub destroy { sub ad_delete { my ($self, $principal) = @_; - my $k_type; - my $k_id; - my $dn; - if ($principal =~ m,^(host|service)/(\S+),xms) { - $k_type = $1; - $k_id = $2; - if ($k_type eq 'host') { - my $host = $k_id; - $host =~ s/[.].*//; - $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_RDN . ',' - . $Wallet::Config::AD_BASE_DN; - } - } + my ($base, $filter) = $self->ldap_base_filter($principal); + my $dn = $self->ldap_get_dn($base, $filter); - my $ldap = $self->ldap_connect(); - my $msgid = $ldap->delete($dn); + $self->ldap_connect(); + my $msgid = $LDAP->delete($dn); if ($msgid->code) { my $m; $m .= "ERROR: Problem deleting $dn\n"; |