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

Add support for setting an Alias' name from the k8s namespace and serviceaccount #110

Merged
merged 19 commits into from Sep 13, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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