diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | client/client.go | 77 | ||||
-rw-r--r-- | client/cmd.go | 131 | ||||
-rw-r--r-- | client/messages.go | 69 | ||||
-rw-r--r-- | client/network.go | 92 | ||||
-rw-r--r-- | config/config.go | 6 | ||||
-rw-r--r-- | path_config.go | 43 |
7 files changed, 157 insertions, 263 deletions
@@ -26,7 +26,7 @@ enable: vault secrets enable -path=krb vault-plugin-kerberos-secrets test-config: - vault write krb/config realm=TARDISPROJECT.UK kdc=localhost:88 kpasswd_server=localhost:464 username=test/admin password=1234 + vault write krb/config realm=TARDISPROJECT.UK kdc=localhost:88 kadmin_server=localhost:749 username=test/admin password=1234 test-role: vault write krb/static-role/test principal=test diff --git a/client/client.go b/client/client.go index 1bfad5f..d6a3487 100644 --- a/client/client.go +++ b/client/client.go @@ -6,40 +6,18 @@ import ( "sync" "git.tardisproject.uk/tcmal/vault-plugin-kerberos-secrets/config" - krbClient "github.com/jcmturner/gokrb5/v8/client" - krbConfig "github.com/jcmturner/gokrb5/v8/config" - "github.com/jcmturner/gokrb5/v8/iana/nametype" - // "github.com/jcmturner/gokrb5/v8/kadmin" - krbMessages "github.com/jcmturner/gokrb5/v8/messages" - krbTypes "github.com/jcmturner/gokrb5/v8/types" ) type client struct { *sync.Mutex - kCfg *krbConfig.Config - kClient *krbClient.Client + config *config.Config } func ClientFromConfig(config *config.Config) (client, error) { - kCfg := krbConfig.New() - kCfg.Realms = []krbConfig.Realm{ - { - Realm: config.Realm, - DefaultDomain: config.Realm, - KDC: config.KDC, - KPasswdServer: config.KPasswdServer, - AdminServer: []string{}, - MasterKDC: config.KDC, - }, - } - - kClient := krbClient.NewWithPassword(config.Username, config.Realm, config.Password, kCfg) - return client{ &sync.Mutex{}, - kCfg, - kClient, + config, }, nil } @@ -47,50 +25,19 @@ func (c client) SetPassword(ctx context.Context, username string, password strin c.Lock() defer c.Unlock() - if err := c.kClient.AffirmLogin(); err != nil { - return fmt.Errorf("error logging in as admin principal: %e", err) - } - - // Get a ticket for using kadmin/admin - cl := c.kClient - ASReq, err := krbMessages.NewASReqForChgPasswd(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName()) + // check if the principal exists + exists, err := c.princExists(ctx, username) if err != nil { - return fmt.Errorf("error creating ticket request for kadmin: %s", err) - } - ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0) - if err != nil { - return fmt.Errorf("error exchanging request for kadmin ticket: %s", err) + return fmt.Errorf("error checking principal exists: %s", err) } - // Construct the change passwd msg - msg, key, err := ChangePasswdMsg( - krbTypes.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username), - cl.Credentials.CName(), - cl.Credentials.Domain(), - password, - ASRep.Ticket, - ASRep.DecryptedEncPart.Key, - ) - - if err != nil { - return fmt.Errorf("error creating change passwd msg: %s", err) - } - - // Send it to kpasswd - r, err := sendToKAdmin(cl, msg) - if err != nil { - return fmt.Errorf("error communicating with kpasswd: %s", err) - } - - // Decrypt the result - if r.ResultCode != 0 { - return fmt.Errorf("error response from kadmin: code: %d; result: %s; krberror: %v", r.ResultCode, r.Result, r.KRBError) - } - - err = r.Decrypt(key) - if err != nil { - return fmt.Errorf("error decrypting result: %s", err) + if !exists { + // if not, create it + err = c.doCreatePrinc(ctx, username, password) + } else { + // otherwise, just set the password + err = c.doChangePassword(ctx, username, password) } - return nil + return err } diff --git a/client/cmd.go b/client/cmd.go new file mode 100644 index 0000000..b69880c --- /dev/null +++ b/client/cmd.go @@ -0,0 +1,131 @@ +package client + +import ( + "context" + "fmt" + "io" + "os/exec" + "strings" +) + +func (c client) princExists(ctx context.Context, name string) (exists bool, err error) { + out, err := c.execKrbAdmin(ctx, fmt.Sprintf("getprinc \"%s\"", name), func(writer io.WriteCloser) error { return nil }) + if err != nil { + err = fmt.Errorf("%s (output: %s)", err, out) + return + } + + if strings.Contains(out, "Principal does not exist") { + exists = false + } else if strings.Contains(out, "Expiration date: ") { + exists = true + } else { + err = fmt.Errorf("unrecognised output format: %s", out) + } + + return +} + +func (c client) doCreatePrinc(ctx context.Context, name string, password string) (err error) { + out, err := c.execKrbAdmin(ctx, fmt.Sprintf("addprinc \"%s\"", name), func(writer io.WriteCloser) error { + toWrite := append([]byte(password), '\n') + for i := 0; i < 2; i++ { + n, err := writer.Write(toWrite) + if err != nil || n != len(toWrite) { + return fmt.Errorf("error writing to stdin: %s", err) + } + } + return nil + }) + + if err != nil { + return + } + + if !strings.Contains(out, "created") { + err = fmt.Errorf("unrecognised output format: %s", out) + } + + return +} + +func (c client) doChangePassword(ctx context.Context, name string, password string) (err error) { + out, err := c.execKrbAdmin(ctx, fmt.Sprintf("cpw \"%s\"", name), func(writer io.WriteCloser) error { + toWrite := append([]byte(password), '\n') + for i := 0; i < 2; i++ { + n, err := writer.Write(toWrite) + if err != nil || n != len(toWrite) { + return fmt.Errorf("error writing to stdin: %s", err) + } + } + return nil + }) + + if err != nil { + return + } + + if !strings.Contains(out, "changed") { + err = fmt.Errorf("unrecognised output format: %s", out) + } + + return +} + +// execKrbAdmin starts krbadmin with the appropriate commands, and tries to authenticate as the admin principal +func (c client) execKrbAdmin(ctx context.Context, query string, writeFunc func(writer io.WriteCloser) error) (string, error) { + kadm, err := exec.LookPath("kadmin") + if err != nil { + return "", fmt.Errorf("error finding kadmin executable: %s", err) + } + cmd := exec.CommandContext( + ctx, + kadm, + "-p", + c.config.Username, + "-r", + c.config.Realm, + "-s", + c.config.KAdminServer, + "-q", + query, + ) + + stdin, err := cmd.StdinPipe() + if err != nil { + return "", fmt.Errorf("error getting stdin pipe: %s", err) + } + + errChan := make(chan error) + + go func() { + defer stdin.Close() + toWrite := append([]byte(c.config.Password), '\n') + n, err := stdin.Write(toWrite) + + if err != nil || n != len(toWrite) { + errChan <- fmt.Errorf("error writing to stdin: %s", err) + return + } + + err = writeFunc(stdin) + if err != nil || n != len(toWrite) { + errChan <- fmt.Errorf("error writing to stdin: %s", err) + return + } + }() + + rawOut, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + out := string(rawOut) + + select { + case err = <-errChan: + return "", err + default: + } + + return out, nil +} diff --git a/client/messages.go b/client/messages.go deleted file mode 100644 index b3d2c4f..0000000 --- a/client/messages.go +++ /dev/null @@ -1,69 +0,0 @@ -package client - -import ( - "github.com/jcmturner/gokrb5/v8/crypto" - "github.com/jcmturner/gokrb5/v8/kadmin" - "github.com/jcmturner/gokrb5/v8/krberror" - "github.com/jcmturner/gokrb5/v8/messages" - "github.com/jcmturner/gokrb5/v8/types" -) - -// ChangePasswdMsg generate a change password request and also return the key needed to decrypt the reply. -func ChangePasswdMsg(targetName types.PrincipalName, cname types.PrincipalName, realm, password string, tkt messages.Ticket, sessionKey types.EncryptionKey) (r kadmin.Request, k types.EncryptionKey, err error) { - // Create change password data struct and marshal to bytes - chgpasswd := kadmin.ChangePasswdData{ - NewPasswd: []byte(password), - TargName: targetName, - TargRealm: realm, - } - chpwdb, err := chgpasswd.Marshal() - if err != nil { - err = krberror.Errorf(err, krberror.KRBMsgError, "error marshaling change passwd data") - return - } - - // Generate authenticator - auth, err := types.NewAuthenticator(realm, cname) - if err != nil { - err = krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator") - return - } - etype, err := crypto.GetEtype(sessionKey.KeyType) - if err != nil { - err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey etype") - return - } - err = auth.GenerateSeqNumberAndSubKey(etype.GetETypeID(), etype.GetKeyByteSize()) - if err != nil { - err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey") - return - } - k = auth.SubKey - - // Generate AP_REQ - APreq, err := messages.NewAPReq(tkt, sessionKey, auth) - if err != nil { - return - } - - // Form the KRBPriv encpart data - kp := messages.EncKrbPrivPart{ - UserData: chpwdb, - Timestamp: auth.CTime, - Usec: auth.Cusec, - SequenceNumber: auth.SeqNumber, - } - kpriv := messages.NewKRBPriv(kp) - - err = kpriv.EncryptEncPart(k) - if err != nil { - err = krberror.Errorf(err, krberror.EncryptingError, "error encrypting change passwd data") - return - } - - r = kadmin.Request{ - APREQ: APreq, - KRBPriv: kpriv, - } - return -} diff --git a/client/network.go b/client/network.go deleted file mode 100644 index b29928f..0000000 --- a/client/network.go +++ /dev/null @@ -1,92 +0,0 @@ -package client - -import ( - "encoding/binary" - "fmt" - "io" - "net" - "strings" - "time" - - krbClient "github.com/jcmturner/gokrb5/v8/client" - "github.com/jcmturner/gokrb5/v8/kadmin" -) - -// From here: https://github.com/jcmturner/gokrb5/blob/v8.4.4/v8/client/passwd.go#L51C1-L75C2 -// It would be really nice if this was public, but it isn't :( -func sendToKAdmin(cl *krbClient.Client, msg kadmin.Request) (r kadmin.Reply, err error) { - _, kps, err := cl.Config.GetKpasswdServers(cl.Credentials.Domain(), true) - if err != nil { - return - } - b, err := msg.Marshal() - if err != nil { - return - } - var rb []byte - rb, err = dialSendTCP(kps, b) - if err != nil { - return - } - err = r.Unmarshal(rb) - - return -} - -// Below are from here: https://github.com/jcmturner/gokrb5/blob/master/v8/client/network.go -// Likewise, it sucks that the change password API is so limited, and there is zero low-level exposure without copy-pasting code. -// dialSendTCP establishes a TCP connection to a KDC. -func dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) { - var errs []string - for i := 1; i <= len(kdcs); i++ { - conn, err := net.DialTimeout("tcp", kdcs[i], 5*time.Second) - if err != nil { - errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) - continue - } - if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { - errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) - continue - } - // conn is guaranteed to be a TCPConn - rb, err := sendTCP(conn.(*net.TCPConn), b) - if err != nil { - errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) - continue - } - return rb, nil - } - return nil, fmt.Errorf("error sending: %s", strings.Join(errs, "; ")) -} - -// sendTCP sends bytes to connection over TCP. -func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { - defer conn.Close() - var r []byte - // RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order. - hb := make([]byte, 4, 4) - binary.BigEndian.PutUint32(hb, uint32(len(b))) - b = append(hb, b...) - - _, err := conn.Write(b) - if err != nil { - return r, fmt.Errorf("error sending to %s: %v", conn.RemoteAddr().String(), err) - } - - sh := make([]byte, 4, 4) - _, err = conn.Read(sh) - if err != nil { - return r, fmt.Errorf("error reading response size header: %v", err) - } - s := binary.BigEndian.Uint32(sh) - - rb := make([]byte, s, s) - _, err = io.ReadFull(conn, rb) - if err != nil { - return r, fmt.Errorf("error reading response: %v", err) - } - if len(rb) < 1 { - return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String()) - } - return rb, nil -} diff --git a/config/config.go b/config/config.go index 270df49..bc4a055 100644 --- a/config/config.go +++ b/config/config.go @@ -1,9 +1,9 @@ package config type Config struct { - Realm string `json:"realm"` - KDC []string `json:"kdc"` - KPasswdServer []string `json:"kpasswd_server"` + Realm string `json:"realm"` + KDC []string `json:"kdc"` + KAdminServer string `json:"kpasswd_server"` Username string `json:"username"` Password string `json:"password"` diff --git a/path_config.go b/path_config.go index 8a3187e..0fcec4c 100644 --- a/path_config.go +++ b/path_config.go @@ -71,21 +71,12 @@ func configSchema() map[string]*framework.FieldSchema { Sensitive: true, }, }, - "kdc": { - Type: framework.TypeCommaStringSlice, - Description: "Available KDCs for the realm", - Required: true, - DisplayAttrs: &framework.DisplayAttributes{ - Name: "KDCs", - Sensitive: false, - }, - }, - "kpasswd_server": { - Type: framework.TypeCommaStringSlice, - Description: "KPasswd servers for the realm", + "kadmin_server": { + Type: framework.TypeString, + Description: "KAdmin server for the realm", Required: true, DisplayAttrs: &framework.DisplayAttributes{ - Name: "KPasswd Servers", + Name: "KAdmin Server", Sensitive: false, }, }, @@ -116,10 +107,9 @@ func (b *krbBackend) pathConfigRead(ctx context.Context, req *logical.Request, d return &logical.Response{ Data: map[string]interface{}{ - "realm": config.Realm, - "kdc": config.KDC, - "kpasswd_server": config.KPasswdServer, - "username": config.Username, + "realm": config.Realm, + "kadmin_server": config.KAdminServer, + "username": config.Username, }, }, nil } @@ -146,23 +136,10 @@ func (b *krbBackend) pathConfigWrite(ctx context.Context, req *logical.Request, return nil, fmt.Errorf("missing realm in configuration") } - // TODO: Also validate these aren't empty - if kdc, ok := data.GetOk("kdc"); ok { - c.KDC = kdc.([]string) + if kpasswd_server, ok := data.GetOk("kadmin_server"); ok { + c.KAdminServer = kpasswd_server.(string) } else if !ok && createOperation { - return nil, fmt.Errorf("missing KDCs in configuration") - } - if len(c.KDC) == 0 { - return nil, fmt.Errorf("no KDCs specified") - } - - if kpasswd_server, ok := data.GetOk("kpasswd_server"); ok { - c.KPasswdServer = kpasswd_server.([]string) - } else if !ok && createOperation { - return nil, fmt.Errorf("missing kpasswd servers in configuration") - } - if len(c.KPasswdServer) == 0 { - return nil, fmt.Errorf("no kpasswd servers specified") + return nil, fmt.Errorf("missing kadmin server in configuration") } if username, ok := data.GetOk("username"); ok { |