Skip to content

Commit

Permalink
feat: Support alternate alias naming schemes (#110)
Browse files Browse the repository at this point in the history
* add new configuration option allowing an Alias' name to be of the form <namespace>/<serviceaccount-name>

Co-authored-by: Damon Earp <damon.n.earp@nasa.gov>
  • Loading branch information
benashz and damonearp committed Sep 13, 2021
1 parent bd7e128 commit 79c7586
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 206 deletions.
25 changes: 23 additions & 2 deletions backend.go
Expand Up @@ -12,8 +12,20 @@ import (
)

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

// aliasNameSourceUnset provides backwards compatibility with preexisting roles.
aliasNameSourceUnset = ""
aliasNameSourceSAToken = "sa_token"
aliasNameSourceSAPath = "sa_path"
aliasNameSourceDefault = aliasNameSourceSAToken
)

var (
// when adding new alias name sources make sure to update the corresponding FieldSchema description in path_role.go
aliasNameSources = []string{aliasNameSourceSAToken, aliasNameSourceSAPath}
errInvalidAliasNameSource = fmt.Errorf(`invalid alias_name_source, must be one of: %s`, strings.Join(aliasNameSources, ", "))
)

// kubeAuthBackend implements logical.Backend
Expand Down Expand Up @@ -132,6 +144,15 @@ func (b *kubeAuthBackend) role(ctx context.Context, s logical.Storage, name stri
return role, nil
}

func validateAliasNameSource(source string) error {
for _, s := range aliasNameSources {
if s == source {
return nil
}
}
return errInvalidAliasNameSource
}

var backendHelp string = `
The Kubernetes Auth Backend allows authentication for Kubernetes service accounts.
`
99 changes: 77 additions & 22 deletions path_login.go
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 @@ -100,18 +100,27 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}

aliasName, err := b.getAliasName(role, serviceAccount)
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 +130,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,12 +146,48 @@ 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
}

func (b *kubeAuthBackend) getAliasName(role *roleStorageEntry, serviceAccount *serviceAccount) (string, error) {
switch role.AliasNameSource {
case aliasNameSourceSAToken, aliasNameSourceUnset:
uid, err := serviceAccount.uid()
if err != nil {
return "", err
}
return uid, nil
case aliasNameSourceSAPath:
return fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name), nil
default:
return "", fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource)
}
}

// 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
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
}

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
}

// Parse into JWT
Expand All @@ -158,15 +203,15 @@ func (b *kubeAuthBackend) aliasLookahead(_ context.Context, _ *logical.Request,
return nil, err
}

saUID := sa.uid()
if saUID == "" {
return nil, errors.New("could not parse UID from claims")
aliasName, err := b.getAliasName(role, sa)
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 +361,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 +417,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

0 comments on commit 79c7586

Please sign in to comment.