Tags

, , , , , , , , ,

Keytab 101

If you have been in places where people were trying to configure single sign-on for Java based application servers such as Oracle WebLogic or JBOSS or IBM WebSphere you might probably have heard the word “keytab” many times. You may also have created many of these “keytab” files and handed them over to the dev guys but still have no idea of what they are – well at least that was me a year back. 🙂

You can read the Massachusetts Institute of Technology (MIT)’s definition of a keytab in here and a very detailed outline about it by Indiana University in here. The gist however is that,

A keytab is a file containing pairs of Kerberos principals and encrypted keys (derived from the Kerberos password), which can be used to log in using Kerberos without being prompted for a password. The most common use of a keytab file is to store a password in a plaintext file in a secure manner.

Alright, we now know what this “keytab” file is but as a Windows sysadmin how does this simple plain text file relate to our daily work when a fellow developer requests one?

Creating a keytab

It’s a 3 step process to fulfill such a request in a Windows domain. The only prerequisite is that you should know the computer name on which this keytab file will be used on and a computer account for that computer should exist in Active Directory.

  1. Create a service account (basically a user account).
  2. Run Ktpass.exe to generate the keytab file.
  3. Test the keytab file (optional step yet an invaluable time saver sometimes).

Ktpass

Ktpass is the tool that does all the magic. If it is run correctly then it generates a keytab file after configuring all the SPN (Service Principal Name) mappings and UPN (User Principal Name) changes in Active Directory. If the user that executes Ktpass lacks any of the required permissions (I will talk in detail about the permissions in a different post) then the execution will fail without generating a keytab file at the step where that lacking privileges are needed.

The Ktpass syntax is customizable according to your individual needs, which is beyond the scope of this post, but it needs a few basic yet important parameters in a specific order to run properly (at least in my experience). It is seldom to change the syntax often and once you figure out the syntax that works for you and produce the outcome your development teams need, then you’re on for automating it.

Automation

Before we dive into the script let’s see the needs of our target customer.

  1. A unique naming standard that depicts the purpose of the service accounts.
  2. Being a global organization, the service account name should depict the region to which it belong.

So in par with the needs I generate the username for the service accounts in the form “appsso<region><#>” where the region is determined by the target application server name and # is an incrementing positive integer. Of course, you can customize this process however you want. Optionally I also generate a 10 character complex password for the service account and restrict it from logging onto workstations other than the one where it is to be used on. The output is a HTML response which is emailed to the email address attached to the domain user account given during the run time.

(Tip: I designed the script to be run from a web site. So if you use Windows authentication in the web site then you can pass the current user’s username directly into the script.)

I have posted the entire script at the bottom of this post but I recommend it to be downloaded directly from here as WordPress sometimes messes up the PowerShell syntax.

What is NOT automated here

The script does not automate the creation of the keytab file. Instead it formulates the Ktpass syntax required to create the ketab file, which should be run manually.

I did automate this part as well earlier (although it was not easy to run this simple command in PowerShell) but it did not provide consistent results during the tests. Most of the times the generated keytab files were corrupted and at other times the script simply did not complete for some reason.

I hope you get to ease your life by automating this one. Until next time!

# ============================================================================================================
# name			: create_keytabfile_prereqs.ps1
# author		: Nimantha Wickremasinghe
# purpose		: Create Service Account for Keytab File
# version		: 1.0
# dependencies	: Powershell modules - ActiveDirectory
# assumptions	: 1 - Site codes consist of 3 characters and are prepended on all computer accounts
# ============================================================================================================

param (
	[Parameter(Mandatory=$true)]
	[string]$requestId,
	[Parameter(Mandatory=$true)]
	[string]$ssoServer,
	[Parameter(Mandatory=$true)]
	[string]$action,
	[Parameter(Mandatory=$true)]
	[string]$execUser,
	[Parameter(Mandatory=$true)]
	[string]$source
)

