Repository: jcmturner/gokrb5
Pull request #73 (2017-12-13 20:46:39)

 Support for cross-realm auth with direct trust
I'm not sure if I get all things correctly, but it works for me and resolves my issue #51.

So the main problem I'm addressing here:
Requesting a ticket for a service in different Realm, other than logged user, raises KDC_ERR_C_PRINCIPAL_UNKNOWN  error if default Realm (from config) is not matching user's Realm.

By comparing the behavior of gokrb5 and MIT's Kerberos in Wireshark, I've realized that there should be an extra step of obtaining TGT from a remote Realm. So I added an additional call `getSessionFromRemoteRealm()` to existing `GetSessionFromRealm()` method that does the job.

Note, my KDC environment is based on AD with multiple domains in a trusted forest.

Additionally I've added few minor related changes:
* fix a bug in `ResolveRealm()` that never matches domain entries from configuration
* use the default Realm from configuration only when Client's credentials missing the Realm

I'm not sure if my changes compliant with RFCs, thus @jcmturner please feel free to amend any part of pull request.  Maybe it would be also a good idea to have a docker image with 2 KDCs trusting each other for integration tests.


Patch:
diff --git a/client/TGSExchange.go b/client/TGSExchange.go
index 905d021..21d2c2b 100644
--- a/client/TGSExchange.go
+++ b/client/TGSExchange.go
@@ -1,12 +1,13 @@
 package client
 
 import (
+	"strings"
+	"time"
+
 	"gopkg.in/jcmturner/gokrb5.v2/iana/nametype"
 	"gopkg.in/jcmturner/gokrb5.v2/krberror"
 	"gopkg.in/jcmturner/gokrb5.v2/messages"
 	"gopkg.in/jcmturner/gokrb5.v2/types"
-	"strings"
-	"time"
 )
 
 // TGSExchange performs a TGS exchange to retrieve a ticket to the specified SPN.
