diff --git a/configure.ac b/configure.ac
index ad9d53d..c620e39 100644
--- a/configure.ac
+++ b/configure.ac
@@ -75,6 +75,9 @@ AC_CHECK_LIB([oauth], [oauth_init_nss],
 	[AC_MSG_ERROR([Missing required oauth lib])])
 AC_SUBST([OAUTH_LIB])
 
+dnl XXX Copy how -loauth is done above
+LIBS="$LIBS -lgssapi_krb5"
+
 PKG_CHECK_MODULES([HAIL],[libhail >= 0.8])
 AC_SUBST([HAIL_LIBS])
 AC_SUBST([HAIL_CFLAGS])
@@ -105,6 +108,12 @@ AC_DEFINE_UNQUOTED([JANSSON_LOAD_FLAG], $iw_cv_func_jansson_flag,
 AC_CHECK_HEADER([oauth.h], ,
   [AC_MSG_ERROR([Missing OAuth development library: liboauth-devel])])
 
+AC_CHECK_HEADER([gssapi.h], [],
+  [AC_MSG_ERROR([Missing gssapi.h header])])
+dnl Maybe we should use setenv("KRB5_KTNAME",...) after all? That always works.
+AC_CHECK_HEADER([gssapi/gssapi_krb5.h], [],
+  [AC_MSG_ERROR([Missing gssapi/gssapi_krb5.h header])])
+
 # from http://www.gnu.org/software/autoconf-archive/
 AX_BOOST_BASE
 AX_BOOST_SYSTEM
diff --git a/rest.c b/rest.c
index 1aca0af..5f1cf1e 100644
--- a/rest.c
+++ b/rest.c
@@ -36,8 +36,11 @@
 #include <microhttpd.h>
 #include <hstor.h>	/* only for ARRAY_SIZE at this point */
 #include <curl/curl.h>
+#include <gssapi.h>
+#include <gssapi/gssapi_krb5.h>
 #include <oauth.h>
 
+#include "base64.h"
 #include "configmake.h" /* for LOCALEDIR */
 #include "dirname.h"
 #include "iwh.h"
@@ -121,6 +124,68 @@ static pthread_mutex_t		 my_lock = PTHREAD_MUTEX_INITIALIZER;
 static const char		*log_name;
 static char			 my_hostname[HOST_NAME_MAX+1];
 static bool			 oauth		= false;
+static bool			 kerberos	= false;
+/* XXX Make keytab location to follow --prefix. */
+static const char		*krb_keytab	= "/etc/iwhd/krb.keytab";
+static const char		*krb_svc_name	= "IWHD";
+#if 0 /* RFC says use default */
+static gss_cred_id_t		 gss_creds; /* struct* */   /* XXX needed? */
+#endif
+
+static void log_gss_error(const char *msg, unsigned int gss_min)
+{
+	char buf[700];
+	int len, rem;
+	int rc;
+	unsigned int gss_maj, dummy_min;
+	unsigned int msg_ctx = 0;
+	gss_buffer_desc gss_str;
+
+	rem = sizeof(buf);
+	len = 0;
+	buf[0] = 0;
+
+	rc = snprintf(buf+len, rem, "%s", msg);
+	if (rc >= rem)
+		goto out;
+	rem -= rc;
+	len += rc;
+
+	/*
+	 * See RFC-2744 clause 5.11. However, the practical code is different.
+	 *  - Everyone relies on the returned string nul-terminated,
+	 *    while RFC uses gss_buf.length.
+	 *  - Some people parse major codes (using different mech_type),
+	 *    while RFC gives no hint how.
+	 */
+	do {
+		gss_maj = gss_display_status(&dummy_min, gss_min,
+					     GSS_C_MECH_CODE, GSS_C_NO_OID,
+					     &msg_ctx, &gss_str);
+		if (gss_maj != GSS_S_COMPLETE) {
+			/*
+			 * If we put "unknown" here, someone may localize it.
+			 */
+			rc = snprintf(buf+len, rem, ": UNK");
+			if (rc >= rem)
+				goto out;
+			rem -= rc;
+			len += rc;
+			goto out;
+		}
+
+		rc = snprintf(buf+len, rem, ": %s", (char*)gss_str.value);
+		if (rc >= rem)
+			goto out;
+		rem -= rc;
+		len += rc;
+
+		gss_release_buffer(&dummy_min, &gss_str);
+	} while(msg_ctx);
+ out:
+	/* there's always a nul at buf[len] */
+	log_msg(0, "%s\n", buf);
+}
 
 static void
 conn_to_client(struct MHD_Connection *conn, char *buf, size_t len)
@@ -266,6 +331,13 @@ do_resp (struct MHD_Connection *conn, my_state *ms,
 		return MHD_NO;
 	}
 
+	/*
+	 * This special-casing seems asking for some kind of a generic header
+	 * list, but every time we try to write it, it comes out even worse.
+	 */
+	if (ms && ms->auth_hdr) {
+		MHD_add_response_header(resp,"WWW-Authenticate",ms->auth_hdr);
+	}
 	if (etag) {
 		MHD_add_response_header(resp,"ETag",etag);
 	}
@@ -291,6 +363,9 @@ do_resp_from_cb (struct MHD_Connection *conn, my_state *ms,
 		simple_closer(ms);
 		return MHD_NO;
 	}
+	if (ms->auth_hdr) {
+		MHD_add_response_header(resp,"WWW-Authenticate",ms->auth_hdr);
+	}
 	MHD_queue_response(conn,MHD_HTTP_OK,resp);
 	MHD_destroy_response(resp);
 	ms->rc = MHD_HTTP_OK;
@@ -495,9 +570,11 @@ proxy_get_data (void *cctx, struct MHD_Connection *conn, const char *url,
 		child_closer(pp);
 		return MHD_NO;
 	}
+	if (ms->auth_hdr) {
+		MHD_add_response_header(resp,"WWW-Authenticate",ms->auth_hdr);
+	}
 	MHD_queue_response(conn,ms->rc,resp);
 	MHD_destroy_response(resp);
-
 	return MHD_YES;
 }
 
