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

set auth alias custom metadata to service account annotations #226

Merged
merged 24 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
19556d7
set auth alias custom metadata to service account annotations
thyton Jan 2, 2024
9254c8f
scaffold integ test for custom metadata assignment
thyton Jan 3, 2024
aebf670
add integ test for custom metadata assignment
thyton Jan 8, 2024
f7406ae
modify integ test
thyton Jan 8, 2024
13c9804
remove debug log
thyton Jan 8, 2024
c034548
check if entity-alias ccanonical_id matches the auth's entity id
thyton Jan 8, 2024
514d16c
make fmt
thyton Jan 8, 2024
0ef1def
new config param use_annotations_as_alias_metadata
thyton Jan 17, 2024
9225cc5
add use_annotations_as_alias_metadata to config read test
thyton Jan 17, 2024
6adb285
update annotation prefix
thyton Jan 17, 2024
ab94fc1
use templated policy in alias metadata integ test
thyton Jan 18, 2024
61df232
update the field use_annotations_as_alias_metadata description
thyton Jan 18, 2024
54c4c93
reformat comment
thyton Jan 18, 2024
696a0e5
Update path_config.go
thyton Jan 19, 2024
6da318a
Update integrationtest/integration_test.go
thyton Jan 19, 2024
de4554d
Update integrationtest/integration_test.go
thyton Jan 19, 2024
2d95ed4
Update integrationtest/integration_test.go
thyton Jan 19, 2024
c12fb4e
move use_annotations_as_alias_metadata to kubeConfig and refactor int…
thyton Jan 19, 2024
df0251b
add a feature entry in CHANGELOG
thyton Jan 19, 2024
0ae3054
reformat
thyton Jan 19, 2024
6a5d10d
Update path_login.go
thyton Jan 19, 2024
9f7ca43
revert accidental change
thyton Jan 19, 2024
6fd5426
Merge branch 'VAULT-1665-add-custom-metadata-from-k8s-annotations' of…
thyton Jan 19, 2024
46e2540
integ test case for using reserved alias metadata keys
thyton Jan 22, 2024
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
8 changes: 8 additions & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ type kubeAuthBackend struct {

// tlsMu provides the lock for synchronizing updates to the tlsConfig.
tlsMu sync.RWMutex

// useAnnotationsAsAliasMetadata indicates that annotations, with the
// "vault.hashicorp.com/alias-metadata-" prefix, of the request JWT
// service account will be added to the alias metadata.
// Note Vault client or the token reviewer service account needs
// to be configured with the permission to get service account
// annotations from Kubernetes API
useAnnotationsAsAliasMetadata bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a member of kubeConfig instead. The lifecycle of fields in kubeAuthBackend matches the lifecycle of the plugin process, but this value is tied to the most recent write to the config endpoint instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out!

}

// Factory returns a new backend as logical.Backend.
Expand Down
198 changes: 136 additions & 62 deletions integrationtest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,38 @@ func annotateServiceAccount(t *testing.T, name string, annotations map[string]st
}
}

