Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport #114 release/vault-1.8.x #117

Merged
merged 1 commit into from
Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kubeauth
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
Expand All @@ -12,8 +13,8 @@ import (
)

const (
configPath string = "config"
rolePrefix string = "role/"
configPath = "config"
rolePrefix = "role/"
)

// kubeAuthBackend implements logical.Backend
Expand Down Expand Up @@ -94,6 +95,17 @@ func (b *kubeAuthBackend) config(ctx context.Context, s logical.Storage) (*kubeC
return conf, nil
}

func (b *kubeAuthBackend) loadConfig(ctx context.Context, s logical.Storage) (*kubeConfig, error) {
config, err := b.config(ctx, s)
if err != nil {
return nil, err
}
if config == nil {
return nil, errors.New("could not load backend configuration")
}
return config, nil
}

// role takes a storage backend and the name and returns the role's storage
// entry
func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name string) (*roleStorageEntry, error) {
Expand Down
99 changes: 68 additions & 31 deletions path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ func pathLogin(b *kubeAuthBackend) *framework.Path {

// pathLogin is used to authenticate to this backend
func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
if len(roleName) == 0 {
return logical.ErrorResponse("missing role"), nil
roleName, resp := b.getFieldValueStr(data, "role")
if resp != nil {
return resp, nil
}

jwtStr := data.Get("jwt").(string)
if len(jwtStr) == 0 {
return logical.ErrorResponse("missing jwt"), nil
jwtStr, resp := b.getFieldValueStr(data, "jwt")
if resp != nil {
return resp, nil
}

b.l.RLock()
Expand All @@ -73,7 +73,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("invalid role name \"%s\"", roleName)), nil
return logical.ErrorResponse(fmt.Sprintf("invalid role name %q", roleName)), nil
}

// Check for a CIDR match.
Expand All @@ -87,31 +87,37 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
}
}

config, err := b.config(ctx, req.Storage)
config, err := b.loadConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, errors.New("could not load backend configuration")
}

serviceAccount, err := b.parseAndValidateJWT(jwtStr, role, config)
if err != nil {
return nil, err
}

aliasName, err := serviceAccount.uid()
if err != nil {
return nil, err
}

// look up the JWT token in the kubernetes API
err = serviceAccount.lookup(ctx, jwtStr, b.reviewFactory(config))
if err != nil {
b.Logger().Error(`login unauthorized due to: ` + err.Error())
return nil, logical.ErrPermissionDenied
}

uid, err := serviceAccount.uid()
if err != nil {
return nil, err
}
auth := &logical.Auth{
Alias: &logical.Alias{
Name: serviceAccount.uid(),
Name: aliasName,
Metadata: map[string]string{
"service_account_uid": serviceAccount.uid(),
"service_account_uid": uid,
"service_account_name": serviceAccount.name(),
"service_account_namespace": serviceAccount.namespace(),
"service_account_secret_name": serviceAccount.SecretName,
Expand All @@ -121,7 +127,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
"role": roleName,
},
Metadata: map[string]string{
"service_account_uid": serviceAccount.uid(),
"service_account_uid": uid,
"service_account_name": serviceAccount.name(),
"service_account_namespace": serviceAccount.namespace(),
"service_account_secret_name": serviceAccount.SecretName,
Expand All @@ -137,36 +143,57 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
}, nil
}

func (b *kubeAuthBackend) getFieldValueStr(data *framework.FieldData, param string) (string, *logical.Response) {
val := data.Get(param).(string)
if len(val) == 0 {
return "", logical.ErrorResponse("missing %s", param)
}
return val, nil
}

// aliasLookahead returns the alias object with the SA UID from the JWT
// Claims.
func (b *kubeAuthBackend) aliasLookahead(_ context.Context, _ *logical.Request, data *framework.FieldData) (*logical.Response, error) {
jwtStr := data.Get("jwt").(string)
if len(jwtStr) == 0 {
return logical.ErrorResponse("missing jwt"), nil
// Only JWTs matching the specified role's configuration will be accepted as valid.
func (b *kubeAuthBackend) aliasLookahead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName, resp := b.getFieldValueStr(data, "role")
if resp != nil {
return resp, nil
}

// Parse into JWT
parsedJWT, err := jws.ParseJWT([]byte(jwtStr))
jwtStr, resp := b.getFieldValueStr(data, "jwt")
if resp != nil {
return resp, nil
}

role, err := b.role(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("invalid role name %q", roleName)), nil
}

// Decode claims into a service account object
sa := &serviceAccount{}
err = mapstructure.Decode(parsedJWT.Claims(), sa)
config, err := b.loadConfig(ctx, req.Storage)
if err != nil {
return nil, err
}

saUID := sa.uid()
if saUID == "" {
return nil, errors.New("could not parse UID from claims")
// validation of the JWT against the provided role ensures alias look ahead requests
// are authentic.
sa, err := b.parseAndValidateJWT(jwtStr, role, config)
if err != nil {
return nil, err
}

aliasName, err := sa.uid()
if err != nil {
return nil, err
}

return &logical.Response{
Auth: &logical.Auth{
Alias: &logical.Alias{
Name: saUID,
Name: aliasName,
},
},
}, nil
Expand Down Expand Up @@ -316,11 +343,17 @@ type serviceAccount struct {

// uid returns the UID for the service account, preferring the projected service
// account value if found
func (s *serviceAccount) uid() string {
// return an error when the UID is empty.
func (s *serviceAccount) uid() (string, error) {
uid := s.UID
if s.Kubernetes != nil && s.Kubernetes.ServiceAccount != nil {
return s.Kubernetes.ServiceAccount.UID
uid = s.Kubernetes.ServiceAccount.UID
}

if uid == "" {
return "", errors.New("could not parse UID from claims")
}
return s.UID
return uid, nil
}

// name returns the name for the service account, preferring the projected
Expand Down Expand Up @@ -366,7 +399,11 @@ func (s *serviceAccount) lookup(ctx context.Context, jwtStr string, tr tokenRevi
if s.name() != r.Name {
return errors.New("JWT names did not match")
}
if s.uid() != r.UID {
uid, err := s.uid()
if err != nil {
return err
}
if uid != r.UID {
return errors.New("JWT UIDs did not match")
}
if s.namespace() != r.Namespace {
Expand Down
Loading