@@ -1373,6 +1450,9 @@ control_api_root (void *cctx, struct MHD_Connection *conn, const char *url,
 	if (!resp) {
 		return MHD_NO;
 	}
+	if (ms->auth_hdr) {
+		MHD_add_response_header(resp,"WWW-Authenticate", ms->auth_hdr);
+	}
 	MHD_queue_response(conn,rc,resp);
 	MHD_destroy_response(resp);
 	ms->rc = rc;
@@ -2431,6 +2512,170 @@ auth_hdr_split (int *pargc, char ***pargv, const char *val)
 	return true;
 }
 
+static bool
+nego_token (char **pbuf, size_t *plen, const char *val)
+{
+	char *buf;
+	size_t len;
+	const char *p;
+	bool rcb;
+
+	if (!(p = auth_type_check(val, "Negotiate")))
+		return false;
+
+	len = strlen(p);
+	if (len == 0) {
+		return false;
+	}
+
+	/* actually should be 3/4, but whatever */
+	buf = malloc(len);
+	if (!buf)
+		return false;	// or maybe just abort()?
+
+	rcb = base64_decode(p, len, buf, &len);
+	if (!rcb) {
+		// free(buf);
+		return false;
+	}
+
+	*pbuf = buf;
+	*plen = len;
+	return true;
+}
+
+static int
+authenticate (struct MHD_Connection *conn, my_state *ms)
+{
+	const char		*val;
+	char			*buf;
+	char			*p;
+	size_t			 len;
+	unsigned int gss_maj, gss_min, dummy_min;
+	gss_ctx_id_t		 gss_ctx = GSS_C_NO_CONTEXT;
+	gss_buffer_desc		 gss_itok;
+	gss_buffer_desc		 gss_otok = GSS_C_EMPTY_BUFFER;
+	gss_name_t		 gss_cli_name = GSS_C_NO_NAME;
+	gss_buffer_desc		 gss_nbuf;
+	gss_cred_id_t		 gss_dcred = GSS_C_NO_CREDENTIAL;
+	struct MHD_Response	*resp;
+	unsigned int		 gss_flags;
+	bool			 rcb;
+	int			 ret;
+
+	val = MHD_lookup_connection_value(conn,MHD_HEADER_KIND,"Authorization");
+	if (!val) {
+		resp = MHD_create_response_from_data(0,NULL,MHD_NO,MHD_NO);
+		if (!resp) {
+			return -1;
+		}
+		MHD_add_response_header(resp,"WWW-Authenticate","Negotiate");
+		MHD_queue_response(conn,MHD_HTTP_UNAUTHORIZED,resp);
+		MHD_destroy_response(resp);
+		ms->rc = MHD_HTTP_UNAUTHORIZED;
+		return 1;
+	}
+
+	rcb = nego_token(&buf, &gss_itok.length, val);
+	if (!rcb) {
+		do_resp(conn,ms,MHD_HTTP_BAD_REQUEST,NULL,"400\r\n");
+		return 1;
+	}
+	gss_itok.value = buf;	/* gcc */
+
+	gss_maj = gss_accept_sec_context(&gss_min,
+		&gss_ctx,
+		GSS_C_NO_CREDENTIAL,
+		&gss_itok,
+		GSS_C_NO_CHANNEL_BINDINGS,
+		&gss_cli_name,
+		NULL,
+		&gss_otok,
+		&gss_flags,
+		NULL,
+		&gss_dcred);
+	if (gss_otok.length) {
+		len = ((gss_otok.length+2)/3) * 4;
+		len += sizeof("Negotiate ")-1;
+		buf = malloc(len + 1);
+		if (!buf) {
+			do_resp(conn,ms,MHD_HTTP_INTERNAL_SERVER_ERROR,
+				NULL,NULL);
+			ret = 1;
+			goto out;
+		}
+		strcpy(buf, "Negotiate ");
+		base64_encode(gss_otok.value, gss_otok.length,
+			      buf + (sizeof("Negotiate ")-1),
+			      len - (sizeof("Negotiate ")-1));
+		buf[len] = 0;	/* base64_encode zero-pads if smaller */
+
+		/* GC frees the previous one, right? */
+		ms->auth_hdr = buf;
+	}
+	if (GSS_ERROR(gss_maj)) {
+		if (verbose) {
+			log_gss_error("gss_accept_sec_context", gss_min);
+		}
+		do_resp(conn,ms,MHD_HTTP_FORBIDDEN,NULL,"403\r\n");
+		ret = 1;
+		goto out;
+	}
+
+	if (gss_maj & GSS_S_CONTINUE_NEEDED) {
+		/* Continuations are impossible in Kerberos, right? */
+		error (EXIT_FAILURE, 0, _("gss_accept_sec_context continuing"));
+	}
+
+	gss_maj = gss_display_name(&gss_min, gss_cli_name, &gss_nbuf, NULL);
+	if (GSS_ERROR(gss_maj)) {
+		/*
+		 * We authenticated successfuly, but if we cannot get the
+		 * client name, it is no use: we will not be able to match.
+		 * Have to refuse this. Return 500 for now, this is a problem.
+		 */
+		log_gss_error("gss_display_name error", gss_min);
+		ret = -1;
+		goto out;
+	}
+	p = memchr((char*)gss_nbuf.value,'@',gss_nbuf.length);
+	if (p) {
+		buf = strndup((char*)gss_nbuf.value,p-(char*)gss_nbuf.value);
+		if (!buf) {
+			ret = -1;
+			goto out;
+		}
+	}
+	else {
+		buf = strndup((char*)gss_nbuf.value,gss_nbuf.length);
+		if (!buf) {
+			ret = -1;
+			goto out;
+		}
+	}
+	ms->auth_user = buf;
+	DPRINTF("auth user %s\n", buf);
+
+	ret = 0;
+ out:
+	if (gss_nbuf.length != 0) {
+		gss_release_buffer(&dummy_min, &gss_nbuf);
+	}
+	if (gss_otok.length != 0) {
+		gss_release_buffer(&dummy_min, &gss_otok);
+	}
+	if (gss_ctx != GSS_C_NO_CONTEXT) {
+		gss_delete_sec_context(&dummy_min, &gss_ctx, GSS_C_NO_BUFFER);
+	}
+	if (gss_dcred != GSS_C_NO_CREDENTIAL) {
+		gss_release_cred(&dummy_min, &gss_dcred);
+	}
+	if (gss_cli_name != GSS_C_NO_NAME) {
+		gss_release_name(&dummy_min, &gss_cli_name);
+	}
+	return ret;
+}
+
 /* Extract the value from key="val". */
 static char *
 oauth_param_val(const char *arg)
