Jonathan Turner hace 9 años
padre
commit
18828bc3d6

+ 1 - 1
README.md

@@ -74,7 +74,7 @@ HTTPResp, err := http.DefaultClient.Do(r)
 
 ### Kerberos Web Service
 A HTTP handler wrapper can be used to implement Kerberos SPNEGO authentication for web services.
-To configure the wrapper the keytab for the service SPN and a Logger are required:
+To configure the wrapper the keytab for the SPN and a Logger are required:
 ```go
 kt, err := keytab.Load("/path/to/file.keytab")
 l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)

+ 0 - 115
debug.go

@@ -1,115 +0,0 @@
-package main
-
-import (
-	"encoding/hex"
-	"fmt"
-	"github.com/jcmturner/gokrb5/client"
-	"github.com/jcmturner/gokrb5/config"
-	"github.com/jcmturner/gokrb5/keytab"
-	"github.com/jcmturner/gokrb5/service"
-	"github.com/jcmturner/gokrb5/testdata"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"net/http/httptest"
-	"os"
-	"time"
-)
-
-const krb5conf = `[libdefaults]
-  default_realm = TEST.GOKRB5
-  dns_lookup_realm = false
-  dns_lookup_kdc = false
-  ticket_lifetime = 3m
-  renew_lifetime = 7m
-  forwardable = yes
-  default_tkt_enctypes = aes256-cts-hmac-sha1-96
-
-[realms]
- TEST.GOKRB5 = {
-  kdc = 10.80.88.88:88
-  admin_server = 10.80.88.88:749
-  default_domain = test.gokrb5
- }
-
-[domain_realm]
- .test.gokrb5 = TEST.GOKRB5
- test.gokrb5 = TEST.GOKRB5
- `
-
-func main() {
-	s := httpServer(false)
-	defer s.Close()
-	//httpRequest("http://host.test.gokrb5/index.html")
-	httpRequest(s.URL)
-	//runClient()
-}
-
-func runClient() {
-	b, err := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
-	kt, _ := keytab.Parse(b)
-	c, _ := config.NewConfigFromString(krb5conf)
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt)
-	cl.WithConfig(c)
-
-	err = cl.Login()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error on AS_REQ: %v\n", err)
-	}
-	cl.EnableAutoSessionRenewal()
-	for i := 0; i < 15; i++ {
-		tkt, _, err := cl.GetServiceTicket("HTTP/host.test.gokrb5")
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error on TGS_REQ: %v\n", err)
-		} else {
-			fmt.Fprintf(os.Stdout, "Service Ticket: %+v\n", tkt)
-		}
-		time.Sleep(time.Duration(1) * time.Minute)
-	}
-}
-
-func httpRequest(url string) {
-	l := log.New(os.Stderr, "GOKRB5 Client: ", log.Ldate|log.Ltime|log.Lshortfile)
-	b, err := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
-	kt, _ := keytab.Parse(b)
-	c, _ := config.NewConfigFromString(krb5conf)
-	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt)
-	cl.WithConfig(c)
-
-	err = cl.Login()
-	if err != nil {
-		l.Printf("Error on AS_REQ: %v\n", err)
-	}
-	r, _ := http.NewRequest("GET", url, nil)
-	err = cl.SetSPNEGOHeader(r, "HTTP/host.test.gokrb5")
-	if err != nil {
-		l.Printf("Error setting client SPNEGO header: %v", err)
-	}
-	httpResp, err := http.DefaultClient.Do(r)
-	if err != nil {
-		l.Printf("Request error: %v\n", err)
-	}
-	fmt.Fprintf(os.Stdout, "RESPONSE CODE: %v\n", httpResp.StatusCode)
-	content, _ := ioutil.ReadAll(httpResp.Body)
-	fmt.Fprintf(os.Stdout, "ResponseBody: %s\n", content)
-}
-
-func httpServer(tls bool) *httptest.Server {
-	l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
-	ks := "0502000000580002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d0360300120020c2bcd4abcde0d2608d5f505e7ab5dc92df5f627e5819703c0b0f1d2c05d51c1600000003000000480002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d0360300110010da152175c7a73f49e5ce4ece7068856400000003000000500002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d03603001000187fc8ef5276e083da6bf89e676d7f98fd1acb9ec2cb20083d00000003000000480002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d0360300170010011f2ef8e75e8378a94154beb002163200000003000000580002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d03603001a0020f9db4e36aad9688d9ea30dbcc269c7ee46bf4f8bd6250f203a9f3836f0a673a600000003000000480002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d0360300190010434981c9dce61ae1012f808bb60fc1c900000003000000400002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d03603000800080b0d4f31e061529800000003000000400002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158e7d0360300030008f7df40f457aec42c00000003"
-	b, _ := hex.DecodeString(ks)
-	kt, _ := keytab.Parse(b)
-	th := http.HandlerFunc(testAppHandler)
-	if tls {
-		s := httptest.NewTLSServer(service.SPNEGOKRB5Authenticate(th, kt, l))
-		return s
-	} else {
-		s := httptest.NewServer(service.SPNEGOKRB5Authenticate(th, kt, l))
-		return s
-	}
-}
-
-func testAppHandler(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusOK)
-	fmt.Fprintln(w, "TEST.GOKRB5 Handler")
-}

+ 78 - 0
example.go

@@ -0,0 +1,78 @@
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"github.com/jcmturner/gokrb5/client"
+	"github.com/jcmturner/gokrb5/config"
+	"github.com/jcmturner/gokrb5/keytab"
+	"github.com/jcmturner/gokrb5/service"
+	"github.com/jcmturner/gokrb5/testdata"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+)
+
+/*
+These examples have the following prerequisites:
+* Hashicorp Vagrant
+* VirtualBox
+
+The test environment relies upon a host only network configured within VirtualBox with a CIDR range of 10.80.0.0/16
+If this does not suit your setup then you will need to set the IP addresses for the private_network in the Vagrantfiles to something that suits you.
+You will also need to update the IPs referenced in the testdata/test_vectors.go file in the TEST_KRB5CONF constant.
+
+Before running execute the following commands (note that the KDC can take a long time to start up):
+cd $GOPATH/src/github.com/jcmturner/gokrb5/testenv/krb5kdc-vagrant && vagrant up
+cd $GOPATH/src/github.com/jcmturner/gokrb5/testenv/krbhttp-vagrant && vagrant up
+*/
+func main() {
+	s := httpServer()
+	defer s.Close()
+	httpRequest(s.URL)
+	//httpRequest("http://host.test.gokrb5/index.html")
+}
+
+func httpRequest(url string) {
+	l := log.New(os.Stderr, "GOKRB5 Client: ", log.Ldate|log.Ltime|log.Lshortfile)
+	b, err := hex.DecodeString(testdata.TESTUSER1_KEYTAB)
+	kt, _ := keytab.Parse(b)
+	c, _ := config.NewConfigFromString(testdata.TEST_KRB5CONF)
+	cl := client.NewClientWithKeytab("testuser1", "TEST.GOKRB5", kt)
+	cl.WithConfig(c)
+
+	err = cl.Login()
+	if err != nil {
+		l.Printf("Error on AS_REQ: %v\n", err)
+	}
+	cl.EnableAutoSessionRenewal()
+	r, _ := http.NewRequest("GET", url, nil)
+	err = cl.SetSPNEGOHeader(r, "HTTP/host.test.gokrb5")
+	if err != nil {
+		l.Printf("Error setting client SPNEGO header: %v", err)
+	}
+	httpResp, err := http.DefaultClient.Do(r)
+	if err != nil {
+		l.Printf("Request error: %v\n", err)
+	}
+	fmt.Fprintf(os.Stdout, "Response Body: %v\n", httpResp.StatusCode)
+	content, _ := ioutil.ReadAll(httpResp.Body)
+	fmt.Fprintf(os.Stdout, "Response Body: %s\n", content)
+}
+
+func httpServer() *httptest.Server {
+	l := log.New(os.Stderr, "GOKRB5 Service: ", log.Ldate|log.Ltime|log.Lshortfile)
+	b, _ := hex.DecodeString(testdata.HTTP_KEYTAB)
+	kt, _ := keytab.Parse(b)
+	th := http.HandlerFunc(testAppHandler)
+	s := httptest.NewServer(service.SPNEGOKRB5Authenticate(th, kt, l))
+	return s
+}
+
+func testAppHandler(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusOK)
+	fmt.Fprintln(w, "TEST.GOKRB5 Handler")
+	return
+}

+ 9 - 0
service/cache.go

@@ -29,22 +29,27 @@ and authenticator sent over the network to a server and replaying
 them following an event that caused the server to lose track of
 recently seen authenticators.*/
 
+// Cache for tickets received from clients keyed by fully qualified client name. Used to track replay of tickets.
 type ServiceCache map[string]ClientEntries
 
+// Entries for client details sent to the service.
 type ClientEntries struct {
 	ReplayMap map[time.Time]ReplayCacheEntry
 	SeqNumber int
 	SubKey    types.EncryptionKey
 }
 
+// Cache entry tracking client time values of tickets sent to the service.
 type ReplayCacheEntry struct {
 	PresentedTime time.Time
 	SName         types.PrincipalName
 	CTime         time.Time // This combines the ticket's CTime and Cusec
 }
 
+// Instance of the ServiceCache. This needs to be a singleton.
 var replayCache ServiceCache
 
+// Get a pointer to the ServiceCache singleton.
 func GetReplayCache(d time.Duration) *ServiceCache {
 	// Create a singleton of the ReplayCache and start a background thread to regularly clean out old entries
 	var once sync.Once
@@ -52,6 +57,7 @@ func GetReplayCache(d time.Duration) *ServiceCache {
 		replayCache = make(ServiceCache)
 		go func() {
 			for {
+				// TODO consider using a context here.
 				time.Sleep(d)
 				replayCache.ClearOldEntries(d)
 			}
@@ -60,6 +66,7 @@ func GetReplayCache(d time.Duration) *ServiceCache {
 	return &replayCache
 }
 
+// Add an entry to the ServiceCache.
 func (c *ServiceCache) AddEntry(sname types.PrincipalName, a types.Authenticator) {
 	ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
 	if ce, ok := (*c)[a.CName.GetPrincipalNameString()]; ok {
@@ -85,6 +92,7 @@ func (c *ServiceCache) AddEntry(sname types.PrincipalName, a types.Authenticator
 	}
 }
 
+// Clear entries from the ServiceCache that are older than the duration provided.
 func (c *ServiceCache) ClearOldEntries(d time.Duration) {
 	for ck := range *c {
 		for ct, e := range (*c)[ck].ReplayMap {
@@ -98,6 +106,7 @@ func (c *ServiceCache) ClearOldEntries(d time.Duration) {
 	}
 }
 
+// Check if the Authenticator provided is a replay within the duration defined. If this is not a replay add the entry to the cache for tracking.
 func (c *ServiceCache) IsReplay(d time.Duration, sname types.PrincipalName, a types.Authenticator) bool {
 	if ck, ok := (*c)[a.CName.GetPrincipalNameString()]; ok {
 		ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)

+ 4 - 1
service/http.go

@@ -20,10 +20,11 @@ import (
 const (
 	// The response on successful authentication always has this header. Capturing as const so we don't have marshaling and encoding overhead.
 	SPNEGO_NegTokenResp_Krb_Accept_Completed = "Negotiate oRQwEqADCgEAoQsGCSqGSIb3EgECAg=="
+	// The response on a failed authentication always has this rejection header. Capturing as const so we don't have marshaling and encoding overhead.
 	SPNEGO_NegTokenResp_Reject               = "Negotiate oQcwBaADCgEC"
 )
 
-// SPNEGO Kerberos HTTP handler wrapper
+// Kerberos SPNEGO authentication HTTP handler wrapper.
 func SPNEGOKRB5Authenticate(f http.Handler, ktab keytab.Keytab, l *log.Logger) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
@@ -92,6 +93,7 @@ func SPNEGOKRB5Authenticate(f http.Handler, ktab keytab.Keytab, l *log.Logger) h
 	})
 }
 
+// Validate the AP_REQ provided in the SPNEGO NegTokenInit.
 func validateAPREQ(a types.Authenticator, APReq messages.APReq) (bool, error) {
 	// Check CName in Authenticator is the same as that in the ticket
 	if !a.CName.Equal(APReq.Ticket.DecryptedEncPart.CName) {
@@ -136,6 +138,7 @@ func validateAPREQ(a types.Authenticator, APReq messages.APReq) (bool, error) {
 	return true, nil
 }
 
+// Set the headers for a rejected SPNEGO negotiation and return an unauthorized status code.
 func rejectSPNEGO(w http.ResponseWriter, l *log.Logger, logMsg string) {
 	if l != nil {
 		l.Println(logMsg)

+ 2 - 1
testdata/test_vectors.go

@@ -98,7 +98,8 @@ var TestVectors = map[string]string{
 }
 
 const (
-	TESTUSER1_KEYTAB = "05020000004b0001000b544553542e474f4b52423500097465737475736572310000000158e543060100120020bbdc430aab7e2d4622a0b6951481453b0962e9db8e2f168942ad175cda6d9de900000001"
+	TESTUSER1_KEYTAB = "05020000003b0001000b544553542e474f4b52423500097465737475736572310000000158ee757c0100110010698c4df8e9f60e7eea5a21bf4526ad25000000010000004b0001000b544553542e474f4b52423500097465737475736572310000000158ee757c0100120020bbdc430aab7e2d4622a0b6951481453b0962e9db8e2f168942ad175cda6d9de900000001"
+	HTTP_KEYTAB      = "0502000000480002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158ee7636010011001057a7754c70c4d85c155c718c2f1292b000000001000000580002000b544553542e474f4b5242350004485454500010686f73742e746573742e676f6b7262350000000158ee763601001200209cad00bbc72d703258e911dc18e6d5487cf737bf67fd111f0c2463ad6033bf5100000001"
 	TEST_AS_REQ      = "6a81a63081a3a103020105a20302010aa30e300c300aa10402020095a2020400a48186308183a00703050040000010a1163014a003020101a10d300b1b09746573747573657231a20d1b0b544553542e474f4b524235a320301ea003020102a11730151b066b72627467741b0b544553542e474f4b524235a511180f32303137303232303134323530315aa70602040f6755a6a814301202011202011102011002011702011902011a"
 	TEST_AS_REP      = "6b8202f3308202efa003020105a10302010ba22e302c302aa103020113a2230421301f301da003020112a1161b14544553542e474f4b524235746573747573657231a30d1b0b544553542e474f4b524235a4163014a003020101a10d300b1b09746573747573657231a582015a6182015630820152a003020105a10d1b0b544553542e474f4b524235a220301ea003020102a11730151b066b72627467741b0b544553542e474f4b524235a382011830820114a003020112a103020101a28201060482010264d3fa49d89b627ed471298846ff92cd8632f657c58fe25322a61fffa32bb7966dc4c44c86a81353def2a11c36c537191406a609147f424a63266c00d02bcc56a27b0969d86ff4352634be9e2a4ac0ad5a36b0b0a3d689f128c0afa97401796e88037a35ad19efaf31d1ed4f3213769c03a58bc90ffac2051db152c0ed0809ad05ffb03aa3afaf731ed85f7a73020cb72355e0de27842dcf7eae3de9f7c14aa237edb25153b217ef3693373bc3cacbebe406910ff9ae9d00b7b08f726cb29a213cb9ad51ba80a8c24fa4b6692a445686889702cfa6ea749bac03e27e982407aca623fbd48586bcf566cfe87e1d9f17a74b1315669c16480f93e9d8782e71a8f11000a682012c30820128a003020112a282011f0482011b99b86153c0393c0e4130628f3e1e0f0a1f034e7e61a111b7fad15884e231c8fd8727e0bc945c9b35be20c57d057c8b09b0de74c53fb38cc15c9a2d483023fc369f5bde4da7324b4732b5a3d9504d92f67026aaa01df4f0138245d2ccb1c5a4014804cf295c7e7e56a867e6cf0c534f667f32da7aa5e700af1461764f1c276a8ff0fbee0e99322fe2059d2321853be09d0956c3afcfd07e3e702646a4678926a77bea20d9aaf3086b6d384821c81900af9013a3519f0e50eab6e1491d72e4ee17c2a44441b2ebc8a796cc3d876e328347dce65f61104e14d4c31532885776c9c8a70186b8b39f928972945c98bd60381ead5448e7ebe93fea308054287ac34b0583b4b9b5e43c5f8518d693ba9eb48a219c27344466b3c693a70462"
 	TEST_TGS_REQ     = "6c82038f3082038ba103020105a20302010ca382031a3082031630820245a103020101a282023c048202386e82023430820230a003020105a10302010ea20703050000000000a382015a6182015630820152a003020105a10d1b0b544553542e474f4b524235a220301ea003020102a11730151b066b72627467741b0b544553542e474f4b524235a382011830820114a003020112a103020101a28201060482010264d3fa49d89b627ed471298846ff92cd8632f657c58fe25322a61fffa32bb7966dc4c44c86a81353def2a11c36c537191406a609147f424a63266c00d02bcc56a27b0969d86ff4352634be9e2a4ac0ad5a36b0b0a3d689f128c0afa97401796e88037a35ad19efaf31d1ed4f3213769c03a58bc90ffac2051db152c0ed0809ad05ffb03aa3afaf731ed85f7a73020cb72355e0de27842dcf7eae3de9f7c14aa237edb25153b217ef3693373bc3cacbebe406910ff9ae9d00b7b08f726cb29a213cb9ad51ba80a8c24fa4b6692a445686889702cfa6ea749bac03e27e982407aca623fbd48586bcf566cfe87e1d9f17a74b1315669c16480f93e9d8782e71a8f11000a481bc3081b9a003020112a281b10481ae8ae3cb8ac47d77cfc7b0b6bf0d3c5f8fcc6dd569344256a6a40c004fc2d23ebbe6ee0b9e00eccf37e710b7c01a7d2a63bbed6d75f2b230d24d724ef90edad2c5680e7e2436ab1145ff68481673444ebd61e3aef79b9ee05809551672c6c436eb8ac732a7fe78bd8f380e68a541191e3125554e4bab63dcc19ea931c1477366a6039ff7b7e62521ebfeffd6784b6ef0c97f653ac4d8dfb304f3e2e843faab12d838c23f1105f0a281c39325987cb03081caa10402020088a281c10481bea081bb3081b8a1173015a003020110a10e040ce613d8e9d544f0e56c60d3bba2819c308199a003020112a2819104818ec4fabcb1ec2f24e04ef51f9247239b28275653fa5cbc1dc9e747530c597631050fe86a5f3cba2ff54270aa771dcefa87efc8c8604407f84e603f5c01a2d929e18103561c3ffbc3a0cf63340bdd67a0739d4d81989827fc1d3f7f13e9dd5cc2346ca08e26a2aaf6d0102fbef8f7a6ee0a1caae7880e953ea678da619038786122a0b71853e8d0b95f544f8fbd6945a461305fa00703050040810000a20d1b0b544553542e474f4b524235a3233021a003020101a11a30181b04485454501b10686f73742e746573742e676f6b726235a511180f32303137303232303032323634325aa706020458a9ab2aa8053003020112"

BIN
testenv/http.testtab


+ 30 - 30
testenv/krb5kdc-vagrant/kdc-setup.sh

@@ -48,9 +48,9 @@ if [ ! -f /opt/krb5/data/principal ]; then
   if [ ! -z "${HOST_PRINCIPALS}" ]; then
     for host in ${HOST_PRINCIPALS}
     do
-      /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue host/$host"
-      /usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${host}.keytab host/$host"
-      chmod 600 ${KEYTAB_DIR}/${host}.keytab
+      /usr/sbin/kadmin.local -q "add_principal -pw hostpasswordvalue -kvno 1 host/$host"
+      #/usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${host}.keytab host/$host"
+      #chmod 600 ${KEYTAB_DIR}/${host}.keytab
       echo "Created host principal host/$host"
     done
   fi
@@ -58,8 +58,8 @@ if [ ! -f /opt/krb5/data/principal ]; then
   if [ ! -z "${SPNs}" ]; then
     for service in ${SPNs}
     do
-      /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue ${service}"
-      /usr/sbin/kadmin.local -q "cpw -pw passwordvalue ${service}"
+      /usr/sbin/kadmin.local -q "add_principal -pw spnpasswordvalue -kvno 1 ${service}"
+      #/usr/sbin/kadmin.local -q "cpw -pw passwordvalue ${service}"
       echo "Created principal for service $service"
     done
   fi
@@ -67,34 +67,34 @@ if [ ! -f /opt/krb5/data/principal ]; then
   if [ ! -z "$INITIAL_USERS" ]; then
     for user in $INITIAL_USERS
     do
-      /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue $user"
-      /usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${user}.testtab $user"
-      echo "User $user added to kerberos database with random password. To update password: sudo /usr/sbin/kadmin.local -q \"change_password $user\""
+      /usr/sbin/kadmin.local -q "add_principal -pw passwordvalue -kvno 1 $user"
+      #/usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${user}.testtab $user"
+      echo "User $user added to kerberos database. To update password: sudo /usr/sbin/kadmin.local -q \"change_password $user\""
     done
   fi
 
-  if [ ! -z "$KEYTABS" ]; then
-    echo "Exporting keytabs"
-    OLDIFS=$IFS
-    IFS=";"
-    KEYTAB=($KEYTABS)
-    for keytabInst in ${KEYTAB[@]}
-    do
-      keytabFileName=$(echo $keytabInst | cut -d! -f1)
-      permissions=$(echo $keytabInst | cut -d! -f2)
-      principals=$(echo $keytabInst | cut -d! -f3)
-      IFS=" "
-      for princ in $principals
-      do
-        /usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${keytabFileName} $princ"
-        echo "Add principal ${princ} to ${keytabFileName}"
-      done
-      IFS=";"
-      chown $permissions ${KEYTAB_DIR}/${keytabFileName}
-      chmod 660 ${KEYTAB_DIR}/${keytabFileName}
-    done
-    IFS=$OLDIFS
-  fi
+#  if [ ! -z "$KEYTABS" ]; then
+#    echo "Exporting keytabs"
+#    OLDIFS=$IFS
+#    IFS=";"
+#    KEYTAB=($KEYTABS)
+#    for keytabInst in ${KEYTAB[@]}
+#    do
+#      keytabFileName=$(echo $keytabInst | cut -d! -f1)
+#      permissions=$(echo $keytabInst | cut -d! -f2)
+#      principals=$(echo $keytabInst | cut -d! -f3)
+#      IFS=" "
+#      for princ in $principals
+#      do
+#        /usr/sbin/kadmin.local -q "ktadd -k ${KEYTAB_DIR}/${keytabFileName} $princ"
+#        echo "Add principal ${princ} to ${keytabFileName}"
+#      done
+#      IFS=";"
+#      chown $permissions ${KEYTAB_DIR}/${keytabFileName}
+#      chmod 660 ${KEYTAB_DIR}/${keytabFileName}
+#    done
+#    IFS=$OLDIFS
+#  fi
 
   echo "Kerberos initialisation complete"
 fi

BIN
testenv/krbhttp-vagrant/http.testtab


BIN
testenv/testuser1.testtab


BIN
testenv/testuser2.testtab


BIN
testenv/testuser3.testtab