Skip to content

Commit

Permalink
set auth alias custom metadata to service account annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
thyton committed Jan 2, 2024
1 parent 6f9c733 commit 19556d7
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 15 deletions.
11 changes: 9 additions & 2 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ type kubeAuthBackend struct {
// only be used in tests.
namespaceValidatorFactory namespaceValidatorFactory

// serviceAccountGetterFactory is used to configure the strategy for retrieving
// service account properties (currently metadata). Currently, the only options
// are using the kubernetes API or mocking the retrieval. Mocks should
// only be used in tests.
serviceAccountGetterFactory serviceAccountGetterFactory

// localSATokenReader caches the service account token in memory.
// It periodically reloads the token to support token rotation/renewal.
// Local token is used when running in a pod with following configuration
Expand Down Expand Up @@ -139,8 +145,9 @@ func Backend() *kubeAuthBackend {
// Set the default TLSConfig
tlsConfig: getDefaultTLSConfig(),
// Set the review factory to default to calling into the kubernetes API.
reviewFactory: tokenReviewAPIFactory,
namespaceValidatorFactory: newNsValidatorWrapper,
reviewFactory: tokenReviewAPIFactory,
namespaceValidatorFactory: newNsValidatorWrapper,
serviceAccountGetterFactory: newServiceAccountGetterWrapper,
}

b.Backend = &framework.Backend{
Expand Down
30 changes: 30 additions & 0 deletions integrationtest/vault/serviceAccountControllerBinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: test-service-account-getter-account-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:controller:service-account-controller
subjects:
- kind: ServiceAccount
name: test-token-reviewer-account
namespace: test
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: test-service-account-getter-account-binding-vault
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:controller:service-account-controller
subjects:
- kind: ServiceAccount
name: vault
namespace: test


30 changes: 18 additions & 12 deletions path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, logical.ErrUnrecoverable
}

serviceAccount, err := b.parseAndValidateJWT(ctx, client, jwtStr, role, config)
sa, err := b.parseAndValidateJWT(ctx, client, jwtStr, role, config)
if err != nil {
if err == jose.ErrCryptoFailure || strings.Contains(err.Error(), "verifying token signature") {
b.Logger().Debug(`login unauthorized`, "err", err)
Expand All @@ -139,20 +139,25 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}

aliasName, err := b.getAliasName(role, serviceAccount)
aliasName, err := b.getAliasName(role, sa)
if err != nil {
return nil, err
}

// look up the JWT token in the kubernetes API
err = serviceAccount.lookup(ctx, client, jwtStr, role.Audience, b.reviewFactory(config))

err = sa.lookup(ctx, client, jwtStr, role.Audience, b.reviewFactory(config))
if err != nil {
b.Logger().Debug(`login unauthorized`, "err", err)
return nil, logical.ErrPermissionDenied
}

uid, err := serviceAccount.uid()
annotations, err := b.serviceAccountGetterFactory(config).annotations(ctx, client, sa.Namespace, sa.Name)
if err != nil {
b.Logger().Debug("failed to get service account annotations", "err", err)
return nil, err
}

uid, err := sa.uid()
if err != nil {
return nil, err
}
Expand All @@ -161,22 +166,23 @@ func (b *kubeAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
Name: aliasName,
Metadata: map[string]string{
"service_account_uid": uid,
"service_account_name": serviceAccount.name(),
"service_account_namespace": serviceAccount.namespace(),
"service_account_secret_name": serviceAccount.SecretName,
"service_account_name": sa.name(),
"service_account_namespace": sa.namespace(),
"service_account_secret_name": sa.SecretName,
},
CustomMetadata: annotations,
},
InternalData: map[string]interface{}{
"role": roleName,
},
Metadata: map[string]string{
"service_account_uid": uid,
"service_account_name": serviceAccount.name(),
"service_account_namespace": serviceAccount.namespace(),
"service_account_secret_name": serviceAccount.SecretName,
"service_account_name": sa.name(),
"service_account_namespace": sa.namespace(),
"service_account_secret_name": sa.SecretName,
"role": roleName,
},
DisplayName: fmt.Sprintf("%s-%s", serviceAccount.namespace(), serviceAccount.name()),
DisplayName: fmt.Sprintf("%s-%s", sa.namespace(), sa.name()),
}

role.PopulateTokenAuth(auth)
Expand Down
37 changes: 36 additions & 1 deletion path_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ var (
testNamespace = "default"
testName = "vault-auth"
testUID = "d77f89bc-9055-11e7-a068-0800276d99bf"
testMetadataAnnotations = map[string]string{"key": "value", "foo": "bar"}
testMockTokenReviewFactory = mockTokenReviewFactory(testName, testNamespace, testUID)
testMockNamespaceValidateFactory = mockNamespaceValidateFactory(
map[string]string{"key": "value", "other": "label"})

testGlobbedNamespace = "def*"
testGlobbedName = "vault-*"

Expand Down Expand Up @@ -290,6 +290,13 @@ func setupBackend(t *testing.T, config *testBackendConfig) (logical.Backend, log

b.(*kubeAuthBackend).reviewFactory = testMockTokenReviewFactory
b.(*kubeAuthBackend).namespaceValidatorFactory = testMockNamespaceValidateFactory

sa := v1.ObjectMeta{Annotations: map[string]string{}}
for k, v := range testMetadataAnnotations {
sa.Annotations[fmt.Sprintf("%s%s", annotationKeyPrefix, k)] = v
}
b.(*kubeAuthBackend).serviceAccountGetterFactory = mockServiceAccountGetterFactory(sa)

return b, storage
}

Expand Down Expand Up @@ -839,6 +846,34 @@ func TestLoginSvcAcctNamespaceSelector(t *testing.T) {
}
}

func TestLoginEntityAliasCustomMetadataAssignment(t *testing.T) {
b, storage := setupBackend(t, defaultTestBackendConfig())

data := map[string]interface{}{
"role": "plugin-test",
"jwt": jwtGoodDataToken,
}

req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: data,
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}

if !reflect.DeepEqual(resp.Auth.Alias.CustomMetadata, testMetadataAnnotations) {
t.Fatalf("expected %#v, got %#v", testMetadataAnnotations, resp.Auth.Alias.CustomMetadata)
}
}

