Valid HTML 4.01 Transitional
Prev: Home Assistant Refresh Key Expired
(Index)
Jim Carter's Bugfixes

Dovecot Mail/IMAP Server Refuses GSSAPI (Kerberos)

James F. Carter
2026-07-30
Symptom:

The mail client (reader) is Thunderbird; this should apply to any mail client. It's set up to use GSSAPI (Kerberos) authentication to the IMAP mail server, which is Dovecot. Everything's working fine: the user's Kerberos ticket is honored for reading mail.

Until I upgrade Dovecot to version 2.4. Thunderbird says that the Kerberos ticket was rejected by the server. Switching back to password authentication gets me access to my mail, hiss, boo.

What's happening:

Strace on Dovecot reveals that when starting up it tries to open the default keytab /etc/krb5.keytab, EACCES (permission denied). The file has mode 0640 root:hostcert and dovecot is in the hostcert group. (This group is not from my distro, OpenSuSE Tumbleweed; my deamons that need access to a host certificate or key are in the group.) But Dovecot's authentication process has dropped privileges to UID:GID dovenull:dovecot and it's no lie that the process lacks permission to read the keytab.

I didn't trace this out in detail, but subsequent tests suggest that when Dovecot starts up it reads the keytab as dovecot:dovecot, having partially but not completely dropped privileges. And evidently if the key for IMAP didn't get stashed at that time, the individual authentication process tries to re-read as dovenull:dovecot, failing.

How to fix:

Several sources on the Internet suggested this: It's poor practice to change permissions on the default keytab (/etc/krb5.keytab) to let dovenull read every service ticket on the machine. Instead, extract the IMAP and dovecot keys (and POP if you use it) to a separate keytab that dovecot:dovecot can read. Suggestion: make the mode 440 dovecot:dovecot because dovenull is also in group dovecot and thus could read the keytab too.

The only problem here is, this operation is manual and requires the sysadmin to authenticate as their admin sub-user, so it can't be automated. If anything changes the IMAP or Dovecot key, even the version (kvno), nobody will remember to re-do extraction into Dovecot's private keytab, and we'll have to go through the whole debugging procedure again. So instead of extracting the needed keys, I copy the whole default keytab and delete all irrelevant keys, which can be done by ktutil without any authorization. I run the script below daily. 99.9% of the time the keytab is unchanged and the new version is shredded with few resouces being wasted, but if ever a relevant key gets changed, and if the sysadmin forgets to run the script by hand (as root), the issue will self-heal within a day.

#!/bin/bash
# Extract Kerberos keytab for Dovecot.  
# Copyright (c) 2026 by James F. Carter , 2026-07-27
# SPDX-License-Identifier: CC-BY-SA-4.0 / https://spdx.org/licenses/CC-BY-SA-4.0.html

# To respond to a client that tries to authenticate by Kerberos, the Dovecot
# main process forks a pair of proceses one of which forks a new process which
# handles the authentication.  It needs to read from a local keytab the private
# key for Kerberos.  But all of these processes drop privileges to 
# UID-422=dovenull GID=498=dovecot which is great for security but not for
# reading /etc/krb5.keytab (640 root:hostcert, which dovenull is not in and
# isn't *going* to be in).  Solution (pretty sanitary): extract Dovecot's
# key(s) into a separate keytab which it does have permission to read: 
# /etc/dovecot/krb5.keytab.dovecot 400 dovenull:dovecot
# *** Except a setup process gets in ahead, owner should be dovecot.  

# So if some maintenance action changes the key, nobody is going to remember
# to update the keytab.  This script takes care of it.  

# Command line arguments:
#   -v		On STDERR print what, if anything, was done to the keytab. 
#		Actual errors are always printed.  
#   Exit code	0 = keytab was unchanged; 1 = was replaced; 2 = actual error.

# Additional requisites: 
#   .	The keytab /etc/dovecot/krb5.keytab.dovecot needs to be readable by
#	a process having UID dovecot, group dovecot.  I have mode 0400 
#	(readonly by owner), owner dovecot, group irrelevant.  
#   .	/etc/dovecot.conf needs to say:
#	auth_mechanisms = plain login gssapi
#	auth_krb5_keytab = /etc/dovecot/krb5.keytab.dovecot
#	auth_gssapi_hostname = "$ALL"

# The following were mentioned in forum posts if the above was not sufficient.
# But not needed in my case.  
#   .	/etc/dovecot.conf also needs:
#	import_environment = $import_environment KRB5_KTNAME  (or equivalent)
#   .	dovecot service unit needs:
#	Environment=KRB5_KTNAME=/etc/dovecot/krb5.keytab.dovecot

[ "X$1" != "X-v" ] ; opt_v=$?		# Value 1 if present, 0 if not.
rc=0					# The eventual exit code

# Prints an error or information message on STDERR.  Args:
#   $1 $code: to get the message printed, global variable $rc must be
#		nonzero (replaced table, or error), or $code -ge 1, 
#		or -v on the command line.  
#   $2 $msg: the rest of the argument(s) make up the message.  No ending 
#		newline.  Backslash escapes like \n or \t are allowed.  
#   Returns	0 (success) always.  
function whine () {
    local code=$1 ; shift
    if [ $rc -ge 1 -o $code -ge 1 -o $opt_v -ge 1 ] ; then 
	echo -e "$*" 1>&2
    fi
    return 0
}