func setupKubernetesAuth(t *testing.T, boundServiceAccountName string, mountConfigOverride map[string]interface{}, roleConfigOverride map[string]interface{}) (*api.Client, func()) {
func createPolicy(t *testing.T, name, policy string) func() {
t.Helper()
// Pick up VAULT_ADDR and VAULT_TOKEN from env vars
client, err := api.NewClient(nil)
if err != nil {
t.Fatal(err)
}

_, err = client.Logical().Write(fmt.Sprintf("/sys/policy/%s", name), map[string]interface{}{
"policy": policy,
})
if err != nil {
t.Fatal(err)
}

cleanup := func() {
_, err = client.Logical().Delete(fmt.Sprintf("/sys/policy/%s", name))
if err != nil {
t.Fatal(err)
}
}

defer func() {
if t.Failed() {
cleanup()
}
}()

return cleanup
}

func setupKubernetesAuth(t *testing.T, mountConfigOverride map[string]interface{}) (*api.Client, func()) {
t.Helper()
// Pick up VAULT_ADDR and VAULT_TOKEN from env vars
client, err := api.NewClient(nil)
Expand Down Expand Up @@ -140,6 +171,12 @@ func setupKubernetesAuth(t *testing.T, boundServiceAccountName string, mountConf
t.Fatal(err)
}

return client, cleanup
}

func setupKubernetesAuthRole(t *testing.T, client *api.Client, boundServiceAccountName string, roleConfigOverride map[string]interface{}) {
t.Helper()

roleConfig := map[string]interface{}{
"bound_service_account_names": boundServiceAccountName,
"bound_service_account_namespaces": "test",
Expand All @@ -148,18 +185,42 @@ func setupKubernetesAuth(t *testing.T, boundServiceAccountName string, mountConf
roleConfig = roleConfigOverride
}

_, err = client.Logical().Write("auth/kubernetes/role/test-role", roleConfig)
_, err := client.Logical().Write("auth/kubernetes/role/test-role", roleConfig)
if err != nil {
t.Fatal(err)
}
}

return client, cleanup
func setupKv1Mount(t *testing.T, client *api.Client, path string) func() {
_, err := client.Logical().Write(fmt.Sprintf("/sys/mounts/%s", path), map[string]interface{}{
"type": "kv",
})
if err != nil {
t.Fatalf("Expected to enable kv secrets engine but got: %v", err)
}

cleanup := func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can probably be simplified with t.Cleanup too?

_, err = client.Logical().Delete(fmt.Sprintf("/sys/mounts/%s", path))
if err != nil {
t.Fatalf("Expected successful kv2 secrets engine mount delete but got: %v", err)
}
}

defer func() {
if t.Failed() {
cleanup()
}
}()

return cleanup
}

func TestSuccess(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, "vault", nil, nil)
client, cleanup := setupKubernetesAuth(t, nil)
defer cleanup()

setupKubernetesAuthRole(t, client, "vault", nil)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
"jwt": createToken(t, "vault", nil),
Expand All @@ -170,12 +231,14 @@ func TestSuccess(t *testing.T) {
}

func TestSuccessWithTokenReviewerJwt(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, "vault", map[string]interface{}{
client, cleanup := setupKubernetesAuth(t, map[string]interface{}{
"kubernetes_host": "https://kubernetes.default.svc.cluster.local",
"token_reviewer_jwt": createToken(t, "test-token-reviewer-account", nil),
}, nil)
})
defer cleanup()

setupKubernetesAuthRole(t, client, "vault", nil)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
"jwt": createToken(t, "vault", nil),
Expand All @@ -186,12 +249,14 @@ func TestSuccessWithTokenReviewerJwt(t *testing.T) {
}

func TestSuccessWithNamespaceLabels(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, nil)
defer cleanup()

roleConfigOverride := map[string]interface{}{
"bound_service_account_names": "vault",
"bound_service_account_namespace_selector": matchLabelsKeyValue,
}
client, cleanup := setupKubernetesAuth(t, "vault", nil, roleConfigOverride)
defer cleanup()
setupKubernetesAuthRole(t, client, "vault", roleConfigOverride)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
Expand All @@ -203,12 +268,14 @@ func TestSuccessWithNamespaceLabels(t *testing.T) {
}

func TestFailWithMismatchNamespaceLabels(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, nil)
defer cleanup()

roleConfigOverride := map[string]interface{}{
"bound_service_account_names": "vault",
"bound_service_account_namespace_selector": mismatchLabelsKeyValue,
}
client, cleanup := setupKubernetesAuth(t, "vault", nil, roleConfigOverride)
defer cleanup()
setupKubernetesAuthRole(t, client, "vault", roleConfigOverride)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
Expand All @@ -224,12 +291,14 @@ func TestFailWithMismatchNamespaceLabels(t *testing.T) {
}

func TestFailWithBadTokenReviewerJwt(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, "vault", map[string]interface{}{
client, cleanup := setupKubernetesAuth(t, map[string]interface{}{
"kubernetes_host": "https://kubernetes.default.svc.cluster.local",
"token_reviewer_jwt": badTokenReviewerJwt,
}, nil)
})
defer cleanup()

setupKubernetesAuthRole(t, client, "vault", nil)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
"jwt": createToken(t, "vault", nil),
Expand All @@ -246,20 +315,47 @@ func TestFailWithBadTokenReviewerJwt(t *testing.T) {
func TestAuthAliasMetadataAssignment(t *testing.T) {
// annotate the service account
expMetadata := map[string]string{
"foo": "bar",
"bar": "baz",
"key-1": "foo",
"key-2": "bar",
}

annotationPrefix := "auth-metadata.vault.hashicorp.com/"
const annotationPrefix = "vault.hashicorp.com/alias-metadata-"
annotations := map[string]string{}
for k, v := range expMetadata {
annotations[annotationPrefix+k] = v
}
annotateServiceAccount(t, "vault", annotations)

client, cleanup := setupKubernetesAuth(t, "vault", nil, nil)
client, cleanup := setupKubernetesAuth(t, map[string]interface{}{
"kubernetes_host": "https://kubernetes.default.svc.cluster.local",
"use_annotations_as_alias_metadata": true,
})
defer cleanup()

// create policy
secret, err := client.Logical().Read("sys/auth/kubernetes")
if err != nil {
t.Fatalf("Expected successful auth configuration GET but got: %v", err)
}

mountAccessor, ok := secret.Data["accessor"]
if !ok {
t.Fatal("Expected auth configuration GET response to have \"accessor\"")
}

const policyNameFoo = "alias-metadata-foo"
const kvPath = "root"
createPolicy(t, policyNameFoo,
fmt.Sprintf(`path "%s/{{identity.entity.aliases.%s.metadata.key-1}}"
{ capabilities = [ "read", "update", "create" ] }`, kvPath, mountAccessor))

roleConfigOverride := map[string]interface{}{
"bound_service_account_names": "vault",
"bound_service_account_namespaces": "test",
"policies": []string{"default", policyNameFoo},
}
setupKubernetesAuthRole(t, client, "vault", roleConfigOverride)

loginSecret, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
"jwt": createToken(t, "vault", nil),
Expand All @@ -268,63 +364,39 @@ func TestAuthAliasMetadataAssignment(t *testing.T) {
t.Fatalf("Expected successful login but got: %v", err)
}

// query the entity alias and match up its custom metadata
secret, err := client.Logical().List("identity/entity-alias/id")
// verify that the templated policy works by creating key value pairs at root/data/foo with the kubernetes auth token
kv1MountCleanup := setupKv1Mount(t, client, kvPath)
defer kv1MountCleanup()

token, err := loginSecret.TokenID()
if err != nil {
t.Fatalf("Expected successful entity-alias list but got: %v", err)
t.Fatalf("Expected successful token ID read but got: %v", err)
}

v, ok := secret.Data["keys"]
if !ok {
t.Fatal("Expected entity-alias LIST response to have \"keys\"")
kvClient, err := api.NewClient(nil)
if err != nil {
t.Fatal(err)
}

keys := v.([]interface{})
if len(keys) == 0 {
t.Fatal("Expected entity-alias LIST response to have non-empty \"keys\"")
kvClient.SetToken(token)
if err != nil {
t.Fatal(err)
}

metadataMatches := 0
for _, key := range keys {
// find the entity-alias that belongs to the login's entity
secret, err = client.Logical().Read(fmt.Sprintf("identity/entity-alias/id/%s", key))
if err != nil {
t.Fatalf("Expected successful entity-alias GET request but got: %v", err)
}

v, ok = secret.Data["canonical_id"]
if !ok {
t.Fatal("Expected entity-alias GET response to have \"canonical_id\"")
}

if v.(string) != loginSecret.Auth.EntityID {
continue
}

// check metadata
v, ok = secret.Data["metadata"]
if !ok {
t.Fatal("Expected entity-alias GET response to have \"metadata\"")
}

metadata := v.(map[string]interface{})
for expK, expV := range expMetadata {
if realK, ok := metadata[expK]; ok && realK.(string) == expV {
metadataMatches += 1
}
}

if len(expMetadata) != metadataMatches {
t.Fatalf("Expected %d matching key value pairs from alias metadata %#v but got: %d",
len(expMetadata), secret.Data, metadataMatches)
}
err = kvClient.KVv1(kvPath).Put(context.Background(), "foo",
map[string]interface{}{
"apiKey": "abc123",
})
if err != nil {
t.Fatalf("Expected successful kv1 PUT but got: %v", err)
}
}

func TestUnauthorizedServiceAccountErrorCode(t *testing.T) {
client, cleanup := setupKubernetesAuth(t, "badServiceAccount", nil, nil)
client, cleanup := setupKubernetesAuth(t, nil)
defer cleanup()

setupKubernetesAuthRole(t, client, "badServiceAccount", nil)

_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
"jwt": createToken(t, "vault", nil),
Expand Down Expand Up @@ -365,9 +437,11 @@ func TestAudienceValidation(t *testing.T) {
if tc.audienceConfig != "" {
roleConfig["audience"] = tc.audienceConfig
}
client, cleanup := setupKubernetesAuth(t, "vault", nil, roleConfig)
client, cleanup := setupKubernetesAuth(t, nil)
defer cleanup()

setupKubernetesAuthRole(t, client, "vault", roleConfig)

login := func(jwt string) error {
_, err := client.Logical().Write("auth/kubernetes/login", map[string]interface{}{
"role": "test-role",
Expand Down
27 changes: 20 additions & 7 deletions path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ then this plugin will use kubernetes.io/serviceaccount as the default issuer.
Name: "Disable use of local CA and service account JWT",
},
},
"use_annotations_as_alias_metadata": {
Type: framework.TypeBool,
Description: `Indicate annotations, with the "vault.hashicorp.com/alias-metadata-"
prefix, of the request JWT service account will be added to the alias metadata.
Note Vault client or the token reviewer service account needs to be configured
with the permission to get service account annotations from Kubernetes API.`,
Default: false,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Use annotations of JWT service account as alias metadata",
},
},
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand Down Expand Up @@ -122,13 +133,14 @@ func (b *kubeAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reque
// Create a map of data to be returned
resp := &logical.Response{
Data: map[string]interface{}{
"kubernetes_host": config.Host,
"kubernetes_ca_cert": config.CACert,
"pem_keys": config.PEMKeys,
"issuer": config.Issuer,
"disable_iss_validation": config.DisableISSValidation,
"disable_local_ca_jwt": config.DisableLocalCAJwt,
"token_reviewer_jwt_set": config.TokenReviewerJWT != "",
"kubernetes_host": config.Host,
"kubernetes_ca_cert": config.CACert,
"pem_keys": config.PEMKeys,
"issuer": config.Issuer,
"disable_iss_validation": config.DisableISSValidation,
"disable_local_ca_jwt": config.DisableLocalCAJwt,
"token_reviewer_jwt_set": config.TokenReviewerJWT != "",
"use_annotations_as_alias_metadata": b.useAnnotationsAsAliasMetadata,
},
}

Expand Down Expand Up @@ -189,6 +201,7 @@ func (b *kubeAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Requ
return nil, err
}

b.useAnnotationsAsAliasMetadata = data.Get("use_annotations_as_alias_metadata").(bool)
return nil, nil
}

Expand Down
Loading