@@ -38,7 +39,7 @@ func (cl *Client) TGSExchange(spn types.PrincipalName, kdcRealm string, tkt mess
 	if err != nil {
 		return tgsReq, tgsRep, krberror.Errorf(err, krberror.EncodingError, "TGS Exchange Error: failed to process the TGS_REP")
 	}
-	if tgsRep.Ticket.SName.NameType == nametype.KRB_NT_SRV_INST {
+	if tgsRep.Ticket.SName.NameType == nametype.KRB_NT_SRV_INST && !tgsRep.Ticket.SName.Equal(spn) {
 		if referral > 5 {
 			return tgsReq, tgsRep, krberror.Errorf(err, krberror.KRBMsgError, "maximum number of referrals exceeded")
 		}
@@ -84,7 +85,7 @@ func (cl *Client) GetServiceTicket(spn string) (messages.Ticket, types.Encryptio
 			return tkt, skey, err
 		}
 	}
-	_, tgsRep, err := cl.TGSExchange(princ, sess.TGT.Realm, sess.TGT, sess.SessionKey, false, 0)
+	_, tgsRep, err := cl.TGSExchange(princ, sess.Realm, sess.TGT, sess.SessionKey, false, 0)
 	if err != nil {
 		return tkt, skey, err
 	}
diff --git a/client/client.go b/client/client.go
index bd40cb7..c07325f 100644
--- a/client/client.go
+++ b/client/client.go
@@ -4,6 +4,7 @@ package client
 import (
 	"errors"
 	"fmt"
+
 	"gopkg.in/jcmturner/gokrb5.v2/config"
 	"gopkg.in/jcmturner/gokrb5.v2/credentials"
 	"gopkg.in/jcmturner/gokrb5.v2/crypto"
@@ -33,6 +34,7 @@ type Config struct {
 }
 
 // NewClientWithPassword creates a new client from a password credential.
+// Set the realm to empty string to use the default realm from config.
 func NewClientWithPassword(username, realm, password string) Client {
 	creds := credentials.NewCredentials(username, realm)
 	return Client{
@@ -192,5 +194,8 @@ func (cl *Client) IsConfigured() (bool, error) {
 
 // Login the client with the KDC via an AS exchange.
 func (cl *Client) Login() error {
-	return cl.ASExchange(cl.Config.LibDefaults.DefaultRealm, 0)
+	if cl.Credentials.Realm == "" {
+		cl.Credentials.Realm = cl.Config.LibDefaults.DefaultRealm
+	}
+	return cl.ASExchange(cl.Credentials.Realm, 0)
 }
diff --git a/client/session.go b/client/session.go
index 990e50a..b8adee1 100644
--- a/client/session.go
+++ b/client/session.go
@@ -2,12 +2,13 @@ package client
 
 import (
 	"fmt"
+	"sync"
+	"time"
+
 	"gopkg.in/jcmturner/gokrb5.v2/iana/nametype"
 	"gopkg.in/jcmturner/gokrb5.v2/krberror"
 	"gopkg.in/jcmturner/gokrb5.v2/messages"
 	"gopkg.in/jcmturner/gokrb5.v2/types"
-	"sync"
-	"time"
 )
 
 // Sessions keyed on the realm name
@@ -96,20 +97,40 @@ func (cl *Client) updateSession(s *session) error {
 	return nil
 }
 
-// GetSessionFromRealm returns the session for the realm provided.
-func (cl *Client) GetSessionFromRealm(realm string) (sess *session, err error) {
-	var ok bool
+func (cl *Client) getSessionFromRemoteRealm(realm string) (*session, error) {
+	cl.sessions.mux.RLock()
+	sess, ok := cl.sessions.Entries[cl.Credentials.Realm]
+	cl.sessions.mux.RUnlock()
+	if !ok {
+		return nil, fmt.Errorf("client does not have a session for realm %s, login first", cl.Credentials.Realm)
+	}
+
+	spn := types.PrincipalName{
+		NameType:   nametype.KRB_NT_SRV_INST,
+		NameString: []string{"krbtgt", realm},
+	}
+
+	_, tgsRep, err := cl.TGSExchange(spn, cl.Credentials.Realm, sess.TGT, sess.SessionKey, false, 0)
+	if err != nil {
+		return nil, err
+	}
+	cl.AddSession(tgsRep.Ticket, tgsRep.DecryptedEncPart)
+
 	cl.sessions.mux.RLock()
 	defer cl.sessions.mux.RUnlock()
-	sess, ok = cl.sessions.Entries[realm]
+	return cl.sessions.Entries[realm], nil
+}
+
+// GetSessionFromRealm returns the session for the realm provided.
+func (cl *Client) GetSessionFromRealm(realm string) (*session, error) {
+	cl.sessions.mux.RLock()
+	sess, ok := cl.sessions.Entries[realm]
+	cl.sessions.mux.RUnlock()
 	if !ok {
-		sess, ok = cl.sessions.Entries[cl.Config.LibDefaults.DefaultRealm]
-		if !ok {
-			err = fmt.Errorf("client does not have a session for realm %s or for the default realm %s, login first", realm, cl.Config.LibDefaults.DefaultRealm)
-			return
-		}
+		// Try to request TGT from trusted remote Realm
+		return cl.getSessionFromRemoteRealm(realm)
 	}
-	return
+	return sess, nil
 }
 
 // GetSessionFromPrincipalName returns the session for the realm of the principal provided.
diff --git a/config/krb5conf.go b/config/krb5conf.go
index 6f260f7..c733c83 100644
--- a/config/krb5conf.go
+++ b/config/krb5conf.go
@@ -6,8 +6,6 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
-	"github.com/jcmturner/asn1"
-	"gopkg.in/jcmturner/gokrb5.v2/iana/etypeID"
 	"io"
 	"os"
 	"os/user"
@@ -15,6 +13,9 @@ import (
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/jcmturner/asn1"
+	"gopkg.in/jcmturner/gokrb5.v2/iana/etypeID"
 )
 
 // Config represents the KRB5 configuration.
@@ -431,10 +432,17 @@ func (d *DomainRealm) deleteMapping(domain, realm string) {
 // The most specific mapping is returned.
 func (c *Config) ResolveRealm(domainName string) string {
 	domainName = strings.TrimSuffix(domainName, ".")
+
+	// Try to match the entire hostname first
+	if r, ok := c.DomainRealm[domainName]; ok {
+		return r
+	}
+
+	// Try to match all DNS domain parts
 	periods := strings.Count(domainName, ".") + 1
-	for i := 1; i <= periods; i++ {
+	for i := 2; i <= periods; i++ {
 		z := strings.SplitN(domainName, ".", i)
-		if r, ok := c.DomainRealm[z[len(z)-1]]; ok {
+		if r, ok := c.DomainRealm["."+z[len(z)-1]]; ok {
 			return r
 		}
 	}
diff --git a/messages/KDCReq.go b/messages/KDCReq.go
index 2cb4ad3..071706b 100644
--- a/messages/KDCReq.go
+++ b/messages/KDCReq.go
@@ -6,6 +6,10 @@ package messages
 import (
 	"crypto/rand"
 	"fmt"
+	"math"
+	"math/big"
+	"time"
+
 	"github.com/jcmturner/asn1"
 	"gopkg.in/jcmturner/gokrb5.v2/asn1tools"
 	"gopkg.in/jcmturner/gokrb5.v2/config"
@@ -19,9 +23,6 @@ import (
 	"gopkg.in/jcmturner/gokrb5.v2/iana/patype"
 	"gopkg.in/jcmturner/gokrb5.v2/krberror"
 	"gopkg.in/jcmturner/gokrb5.v2/types"
-	"math"
-	"math/big"
-	"time"
 )
 
 type marshalKDCReq struct {
@@ -169,7 +170,7 @@ func NewTGSReq(cname types.PrincipalName, kdcRealm string, c *config.Config, tkt
 		types.SetFlag(&a.ReqBody.KDCOptions, flags.Renew)
 		types.SetFlag(&a.ReqBody.KDCOptions, flags.Renewable)
 	}
-	auth, err := types.NewAuthenticator(c.LibDefaults.DefaultRealm, cname)
+	auth, err := types.NewAuthenticator(tkt.Realm, cname)
 	if err != nil {
 		return a, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
 	}


Test patch:
diff --git a/client/client_integration_test.go b/client/client_integration_test.go
index 634bab5..4ccb739 100644
--- a/client/client_integration_test.go
+++ b/client/client_integration_test.go
@@ -5,15 +5,16 @@ package client
 
 import (
 	"encoding/hex"
+	"net/http"
+	"os"
+	"testing"
+
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/jcmturner/gokrb5.v2/config"
 	"gopkg.in/jcmturner/gokrb5.v2/credentials"
 	"gopkg.in/jcmturner/gokrb5.v2/iana/etypeID"
 	"gopkg.in/jcmturner/gokrb5.v2/keytab"
 	"gopkg.in/jcmturner/gokrb5.v2/testdata"
-	"net/http"
-	"os"
-	"testing"
 )
 
 func TestClient_SuccessfulLogin_Keytab(t *testing.T) {
@@ -54,7 +55,7 @@ func TestClient_SuccessfulLogin_Password(t *testing.T) {
 	}
 	for _, test := range tests {
 		c.Realms[0].KDC = []string{addr + ":" + test}
-		cl := NewClientWithPassword("testuser1", "TESTGOKRB5", "passwordvalue")
+		cl := NewClientWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue")
 		cl.WithConfig(c)
 
 		err := cl.Login()
@@ -141,7 +142,7 @@ func TestClient_ASExchange_TGSExchange_EncTypes_Password(t *testing.T) {
 		c.LibDefaults.DefaultTktEnctypeIDs = []int{etypeID.ETypesByName[test]}
 		c.LibDefaults.DefaultTGSEnctypes = []string{test}
 		c.LibDefaults.DefaultTGSEnctypeIDs = []int{etypeID.ETypesByName[test]}
-		cl := NewClientWithPassword("testuser1", "TESTGOKRB5", "passwordvalue")
+		cl := NewClientWithPassword("testuser1", "TEST.GOKRB5", "passwordvalue")
 		cl.WithConfig(c)
 
 		err := cl.Login()
diff --git a/config/krb5conf_test.go b/config/krb5conf_test.go
index ea34454..e38a16e 100644
--- a/config/krb5conf_test.go
+++ b/config/krb5conf_test.go
@@ -1,11 +1,12 @@
 package config
 
 import (
-	"github.com/stretchr/testify/assert"
 	"io/ioutil"
 	"os"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/assert"
 )
 
 const (
@@ -56,6 +57,10 @@ const (
  .test.gokrb5 = TEST.GOKRB5
 
  test.gokrb5 = TEST.GOKRB5
+ 
+  .example.com = EXAMPLE.COM
+ hostname1.example.com = EXAMPLE.COM
+ hostname2.example.com = TEST.GOKRB5
 
 [appdefaults]
  pam = {
@@ -281,3 +286,27 @@ func TestParseDuration(t *testing.T) {
 	}
 
 }
+
+func TestResolveRealm(t *testing.T) {
+	c, err := NewConfigFromString(krb5Conf)
+	if err != nil {
+		t.Fatalf("Error loading config: %v", err)
+	}
+
+	tests := []struct {
+		domainName string
+		want       string
+	}{
+		{"unknown.com", "TEST.GOKRB5"},
+		{"hostname1.example.com", "EXAMPLE.COM"},
+		{"hostname2.example.com", "TEST.GOKRB5"},
+		{"one.two.three.example.com", "EXAMPLE.COM"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.domainName, func(t *testing.T) {
+			if got := c.ResolveRealm(tt.domainName); got != tt.want {
+				t.Errorf("Config.ResolveRealm() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/testenv/docker/krb5kdc-res/Dockerfile b/testenv/docker/krb5kdc-res/Dockerfile
new file mode 100644
index 0000000..21f2fd8
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/Dockerfile
@@ -0,0 +1,19 @@
+FROM centos:latest
+MAINTAINER Jonathan Turner <jt@jtnet.co.uk>
+
+EXPOSE 88
+ENTRYPOINT ["/usr/sbin/krb5kdc", "-n"]
+
+RUN yum install -y \
+  krb5-server \
+  tcpdump krb5-workstation vim \
+ && yum update -y && yum clean all
+
+ADD krb5.conf /etc/krb5.conf
+ADD kdc.conf /var/kerberos/krb5kdc/kdc.conf
+ADD kadm5.acl /var/kerberos/krb5kdc/kadm5.acl
+ADD krb5kdc-init.sh /opt/krb5/bin/krb5kdc-init.sh
+RUN mkdir -p /opt/krb5/log && \
+  mkdir -p /var/log/kerberos && \
+  /bin/bash /opt/krb5/bin/krb5kdc-init.sh && \
+  ln -sf /dev/stdout /var/log/krb5kdc.log
diff --git a/testenv/docker/krb5kdc-res/README.md b/testenv/docker/krb5kdc-res/README.md
new file mode 100644
index 0000000..7c44ae6
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/README.md
@@ -0,0 +1,16 @@
+# KDC Intergation Test Instance for RESDOM.GOKRB5
+
+DO NOT USE THIS CONTAINER FOR ANY PRODUCTION USE!!!
+
+To run:
+```bash
+docker run -v /etc/localtime:/etc/localtime:ro -p 188:88 -p 188:88/udp --rm --name gokrb5-res jcmturner/gokrb5:kdc-resdom &
+```
+
+To build:
+```bash
+docker build -t jcmturner/gokrb5:kdc-resdom --force-rm=true --rm=true .
+docker push jcmturner/gokrb5:kdc-resdom
+```
+
+
diff --git a/testenv/docker/krb5kdc-res/kadm5.acl b/testenv/docker/krb5kdc-res/kadm5.acl
new file mode 100644
index 0000000..149cded
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/kadm5.acl
@@ -0,0 +1,4 @@
+testuser1@RESDOM.GOKRB5	*
+*/admin@RESDOM.GOKRB5	*
+*/*@RESDOM.GOKRB5		i
+*@RESDOM.GOKRB5		i
diff --git a/testenv/docker/krb5kdc-res/kdc.conf b/testenv/docker/krb5kdc-res/kdc.conf
new file mode 100644
index 0000000..d4c4c21
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/kdc.conf
@@ -0,0 +1,14 @@
+[kdcdefaults]
+ kdc_ports = 88
+ kdc_tcp_ports = 88
+
+[realms]
+ RESDOM.GOKRB5 = {
+  master_key_type = aes256-cts-hmac-sha1-96
+  max_life = 12h 0m 0s
+  max_renewable_life = 7d 0h 0m 0s
+  acl_file = /var/kerberos/krb5kdc/kadm5.acl
+  dict_file = /usr/share/dict/words
+  admin_keytab = /opt/krb5/data/kadm5.keytab
+  supported_enctypes = aes128-cts-hmac-sha1-96:normal aes256-cts-hmac-sha1-96:normal aes128-cts-hmac-sha256-128:normal aes256-cts-hmac-sha384-192:normal des3-cbc-sha1-kd:normal rc4-hmac:normal
+ }
diff --git a/testenv/docker/krb5kdc-res/krb5.conf b/testenv/docker/krb5kdc-res/krb5.conf
new file mode 100644
index 0000000..1113cb2
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/krb5.conf
@@ -0,0 +1,39 @@
+[logging]
+ default = FILE:/var/log/krb5libs.log
+ kdc = FILE:/var/log/krb5kdc.log
+ admin_server = FILE:/var/log/kadmind.log
+
+
+[libdefaults]
+  default_realm = RESDOM.GOKRB5
+  dns_lookup_realm = false
+  dns_lookup_kdc = false
+  ticket_lifetime = 24h
+  forwardable = yes
+
+[realms]
+ TEST.GOKRB5 = {
+  kdc = 127.0.0.1:88
+  admin_server = 127.0.0.1:749
+  default_domain = test.gokrb5
+ }
+  RESDOM.GOKRB5 = {
+   kdc = 127.0.0.1:188
+   admin_server = 127.0.0.1:749
+   default_domain = resdom.gokrb5
+  }
+
+[domain_realm]
+ .test.gokrb5 = TEST.GOKRB5
+ test.gokrb5 = TEST.GOKRB5
+ .resdom.gokrb5 = RESDOM.GOKRB5
+ resdom.gokrb5 = RESDOM.GOKRB5
+
+[appdefaults]
+ pam = {
+   debug = false
+   ticket_lifetime = 36000
+   renew_lifetime = 36000
+   forwardable = true
+   krb4_convert = false
+ }
diff --git a/testenv/docker/krb5kdc-res/krb5kdc-init.sh b/testenv/docker/krb5kdc-res/krb5kdc-init.sh
new file mode 100644
index 0000000..1a225c4
--- /dev/null
+++ b/testenv/docker/krb5kdc-res/krb5kdc-init.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+REALM=RESDOM.GOKRB5
+DOMAIN=resdom.gokrb5
+SERVER_HOST=kdc.resdom.gokrb5
+ADMIN_USERNAME=adminuser
+HOST_PRINCIPALS="kdc.resdom.gokrb5 host.resdom.gokrb5"
+SPNs="HTTP/host.resdom.gokrb5"
+
+create_entropy() {
+   while true
+   do
+     sleep $(( ( RANDOM % 10 )  + 1 ))
+     echo "Generating Entropy... $RANDOM"
+   done
+}
+
+create_entropy &
+ENTROPY_PID=$!
+
+
+  echo "Kerberos initialisation required. Creating database for ${REALM} ..."
+  echo "This can take a long time if there is little entropy. A process has been started to create some."
+  MASTER_PASSWORD=$(echo $RANDOM$RANDOM$RANDOM | md5sum | awk '{print $1}')
+  /usr/sbin/kdb5_util create -r ${REALM} -s -P ${MASTER_PASSWORD}
+  kill -9 ${ENTROPY_PID}
+  echo "Kerberos database created."
+  /usr/sbin/kadmin.local -q "add_principal -randkey ${ADMIN_USERNAME}/admin"
+  echo "Kerberos admin user created: ${ADMIN_USERNAME} To update password: sudo /usr/sbin/kadmin.local -q \"change_password ${ADMIN_USERNAME}/admin\""
+
+  KEYTAB_DIR="/keytabs"
+  mkdir -p $KEYTAB_DIR
+
+  if [ ! -z "${HOST_PRINCIPALS}" ]; then
+    for host in ${HOST_PRINCIPALS}
+    do
+      /usr/sbin/kadmin.local -q "add_principal -pw hostpasswordvalue -kvno 1 host/$host"
+      echo "Created host principal host/$host"
+    done
+  fi
+
+  /usr/sbin/kadmin.local -q "add_principal -pw spnpasswordvalue -kvno 1 HTTP/host.resdom.gokrb5"
+
+  /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue -kvno 1 testuser1"
+  /usr/sbin/kadmin.local -q "add_principal +requires_preauth -pw passwordvalue -kvno 1 testuser2"
+  /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue -kvno 1 testuser3"
+
+  # Set up trust
+  /usr/sbin/kadmin.local -q "add_principal -requires_preauth -pw trustpasswd -kvno 1 krbtgt/TEST.GOKRB5@RESDOM.GOKRB5"
+  /usr/sbin/kadmin.local -q "add_principal -requires_preauth -pw trustpasswd -kvno 1 krbtgt/RESDOM.GOKRB5@TEST.GOKRB5"
+
+
+  echo "Kerberos initialisation complete"
diff --git a/testenv/docker/krb5kdc/krb5kdc-init.sh b/testenv/docker/krb5kdc/krb5kdc-init.sh
index cd4fe0e..ae5b72a 100644
--- a/testenv/docker/krb5kdc/krb5kdc-init.sh
+++ b/testenv/docker/krb5kdc/krb5kdc-init.sh
@@ -45,4 +45,8 @@ ENTROPY_PID=$!
   /usr/sbin/kadmin.local -q "add_principal +requires_preauth -pw passwordvalue -kvno 1 testuser2"
   /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue -kvno 1 testuser3"
 
+  # Set up trust
+  /usr/sbin/kadmin.local -q "add_principal -requires_preauth -pw trustpasswd -kvno 1 krbtgt/TEST.GOKRB5@RESDOM.GOKRB5"
+  /usr/sbin/kadmin.local -q "add_principal -requires_preauth -pw trustpasswd -kvno 1 krbtgt/RESDOM.GOKRB5@TEST.GOKRB5"
+
   echo "Kerberos initialisation complete"
diff --git a/testenv/mit-krb5kdc/bootstrap.sh b/testenv/mit-krb5kdc/bootstrap.sh
index 814ea5b..37d8d63 100644
--- a/testenv/mit-krb5kdc/bootstrap.sh
+++ b/testenv/mit-krb5kdc/bootstrap.sh
@@ -26,11 +26,12 @@ EOF
 
 cp /vagrant/krb5.conf /etc/krb5.conf
 cp /vagrant/*.service /etc/systemd/system/
-systemctl enable krb5kdc krb5kdc-latest krb5kdc-older httpd dns
+systemctl enable krb5kdc krb5kdc-resdom krb5kdc-latest krb5kdc-older httpd dns
 
 
 /usr/bin/docker pull jcmturner/gokrb5:http
 /usr/bin/docker pull jcmturner/gokrb5:kdc-centos-default
+/usr/bin/docker pull jcmturner/gokrb5:kdc-resdom
 /usr/bin/docker pull jcmturner/gokrb5:kdc-older
 /usr/bin/docker pull jcmturner/gokrb5:kdc-latest
 /usr/bin/docker pull jcmturner/gokrb5:dns
@@ -38,4 +39,4 @@ systemctl enable krb5kdc krb5kdc-latest krb5kdc-older httpd dns
 
 reboot
 
-#systemctl start docker krb5kdc krb5kdc-latest krb5kdc-older
+#systemctl start docker krb5kdc krb5kdc-res krb5kdc-latest krb5kdc-older
diff --git a/testenv/mit-krb5kdc/krb5kdc-resdom.service b/testenv/mit-krb5kdc/krb5kdc-resdom.service
new file mode 100644
index 0000000..21ac7c9
--- /dev/null
+++ b/testenv/mit-krb5kdc/krb5kdc-resdom.service
@@ -0,0 +1,22 @@
+[Unit]
+Description=Kerberos KDC Resource Domain
+After=docker.service
+After=network.target
+Requires=docker.service
+
+[Service]
+Environment="DOCKER_IMAGE=jcmturner/gokrb5:kdc-resdom"
+Environment="PORT=188"
+Environment="NAME=gokrb5-kdc-resdom"
+TimeoutStartSec=0
+#Restart=always
+ExecStartPre=-/usr/bin/docker kill %n
+ExecStartPre=-/usr/bin/docker rm %n
+ExecStartPre=-/usr/bin/docker pull ${DOCKER_IMAGE}
+ExecStart=/usr/bin/docker run -h kdc.resdom.gokrb5 -v /etc/localtime:/etc/localtime:ro -p ${PORT}:88 -p ${PORT}:88/udp --rm --name ${NAME} ${DOCKER_IMAGE}
+ExecStartPost=/bin/sh -c "while [ ! -f ${DATA_HOST}/.initialised ]; do sleep 2; done"
+ExecStop=/usr/bin/docker stop --time=60 %n
+ExecStopPost=-/usr/bin/docker rm %n
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file