# Should run only on the host(s) running dovecot; exit silently if not.
[ "$(systemctl is-enabled dovecot)" == "enabled" ] || exit 0

whine 1 "==== Check dovecot's Kerberos (GSSAPI) keytab (dovecot.J)"

logf=/var/tmp/system/dovecot.J.log

# Creates a new correct keytab whether needed or not. 
ktabdf=/etc/krb5.keytab			# The default keytab
ktab=/etc/dovecot/krb5.keytab.dovecot	# Dovecot's private keytab
ktabn=$ktab.new				# New version of Dovecot's keytab
rm -f $ktabn

# Extracts the Kerberos key(s) for IMAP and drops them in the new keytab. 
# Actually it copies the whole default keytab and deletes irrelevant keys,
# which it can do without authenticating to the KDC to retrieve relevant keys. 
(
umask 0377

# Explanation of the big pipeline: in ktutil, when you delete a slot, following
# slots are renumbered, so you have to delete from end to front.  
#	echo "$ktu_1" | ktutil | 		# Convert default keytab to text
#	awk '$1 ~ /^[0-9][0-9]*$/ {print}' |	# Print keys, omit title
#	sort -k1,1nr |				# Sort reversed by slot number
#	awk -v src=$ktabdf -v dest=$ktabn 
#	    'BEGIN {print "read_kt", src}	# ktutil reads default keytab
#	    $5 !~ /^imap\// && $5 !~ /^dovecot\// {print "delete_entry", $1}
#						# Delete irrelevant keys
#	    END {print "write_kt", dest}' |	# Survivors to Dovecot keytab
#	ktutil >& $logf				# ktutil #2 execs these cmds.
ktu_1="read_kt $ktabdf
list -t -k -e
quit
"
echo "$ktu_1" | ktutil | \
    awk '$1 ~ /^[0-9][0-9]*$/ {print}' | \
    sort -k1,1nr | \
    awk -v src=$ktabdf -v dest=$ktabn 'BEGIN {print "read_kt", src}
	$5 !~ /^imap\// && $5 !~ /^dovecot\// {print "delete_entry", $1}
	END {print "write_kt", dest}' | \
    ktutil >& $logf
)

chown dovecot:dovecot $ktabn
# chmod 0400 $ktabn # should have happened due to umask

# If you extract a keytab twice, the 2 instances won't compare equal, probably
# because all keys' timestamp are changed to different present times.  This
# function canonicalizes a keytab, hashes it, and deposits the result.  Args:
#   $1 $ktab		The keytab to be hashed.  If it doesn't exist or is
#			empty, '' (empty string) is the returned value.  
#   $2 $Hash		Name in caller's context of a variable where the hash
#			is delivered.  
#   Returns		Nothing; the hash is left in $Hash.
function hashkt () {
    local ktab="$1"
    declare -n Hash="$2"
    if [ ! -s $ktab ]; then Hash='' ; return ; fi
    local ktu_1="read_kt $ktab
list -k -e
quit
"
# Explanation of this pipeline: 
#	Hash="$(echo "$ktu_1" | ktutil |	# Convert $ktab to text
#	awk '$1 ~ /^[0-9][0-9]*$/ {print}' |	# Lose the title lines
#	sort -k1,1nr |				# Sort in a repeatable order
#	awk '{$1 = "" ; print}' |		# Remove slot nbr and print
#	sha256sum | tr -d ' ')"			# Delivers canonical hash
    Hash="$(echo "$ktu_1" | ktutil | \
		awk '$1 ~ /^[0-9][0-9]*$/ {print}' | \
		sort -k1,1nr | \
		awk '{$1 = "" ; print}' | \
		sha256sum | tr -d ' ')"
}

# Has the keytab changed?  Install the new one if so.  Values of chgd:
# 0 = keytab was not changed or new one was botched; 1 = no old keytab, 
# install the new one; 2 = new keytab differs from the old one; install it.
# rc = eventual exit code, same as $chgd except 2 is changed to 1 (set earlier).
chgd=0
hashkt $ktab  hash
hashkt $ktabn hashn

msg="No message (bug)"
if  [ -z "$hashn" ] ; then rc=2 ; msg="New keytab $ktabn was botched (bad)"
elif [ -z "$hash" ] ; then chgd=1 ; rc=1 ; msg="lacks $ktab, installing new one"
elif [ "$hashn" == "$hash" ] ; then
    msg="$ktab unchanged (good)"
else 
    chgd=2 ; rc=1 ; msg="$ktab changed, installing new one"
fi
whine 0 "$msg"

# Installing the new keytab (if changed).  Unwanted version gets shredded.
case $chgd in
    (0)	shred -u $ktabn ;;		# Keytab is unchanged, toss new one.
    (1)	mv $ktabn $ktab ;;		# Missing old keytab, just install new.
    (2)	mv $ktab $ktab.old		# Unequal keytabs, shred old, inst new
	mv $ktabn $ktab
	shred -u $ktab.old
	;;
esac

exit $rc

Prev: Home Assistant Refresh Key Expired
(Index)