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 17 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
41 changes: 39 additions & 2 deletions backend.go
Expand Up @@ -12,8 +12,24 @@ 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 (
aliasNameSources = []string{aliasNameSourceSAToken, aliasNameSourceSAPath}
// map alias name source to its description
aliasNameSourceMap = map[string]string{
aliasNameSourceSAToken: "format: <token.uid>",
aliasNameSourceSAPath: "format: <namespace>/<serviceaccount>",
}
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 +148,27 @@ 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
}

func getAliasNameSourceDesc() string {
benashz marked this conversation as resolved.
Show resolved Hide resolved
var desc = make([]string, len(aliasNameSources))
for i, s := range aliasNameSources {
d := aliasNameSourceMap[s]
if s == aliasNameSourceDefault {
d = d + " [default]"
}
desc[i] = fmt.Sprintf("%q (%s)", s, d)
}
return strings.Join(desc, ", ")
}

var backendHelp string = `
The Kubernetes Auth Backend allows authentication for Kubernetes service accounts.
`
97 changes: 76 additions & 21 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 Down Expand Up @@ -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 \"%s\"", roleName)), nil
benashz marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
}
return s.UID

if uid == "" {
return "", errors.New("could not parse UID from claims")
}
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