@@ -2692,6 +2937,13 @@ access_handler_0 (void *cctx, struct MHD_Connection *conn, const char *url,
 		if (ms->state == MS_NEW)
 			log_http_request(conn, method, url);
 
+		if (kerberos) {
+			rc = authenticate(conn, ms);
+			if (rc < 0)
+				goto out_no;
+			if (rc > 0)
+				return MHD_YES;
+		}
 		if (oauth) {
 			rc = do_oauth(conn,ms,url,method);
 			if (rc < 0)
@@ -2740,6 +2992,13 @@ access_handler_0 (void *cctx, struct MHD_Connection *conn, const char *url,
 		pthread_mutex_lock(&my_lock);
 		LIST_INSERT_HEAD(&my_states,ms,gc_link);
 		pthread_mutex_unlock(&my_lock);
+		if (kerberos) {
+			rc = authenticate(conn,ms);
+			if (rc < 0)
+				goto out_no;
+			if (rc > 0)
+				return MHD_YES;
+		}
 		if (oauth) {
 			rc = do_oauth(conn,ms,url,method);
 			if (rc < 0)
@@ -2825,9 +3084,12 @@ static const struct option my_options[] = {
 	{ "config",  required_argument, NULL, 'c' },
 	{ "db",      required_argument, NULL, 'd' },
 	{ "logfile", required_argument, NULL, 'l' },
+	{ "kerberos", no_argument,      NULL, 'k' },
 	{ "master",  required_argument, NULL, 'm' },
+	{ "krbsvcnm", required_argument, NULL, 'n' },
 	{ "oauth",   no_argument,       NULL, 'o' },
 	{ "port",    required_argument, NULL, 'p' },
+	{ "krbkeytab", required_argument, NULL, 't' },
 	{ "usercred", required_argument, NULL, 'U' },
 	{ "userlist", required_argument, NULL, 'u' },
 	{ "verbose", no_argument,       NULL, 'v' },
@@ -2856,9 +3118,12 @@ A configuration file must be specified.\n\
   -c, --config=FILE       config file [required]\n\
   -d, --db=HOST_PORT      database server as ip[:port]\n\
   -l, --logfile=FILE      logfile (default stdout/stderr)\n\
+  -k, --kerberos          enable Kerberos\n\
   -m, --master=HOST_PORT  master (upstream) server as ip[:port]\n\
+  -n, --krbsvcnm=NAME     Kerberos service name (default IWHD)\n\
   -o, --oauth             enable OAuth\n\
   -p, --port=PORT         alternate listen port (default 9090)\n\
+  -t, --krbkeytab=FILE    location of Kerberos keytab\n\
   -u, --userlist=FILE     list of user names, secrets (0600 permissions)\n\
   -v, --verbose           verbose/debug output\n\
 \n\
@@ -2879,6 +3144,79 @@ Report %s bugs to %s.\n\
   exit (status);
 }
 
+static void gss_init(void)
+{
+	char *gss_sname;
+	gss_buffer_desc gss_nbuf; /* struct */
+	unsigned int gss_maj, gss_min, dummy_min;
+	gss_name_t gss_iname; /* struct* */
+	int rc;
+
+	gss_maj = krb5_gss_register_acceptor_identity(krb_keytab);
+	if (gss_maj != 0)
+		error (EXIT_FAILURE, 0, _("cannot set keytab %s"), krb_keytab);
+
+	if (gethostname(my_hostname, HOST_NAME_MAX) != 0)
+		error (EXIT_FAILURE, errno, _("gethostname"));
+	my_hostname[HOST_NAME_MAX] = 0;
+
+	if (strchr(krb_svc_name, '@') != NULL ||
+	    strchr(krb_svc_name, '/') != NULL) {
+		gss_sname = strdup(krb_svc_name);
+		if (!gss_sname)
+			error (EXIT_FAILURE, 0, _("out of memory"));
+	}
+	else {
+		rc = asprintf(&gss_sname, "%s@%s", krb_svc_name, my_hostname);
+		if (rc < 0)
+			error (EXIT_FAILURE, 0, _("out of memory"));
+	}
+
+	gss_nbuf.length = strlen(gss_sname)+1;
+	gss_nbuf.value = gss_sname;
+
+	gss_maj = gss_import_name(&gss_min, &gss_nbuf,
+				  GSS_C_NT_HOSTBASED_SERVICE, &gss_iname);
+	if (GSS_ERROR(gss_maj)) {
+		log_gss_error("gss_import_name error", gss_min);
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 * Why re-import only to print? We just exported the gss_sname, right?
+	 * Well, who knows, maybe something went pear-shaped.
+	 * Not sure what to do with the type OID though, so just NULL it.
+	 */
+	if (verbose) {
+		gss_maj = gss_display_name(&gss_min, gss_iname, &gss_nbuf,NULL);
+		if (GSS_ERROR(gss_maj)) {
+			log_gss_error("gss_display_name error", gss_min);
+			exit(EXIT_FAILURE);
+		}
+		printf("kerberos service: %s\n", (char*)gss_nbuf.value);
+		gss_release_buffer(&dummy_min, &gss_nbuf);
+	}
+
+#if 0 /* RFC says use default */
+	/*
+	 * The GSS_C_INDEFINITE does not yield a credential with an
+	 * indefinite lifetime, merely "the maximum permitted lifetime".
+	 * So, this function has to be called from the main loop somewhere. XXX
+	 * Then, we need to call gss_release_cred(), too.
+	 */
+	gss_maj = gss_acquire_cred(&gss_min, gss_iname, GSS_C_INDEFINITE,
+				   GSS_C_NO_OID_SET, GSS_C_ACCEPT, &gss_creds,
+				   NULL, NULL);
+	if (GSS_ERROR(gss_maj)) {
+		log_gss_error("gss_acquire_cred error", gss_min);
+		exit(EXIT_FAILURE);
+	}
+#endif
+
+	gss_release_name(&dummy_min, &gss_iname);
+	// free(gss_sname); -- don't crash GC
+}
+
 static void oauth_init(void)
 {
 	if (gethostname(my_hostname, HOST_NAME_MAX) != 0)
@@ -2958,7 +3296,7 @@ main (int argc, char **argv)
 	db_host = xstrdup ("localhost");
 
 	for (;;)
-	switch (getopt_long(argc,argv,"ac:d:l:m:op:u:U:v",my_options,NULL))
+	switch (getopt_long(argc,argv,"ac:d:kl:m:n:op:t:u:U:v",my_options,NULL))
 	{
 	case 'a':
 		autostart = true;
@@ -2975,11 +3313,17 @@ main (int argc, char **argv)
 		assert (optarg);
 		log_name = optarg;
 		break;
+	case 'k':
+		kerberos = true;
+		break;
 	case 'm':
 		assert (optarg);
 		free (master_host); master_host = NULL;
 		extract_host_port (optarg, &master_host, &master_port);
 		break;
+	case 'n':
+		krb_svc_name = optarg;
+		break;
 	case 'o':
 		oauth = true;
 		break;
@@ -2987,6 +3331,9 @@ main (int argc, char **argv)
 		assert (optarg);
 		my_port = get_port (optarg);
 		break;
+	case 't':
+		krb_keytab = optarg;
+		break;
 	case 'U':	/* Uppercase because very temporary until -u appears. */
 		usercred = optarg;
 		break;
@@ -3063,6 +3410,8 @@ args_done:
 		usage (EXIT_FAILURE);
 	}
 
+	if (kerberos)
+		gss_init();
 	if (oauth)
 		oauth_init();
 
diff --git a/state_defs.h b/state_defs.h
index 6b09c55..7e09b73 100644
--- a/state_defs.h
+++ b/state_defs.h
@@ -50,6 +50,8 @@ typedef struct _my_state {
 	MHD_AccessHandlerCallback	 handler;
 	ms_state			 state;
 	bool				 authorized;
+	char				*auth_hdr;
+	char				*auth_user;
 	/* for GC library */
 	LIST_ENTRY(_my_state)		 gc_link;
 	/* for proxy ops */
diff --git a/t/.gitignore b/t/.gitignore
index e5baa0c..75cf95c 100644
--- a/t/.gitignore
+++ b/t/.gitignore
@@ -1,2 +1,3 @@
 *.o
+gssapi_test.so
 poke
diff --git a/t/Makefile.am b/t/Makefile.am
index 77094b9..3515eb0 100644
--- a/t/Makefile.am
+++ b/t/Makefile.am
@@ -26,12 +26,14 @@ TESTS =						\
   timeout					\
   auto						\
   registration					\
-  oauth
+  oauth						\
+  kerberos
 
 lock_dir = $(abs_builddir)/lock-dir
 clean-local:
 	$(AM_V_GEN)rm -rf "$(lock_dir)"
 	$(AM_V_GEN)rm -f poke poke.o hstor.o
+	$(AM_V_GEN)rm -f gssapi_test.so
 
 .PHONY: prereq
 prereq:
@@ -47,10 +49,16 @@ LDADD = ../lib/libiwhd.a
 
 oauth:   poke
 
+gssapi_test.so:  $(srcdir)/gssapi_test.c
+	gcc -Wall -I$(top_builddir) -O2 -fpic -shared -o gssapi_test.so $<
+
+kerberos:  gssapi_test.so
+
 EXTRA_DIST =					\
   $(TESTS)					\
   init.cfg					\
-  init.sh
+  init.sh					\
+  gssapi_test.c
 
 TESTS_ENVIRONMENT =				\
   tmp__=$$TMPDIR; test -d "$$tmp__" || tmp__=.; \
diff --git a/t/gssapi_test.c b/t/gssapi_test.c
new file mode 100644
index 0000000..7d5ca11
--- /dev/null
+++ b/t/gssapi_test.c
@@ -0,0 +1,261 @@
+/* Copyright (C) 2011 Red Hat, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include <config.h>	/* not needed but for "make syntax-check" */
+
+#include <sys/types.h>
+#include <pwd.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <gssapi/gssapi.h>
+/* #include <gssapi/gssapi_generic.h> */
+#include <gssapi/gssapi_krb5.h>
+
+struct gss_name_struct {
+	const char *name;
+};
+
+struct gss_ctx_id_struct {
+	unsigned char tag[4];
+};
+
+static const unsigned char tag0[4] = { 'A', 'b', '5', '3' };
+
+static struct gss_name_struct *new_name(char *p, size_t len)
+{
+	struct gss_name_struct *ret;
+	char *nm;
+
+	ret = malloc(sizeof(struct gss_name_struct));
+	if (!ret) {
+		return NULL;
+	}
+
+	nm = strndup(p, len);
+	if (!nm) {
+		free(ret);
+		return NULL;
+	}
+
+	ret->name = nm;
+	return ret;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_import_name(
+    OM_uint32 *minor_status,
+    gss_buffer_t input_name_buffer,
+    gss_OID input_name_type,
+    gss_name_t *output_name)
+{
+	struct gss_name_struct *out;
+
+	out = new_name(input_name_buffer->value, input_name_buffer->length);
+	if (!out) {
+		*minor_status = 1;
+		return GSS_S_FAILURE;
+	}
+	*output_name = out;
+	return 0;
+	// return GSS_S_CALL_BAD_STRUCTURE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_display_status(
+    OM_uint32 *minor_status,
+    OM_uint32 status_value,
+    int status_type,
+    gss_OID mech_type,
+    OM_uint32 *message_context,
+    gss_buffer_t status_string)
+{
+	char *out;
+
+	out = malloc(30);
+	if (!out) {
+		/* This is so going to crash in the app later... */
+		status_string->value = NULL;
+		*message_context = 0;
+		*minor_status = 1;
+		return GSS_S_FAILURE;
+	}
+	snprintf(out, 30, "maj=0x%x", status_value);
+	status_string->value = out;
+	status_string->length = strlen(out);
+	*message_context = 0;
+	*minor_status = 0;
+	return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_display_name(
+    OM_uint32 *minor_status,
+    gss_name_t input_name,
+    gss_buffer_t output_name_buffer,
+    gss_OID *output_name_type)
+{
+	char *out;
+
+	out = strdup(input_name->name);
+	if (!out) {
+		output_name_buffer->value = NULL;
+		output_name_buffer->length = 0;
+		*minor_status = 1;
+		return GSS_S_FAILURE;
+	}
+
+	output_name_buffer->value = out;
+	output_name_buffer->length = strlen(out);
+	return 0;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_init_sec_context(
+    OM_uint32 *minor_status,
+    gss_cred_id_t claimant_cred_handle,
+    gss_ctx_id_t *context_handle,	/* return this (later) */
+    gss_name_t target_name,
+    gss_OID mech_type,
+    OM_uint32 req_flags,
+    OM_uint32 time_req,
+    gss_channel_bindings_t input_chan_bindings,
+    gss_buffer_t input_token,
+    gss_OID *actual_mech_type,
+    gss_buffer_t output_token,		/* return this as necessary */
+    OM_uint32 *ret_flags,
+    OM_uint32 *time_rec)
+{
+	unsigned char *obuf;
+	struct gss_ctx_id_struct *ctx;
+	char pwbuf[200];
+	struct passwd pwb, *pw;
+	size_t len;
+	int rc;
+
+	rc = getpwuid_r(getuid(), &pwb, pwbuf, sizeof(pwbuf), &pw);
+	if (rc != 0 || pw == NULL) {
+		*minor_status = 1;
+		return GSS_S_FAILURE;
+	}
+
+	/* Kerberos has no server challenge (input_token), so ignore it. */
+
+	ctx = *context_handle;
+	if (!ctx) {
+		ctx = malloc(sizeof(struct gss_ctx_id_struct));
+		if (!ctx) {
+			*minor_status = 1;
+			return GSS_S_FAILURE;
+		}
+		memcpy(ctx->tag, tag0, 4);
+		*context_handle = ctx;
+	}
+
+	len = strlen(pw->pw_name);
+	obuf = malloc(4 + len);
+	if (!obuf) {
+		free(ctx);
+		*minor_status = 2;
+		return GSS_S_FAILURE;
+	}
+	memcpy(obuf, tag0, 4);
+	memcpy(obuf+4, pw->pw_name, len);
+	output_token->value = obuf;
+	output_token->length = 4 + len;
+
+	return 0;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_accept_sec_context(
+    OM_uint32 *minor_status,
+    gss_ctx_id_t *context_handle,
+    gss_cred_id_t acceptor_cred_handle,
+    gss_buffer_t input_token_buffer,
+    gss_channel_bindings_t input_chan_bindings,
+    gss_name_t *src_name,
+    gss_OID *mech_type,
+    gss_buffer_t output_token,
+    OM_uint32 *ret_flags,
+    OM_uint32 *time_rec,
+    gss_cred_id_t *delegated_cred_handle)
+{
+	struct gss_name_struct *out;
+
+	if (input_token_buffer->length <= 4) {
+		*minor_status = 0;
+		return GSS_S_DEFECTIVE_TOKEN;	/* 0x90000 */
+	}
+	if (memcmp(input_token_buffer->value, tag0, 4) != 0) {
+		*minor_status = 1;
+		return GSS_S_DEFECTIVE_TOKEN;	/* 0x90000 */
+	}
+
+	out = new_name(input_token_buffer->value + 4,
+		       input_token_buffer->length - 4);
+	if (!out) {
+		*minor_status = 1;
+		return GSS_S_FAILURE;
+	}
+
+	*src_name = out;
+	return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_delete_sec_context(
+    OM_uint32 *minor_status,
+    gss_ctx_id_t *context_handle,
+    gss_buffer_t output_token)
+{
+	/* We leak on purpose: afraid to crash with GC. */
+	return 0;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_release_buffer(
+    OM_uint32 *minstat,	/* minor_status */
+    gss_buffer_t buf)	/* buffer */
+{
+	return 0;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_release_name(
+    OM_uint32 *minstat,	/* minor_status */
+    gss_name_t *name)	/* input_name */
+{
+	return 0;
+}
+
+#if 0
+OM_uint32 KRB5_CALLCONV
+gss_acquire_cred(
+    OM_uint32 *,        /* minor_status */
+    gss_name_t,         /* desired_name */
+    OM_uint32,          /* time_req */
+    gss_OID_set,        /* desired_mechs */
+    gss_cred_usage_t,   /* cred_usage */
+    gss_cred_id_t *,    /* output_cred_handle */
+    gss_OID_set *,      /* actual_mechs */
+    OM_uint32 *);       /* time_rec */
+#endif
+
+OM_uint32 KRB5_CALLCONV krb5_gss_register_acceptor_identity(const char *fname)
+{
+	/* Just ignore the keytab for the moment. */
+	return 0;
+}
diff --git a/t/kerberos b/t/kerberos
new file mode 100755
index 0000000..b42158d
--- /dev/null
+++ b/t/kerberos
@@ -0,0 +1,52 @@
+#!/bin/sh
+# Test authentication functionality.
+
+. "${srcdir=.}/init.sh"; path_prepend_ ..
+
+mkdir FS mongod iwhd || framework_failure_ mkdir failed
+
+# This actually mostly helps against weirdness with global variables.
+# The makefile ensures that we only ever get here with the .so built.
+test -f ${abs_top_builddir}/t/gssapi_test.so \
+  || fail_ "no $abs_top_builddir/t/gssapi_test.so"
+#test -f ./t/gssapi_test.so || fail_ "no ./t/gssapi_test.so"
+
+m_port=$(get_port $mongo_base_port $lock_dir/m-) \
+  || fail_ "failed to get mongodb port"
+
+mongod --port $m_port --pidfilepath mongod/pid --dbpath mongod > mongod.log 2>&1 &
+mongo_pid=$!
+cleanup_() { kill -9 $mongo_pid; }
+
+# Wait for up to 5 seconds for mongod to begin listening.
+wait_for .1 50 'mongo localhost:$m_port < /dev/null' \
+  || framework_failure_ mongod failed to start
+
+port=$(get_port 9095 $lock_dir/i-) || fail_ "failed to get iwhd port"
+
+ulimit -c unlimited
+
+printf '[{"path": "FS", "type": "fs", "name": "primary"}]\n' \
+  > iwhd.cfg || fail=1
+
+LD_PRELOAD=${abs_top_builddir}/t/gssapi_test.so iwhd -v -k \
+  -p $port -c iwhd.cfg -d localhost:$m_port &
+iwhd_pid=$!
+cleanup_() { kill -9 $mongo_pid; kill $iwhd_pid; }
+
+## Wait for up to 5 seconds for iwhd to begin listening on $port.
+#wait_for .1 50 "curl -s http://localhost:$port" \
+#  || { echo iwhd failed to listen; Exit 1; }
+sleep 5
+
+fail=0
+
+# LD_PRELOAD=/q/zaitcev/hail/iwhd-tip/t/gssapi_test.so curl -0 -v --negotiate -u : http://localhost:9090/
+# We use -0 because of a mysterious problem with curl attempting to reuse
+# the server connection and failing in a spectacular fashion. Perhaps iwhd
+# does not implement HTTP 1.1 pipelining properly.
+LD_PRELOAD=${abs_top_builddir}/t/gssapi_test.so curl -0 -v \
+  --negotiate -u : http://localhost:$port/ \
+  || fail=1
+
+Exit $fail
