-
Notifications
You must be signed in to change notification settings - Fork 63
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
thyton
merged 24 commits into
main
from
VAULT-1665-add-custom-metadata-from-k8s-annotations
Jan 22, 2024
Merged
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 9254c8f
scaffold integ test for custom metadata assignment
thyton aebf670
add integ test for custom metadata assignment
thyton f7406ae
modify integ test
thyton 13c9804
remove debug log
thyton c034548
check if entity-alias ccanonical_id matches the auth's entity id
thyton 514d16c
make fmt
thyton 0ef1def
new config param use_annotations_as_alias_metadata
thyton 9225cc5
add use_annotations_as_alias_metadata to config read test
thyton 6adb285
update annotation prefix
thyton ab94fc1
use templated policy in alias metadata integ test
thyton 61df232
update the field use_annotations_as_alias_metadata description
thyton 54c4c93
reformat comment
thyton 696a0e5
Update path_config.go
thyton 6da318a
Update integrationtest/integration_test.go
thyton de4554d
Update integrationtest/integration_test.go
thyton 2d95ed4
Update integrationtest/integration_test.go
thyton c12fb4e
move use_annotations_as_alias_metadata to kubeConfig and refactor int…
thyton df0251b
add a feature entry in CHANGELOG
thyton 0ae3054
reformat
thyton 6a5d10d
Update path_login.go
thyton 9f7ca43
revert accidental change
thyton 6fd5426
Merge branch 'VAULT-1665-add-custom-metadata-from-k8s-annotations' of…
thyton 46e2540
integ test case for using reserved alias metadata keys
thyton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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) | ||
|
@@ -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", | ||
|
@@ -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() { | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_, 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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
|
@@ -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), | ||
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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), | ||
|
@@ -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" | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
createPolicy(t, policyNameFoo, | ||
fmt.Sprintf(`path "%s/{{identity.entity.aliases.%s.metadata.key-1}}" | ||
{ capabilities = [ "read", "update", "create" ] }`, kvPath, mountAccessor)) | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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{}{ | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"role": "test-role", | ||
"jwt": createToken(t, "vault", nil), | ||
|
@@ -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() | ||
thyton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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), | ||
|
@@ -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", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 inkubeAuthBackend
matches the lifecycle of the plugin process, but this value is tied to the most recent write to the config endpoint instead.There was a problem hiding this comment.
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!