func TestAliasLookAhead(t *testing.T) {
testCases := map[string]struct {
role string
Expand Down
108 changes: 108 additions & 0 deletions service_account_getter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package kubeauth

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

v1 "k8s.io/api/core/v1"
kubeerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

const annotationKeyPrefix = "auth-metadata.vault.hashicorp.com/"

// serviceAccountGetter defines a namespace validator interface
type serviceAccountGetter interface {
annotations(context.Context, *http.Client, string, string) (map[string]string, error)
}

type serviceAccountGetterFactory func(*kubeConfig) serviceAccountGetter

// serviceAccountGetterWrapper implements the serviceAccountGetter interface
type serviceAccountGetterWrapper struct {
config *kubeConfig
}

func newServiceAccountGetterWrapper(config *kubeConfig) serviceAccountGetter {
return &serviceAccountGetterWrapper{
config: config,
}
}

func (w *serviceAccountGetterWrapper) annotations(ctx context.Context, client *http.Client, namespace, serviceAccount string) (map[string]string, error) {
url := fmt.Sprintf("%s/api/v1/namespaces/%s/serviceaccounts/%s",
strings.TrimSuffix(w.config.Host, "/"), namespace, serviceAccount)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}

// Use the configured TokenReviewer JWT as the bearer
if w.config.TokenReviewerJWT == "" {
return nil, errors.New("service account lookup failed: TokenReviewer JWT needs to be configured to retrieve service accounts")
}
setRequestHeader(req, fmt.Sprintf("Bearer %s", w.config.TokenReviewerJWT))

resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
var errStatus metav1.Status
if err = json.Unmarshal(body, &errStatus); err != nil {
return nil, fmt.Errorf("failed to parse error status on service account retrieval failure err=%s", err)
}

if errStatus.Status != metav1.StatusSuccess {
return nil, fmt.Errorf("failed to get service account (code %d status %s)",
resp.StatusCode, kubeerrors.FromObject(runtime.Object(&errStatus)))
}
}
var sa v1.ServiceAccount
err = json.Unmarshal(body, &sa)
if err != nil {
return nil, err
}

annotations := map[string]string{}
for k, v := range sa.Annotations {
if strings.HasPrefix(k, annotationKeyPrefix) {
newK := strings.TrimPrefix(k, annotationKeyPrefix)
annotations[newK] = v
}
}
return annotations, nil
}

type mockServiceAccountGetter struct {
meta metav1.ObjectMeta
}

func mockServiceAccountGetterFactory(meta metav1.ObjectMeta) serviceAccountGetterFactory {
return func(config *kubeConfig) serviceAccountGetter {
return &mockServiceAccountGetter{
meta: meta,
}
}
}

func (v *mockServiceAccountGetter) annotations(context.Context, *http.Client, string, string) (map[string]string, error) {
annotations := map[string]string{}
for k, v := range v.meta.Annotations {
if strings.HasPrefix(k, annotationKeyPrefix) {
newK := strings.TrimPrefix(k, annotationKeyPrefix)
annotations[newK] = v
}
}
return annotations, nil
}

0 comments on commit 19556d7

Please sign in to comment.