$strPat = "^[a-zA-Z0-9\s]+$"
$errorsList = @()
$ErrorActionPreference = "stop"
if ($ssoServer -match $strPat) {
	if ($requestId -match $strPat) {
		try {			
			$date = Get-Date -Format "yyyyMMdd"
			$ssoServer = $ssoServer.ToLower()
			$principalType = "KRB5_NT_PRINCIPAL"
            # IMPORTANT - Replace mycompany with your domain name
			$principalName = "HTTP/$ssoServer.mycompany.com@MYCOMPANY.COM"
			
            # IMPORTANT - Replace SomeSharedNetworkPath with a valid location
			$outputPath = "\\SomeSharedNetworkPath\KeytabFiles"
			$outputFileName = "keytab_"+ $ssoServer +"_"+ $requestId +".keytab"
			$outputPathFull = "$outputPath\$outputFileName"
			$outputPathUrl = $outputPath -replace ("\\", "/")
			$outputFileUrl = "file:///"+ $outputPathUrl +"/$outputFileName"
			$OuPath = 'OU=dev,OU=Users,DC=mycompany,DC=com';
			$siteCode = $ssoServer.Substring(0, 3)
			$userAccountDescription = "RequestId: "+ $requestId +" | SSOServer: "+ $ssoServer +" | CreatedBy: "+ $execUser
			$script:password = $null
            
            # IMPORTANT - Replace with your actual SMTP address
            $smtpServer = "smtp.mycompany.com"
            $smtpSender = "no-reply@mycompany.com"
            $smtpReplyTo = "me@mycompany.com"
			
			# +--+--+--+--+
			# |	FUNCTIONS |
			# +--+--+--+--+

			function GetNextAvailableName {			
				param (
					[string]$site
				)			
				Import-Module ActiveDirectory -ErrorAction Stop				
				# convert site to country code
                # main site codes are added here
				switch ($site) {				
					"cmb" {$countryCode = "lk"}
					"nyc" {$countryCode = "us"}
					default {$countryCode = $site}
				}				

				$strPat2 = "appsso"+ $countryCode +"*"
				$existingAccnts = Get-ADUser -Filter {(sAMAccountName -like $strPat2) -and (ObjectClass -eq "user")} | Sort-Object sAMAccountName
				
				$count = $chkVar = 1
				if ($existingAccnts) {
					$existingAccntsCount = ($existingAccnts | Measure-Object).Count
					$lastAccount = $existingAccnts[$existingAccntsCount - 1]
					$lastAccountUname = $lastAccount.SamAccountName
					
					while ($chkVar) {
						$ErrorActionPreference = "continue"
						Clear-Variable chkVar
						$uname = "appsso"+ $countryCode + $count
						
						try {					
							$chkVar = Get-ADUser $uname
						}
						catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {						
							$chkVar = $null
						}
						catch {						
							$chkVar = $null
						}					
						if ($chkVar) {					
							$count++
						}
					}
				} else {
				    # DO NOTHING
				}
				$newNumber = $count
				$availableName = "appsso"+ $countryCode + $newNumber

				return $availableName
			}

			function GeneratePassword {
				$passwordLength = 10 # default length
				$passwordNumericLength = 1
				$passwordAlphaCapsLength = 1
				$passwordSymbolLength = 1
				$passwordAlphaLength = $passwordLength - $passwordNumericLength - $passwordAlphaCapsLength

				# generate random password that includes Alphabetic, Numeric, and a few “Specials”
				$password = (([char[]](48..122) | sort {Get-Random})[1..$passwordLength] -join '')
                # sanitize password to avoid powershell syntax interference
				$password = $password -replace "\\", "."
				$password = $password -replace "<", "."
				$password = $password -replace ">", "."
				$password = $password -replace ",", "."
				$password = $password -replace ";", "."
				
				return $password
			}			

			function CreateADUser {			
				param (
					[string]$fullName, 
					[string]$userName,
					[string]$password
				)
				[bool]$passwordNeverExpires = $true
				[bool]$cannotChangePassword = $true
				$upn = "$userName@mycompany.com"			
				
				$securePwd = ConvertTo-SecureString -String $password -AsPlainText -Force

				Import-Module ActiveDirectory -ErrorAction Stop | Out-Null

				New-ADUser -Name $fullName -SamAccountName $userName -DisplayName $fullName -AccountPassword $securePwd -UserPrincipalName $upn -Description $userAccountDescription -Path $OuPath -Enabled $true -PasswordNeverExpires $passwordNeverExpires -CannotChangePassword $cannotChangePassword -LogonWorkstations $ssoServer
			}

			function SendMail {			
				param (
					[string]$recipientUname, 
					[string]$subject, 
					[string]$msgBody,
					[string]$attachmentFullPath
				)				
				$recipientMailAddr = (Get-ADUser -Identity $recipientUname -Properties mail).mail
								
				$msg = New-Object Net.Mail.MailMessage
				$smtp = New-Object Net.Mail.SmtpClient($smtpServer)
				
				$msg.From = $smtpSender
				$msg.ReplyTo = $smtpReplyTo
				$msg.To.Add($recipientMailAddr)
				$msg.Subject = $subject
				$msg.Body = $msgBody
				$msg.IsBodyHTML = $true
				
				if (Test-Path -Path $attachmentFullPath) {				
					$attachment = New-Object System.Net.Mail.Attachment($attachmentFullPath, 'text/plain')
	  				$msg.Attachments.Add($attachment)
				}				
				$smtp.Send($msg)
			}			

			# +--+--+--+
			# |	MAIN  |
			# +--+--+--+
			Import-Module ActiveDirectory -ErrorAction Stop | Out-Null			
			# check if computer account exists
			$ssoServerADComputerAccount = Get-ADComputer -Identity $ssoServer -ErrorAction SilentlyContinue
			
			if ($ssoServerADComputerAccount) {				
				# create AD user
				$fullName = "App SSO Service Account "+ $ssoServer
				$svcAccntName = GetNextAvailableName -site $siteCode
				$ktPassMapuser = "MYCOMPANY\$svcAccntName"
				$randomPwd = GeneratePassword
				CreateADUser -fullName $fullName -userName $svcAccntName -password $randomPwd

				# ktpass command to run
				$ktpass = "ktpass -princ $principalName -mapuser $ktPassMapuser -pass $randomPwd -out $outputPathFull -kvno 0 -ptype $principalType"
				
				# kinit to test
				$kinit = "kinit -k -t $outputPathFull $principalName"
				
                #=== Transaction Information - Customer Copy ===#
				# output to the web page
				$htmlHeader = @()
				$htmlHeader += ""
				
				$htmlBody = @()
				$htmlBody += ""
				$htmlBody += "<span style="font-family: cambria;"><span style="font-family: cambria;">"
				$htmlBody += "<span style="color: green;"> New user account created! </span>"
				$htmlBody += "
"
				$htmlBody += "
"
				$htmlBody += "</span></span>"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += "</pre>
<table>
<tbody>
<tr>
<td> Full name</td>
<td> :</td>
<td> $fullName</td>
</tr>
<tr>
<td> User name</td>
<td> :</td>
<td> $svcAccntName</td>
</tr>
<tr>
<td> Password</td>
<td> :</td>
<td> $randomPwd</td>
</tr>
<tr>
<td> Description</td>
<td> :</td>
<td> $userAccountDescription</td>
</tr>
<tr>
<td> Generate keytab</td>
<td> :</td>
<td> $ktpass</td>
</tr>
<tr>
<td> Test keytab</td>
<td> :</td>
<td> $kinit</td>
</tr>
</tbody>
</table>
<pre><span style="font-family: cambria;"><span style="font-family: cambria;">"
                #---Supplementary Information---#
                $htmlBody += "</span></span></pre>

<hr />

<pre><span style="font-family: cambria;"><span style="font-family: cambria;">"
				$htmlBody += "</span></span>"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += ""
				$htmlBody += "	"
				$htmlBody += "</pre>
<table>
<tbody>
<tr>
<td> How to use Ktpass.exe</td>
<td> :</td>
<td> <a href="https://support.microsoft.com/en-us/kb/324144"> MS KB324144 </a></td>
</tr>
<tr>
<td> Obtaining Ktpass.exe</td>
<td> :</td>
<td> <a href="https://support.microsoft.com/en-us/kb/892777"> MS KB892777 </a></td>
</tr>
</tbody>
</table>
<pre><span style="font-family: cambria;">"
				$htmlBody += "
"
				$htmlBody += "</span>"
				$htmlBody += ""
				
				$htmlFooter = @()
				$htmlFooter += ""
				$htmlFooter += "Directory Services - MYCOMPANY"
				$htmlFooter += ""
				$htmlFooter += ""
                ##=== Transaction Information - Customer Copy ===##
				
				$htmlContent = $htmlHeader + $htmlBody + $htmlFooter
				Write-Output $htmlContent
				
				$mailSubject = "New Service Account for Keytab File Created ($requestId)"
				SendMail -recipientUname $execUser -subject $mailSubject -msgBody $htmlContent -attachmentFullPath $outputPathFull
			}
			else {
				$error = "<span style="color: red;">Active Directory computer account for $ssoServer does not exist!</span>"
                Write-Output $error
			}
		}
		catch {
			$error = "Error: $Error"
            Write-Output $error
		}
	}
	else {
		Write-Output "<span style="color: red;">Invalid characters in the Case ID!</span>"
	}
}
else {
	Write-Output "<span style="color: red;">Invalid characters in the Server Name!</span>"
}

Advertisements