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

feat: optional audiences field on token create #24

Merged
merged 5 commits into from
Mar 10, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Unreleased

Features:
* add `audiences` option to set audiences for the k8s token created from the TokenRequest API, and add `token_default_audiences`
option to set the default audiences on role write [GH-24](https://github.com/hashicorp/vault-plugin-secrets-kubernetes/pull/24)


### IMPROVEMENTS:

* enable plugin multiplexing [GH-23](https://github.com/hashicorp/vault-plugin-secrets-kubernetes/pull/23)
Expand Down
3 changes: 2 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ func newClient(config *kubeConfig) (*client, error) {
return &client{k8sClient}, nil
}

func (c *client) createToken(ctx context.Context, namespace, name string, ttl time.Duration) (*authenticationv1.TokenRequestStatus, error) {
func (c *client) createToken(ctx context.Context, namespace, name string, ttl time.Duration, audiences []string) (*authenticationv1.TokenRequestStatus, error) {
intTTL := int64(ttl.Seconds())
resp, err := c.k8s.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, name, &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: &intTTL,
Audiences: audiences,
},
}, metav1.CreateOptions{})
if err != nil {
Expand Down
80 changes: 80 additions & 0 deletions integrationtest/creds_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,81 @@ func TestCreds_ttl(t *testing.T) {
}
}

// Test token audiences handling and defaults
func TestCreds_audiences(t *testing.T) {
// Pick up VAULT_ADDR and VAULT_TOKEN from env vars
client, err := api.NewClient(nil)
if err != nil {
t.Fatal(err)
}

path, umount := mountHelper(t, client)
defer umount()
client, delNamespace := namespaceHelper(t, client)
defer delNamespace()

// create default config
_, err = client.Logical().Write(path+"/config", map[string]interface{}{})
require.NoError(t, err)

type testCase struct {
roleConfig map[string]interface{}
credsConfig map[string]interface{}
expectedAudiences []interface{}
}

tests := map[string]testCase{
"both set": {
roleConfig: map[string]interface{}{
"allowed_kubernetes_namespaces": []string{"*"},
"service_account_name": "sample-app",
"token_default_audiences": []string{"foo", "bar"},
},
credsConfig: map[string]interface{}{
"kubernetes_namespace": "test",
"audiences": "baz,qux",
},
expectedAudiences: []interface{}{"baz", "qux"},
},
"default to token_default_audiences": {
roleConfig: map[string]interface{}{
"allowed_kubernetes_namespaces": []string{"*"},
"service_account_name": "sample-app",
"token_default_audiences": []string{"foo", "bar"},
},
credsConfig: map[string]interface{}{
"kubernetes_namespace": "test",
},
expectedAudiences: []interface{}{"foo", "bar"},
},
"default to audiences of k8s cluster default if both not set": {
roleConfig: map[string]interface{}{
"allowed_kubernetes_namespaces": []string{"*"},
"service_account_name": "sample-app",
},
credsConfig: map[string]interface{}{
"kubernetes_namespace": "test",
},
expectedAudiences: []interface{}{"https://kubernetes.default.svc.cluster.local"},
},
}
i := 0
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
roleName := fmt.Sprintf("testrole-%d", i)
_, err = client.Logical().Write(path+"/roles/"+roleName, tc.roleConfig)
assert.NoError(t, err)

creds, err := client.Logical().Write(path+"/creds/"+roleName, tc.credsConfig)
assert.NoError(t, err)
require.NotNil(t, creds)

testK8sTokenAudiences(t, tc.expectedAudiences, creds.Data["service_account_token"].(string))
})
i = i + 1
}
}

func TestCreds_service_account_name(t *testing.T) {
// Pick up VAULT_ADDR and VAULT_TOKEN from env vars
client, err := api.NewClient(nil)
Expand Down Expand Up @@ -164,6 +239,7 @@ func TestCreds_service_account_name(t *testing.T) {
"service_account_name": "sample-app",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": nil,
}, roleResponse.Data)

result1, err := client.Logical().Write(path+"/creds/testrole", map[string]interface{}{
Expand Down Expand Up @@ -241,6 +317,7 @@ func TestCreds_kubernetes_role_name(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": nil,
}
testRoleType(t, client, path, roleConfig, expectedRoleResponse)
})
Expand Down Expand Up @@ -275,6 +352,7 @@ func TestCreds_kubernetes_role_name(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": nil,
}
testClusterRoleType(t, client, path, roleConfig, expectedRoleResponse)
})
Expand Down Expand Up @@ -344,6 +422,7 @@ func TestCreds_generated_role_rules(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": nil,
}
testRoleType(t, client, path, roleConfig, expectedRoleResponse)
})
Expand Down Expand Up @@ -379,6 +458,7 @@ func TestCreds_generated_role_rules(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": nil,
}
testClusterRoleType(t, client, path, roleConfig, expectedRoleResponse)
})
Expand Down
10 changes: 10 additions & 0 deletions integrationtest/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,16 @@ func testK8sTokenTTL(t *testing.T, expectedSec int, token string) {
assert.Equal(t, expectedSec, int(exp-iat))
}

func testK8sTokenAudiences(t *testing.T, expectedAudiences []interface{}, token string) {
parsed, err := josejwt.ParseSigned(token)
require.NoError(t, err)
claims := map[string]interface{}{}
err = parsed.UnsafeClaimsWithoutVerification(&claims)
require.NoError(t, err)
aud := claims["aud"].([]interface{})
assert.ElementsMatch(t, expectedAudiences, aud)
}

func combineMaps(maps ...map[string]string) map[string]string {
newMap := make(map[string]string)
for _, m := range maps {
Expand Down
5 changes: 5 additions & 0 deletions integrationtest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func TestRole(t *testing.T) {
"generated_role_rules": sampleRules,
"token_default_ttl": "1h",
"token_max_ttl": "24h",
"token_default_audiences": []string{"foobar"},
})
assert.NoError(t, err)

Expand All @@ -180,6 +181,7 @@ func TestRole(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": []interface{}{"foobar"},
}, result.Data)

// update
Expand All @@ -188,6 +190,7 @@ func TestRole(t *testing.T) {
"extra_annotations": sampleExtraAnnotations,
"extra_labels": sampleExtraLabels,
"token_default_ttl": "30m",
"token_default_audiences": []string{"bar"},
})

result, err = client.Logical().Read(path + "/roles/testrole")
Expand All @@ -205,6 +208,7 @@ func TestRole(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": thirtyMinutes,
"token_default_audiences": []interface{}{"bar"},
}, result.Data)

// update again
Expand All @@ -228,6 +232,7 @@ func TestRole(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": thirtyMinutes,
"token_default_audiences": []interface{}{"bar"},
}, result.Data)

result, err = client.Logical().List(path + "/roles")
Expand Down
4 changes: 4 additions & 0 deletions integrationtest/wal_rollback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func TestCreds_wal_rollback(t *testing.T) {
"kubernetes_role_type": "RolE",
"token_default_ttl": "1h",
"token_max_ttl": "24h",
"token_default_audiences": []string{"foobar"},
}
expectedRoleResponse := map[string]interface{}{
"allowed_kubernetes_namespaces": []interface{}{"test"},
Expand All @@ -80,6 +81,7 @@ func TestCreds_wal_rollback(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": []interface{}{"foobar"},
}

_, err := client.Logical().Write(mountPath+"/roles/walrole", roleConfig)
Expand Down Expand Up @@ -138,6 +140,7 @@ func TestCreds_wal_rollback(t *testing.T) {
"kubernetes_role_type": "ClusterRole",
"token_default_ttl": "1h",
"token_max_ttl": "24h",
"token_default_audiences": []string{"foobar"},
}
expectedRoleResponse := map[string]interface{}{
"allowed_kubernetes_namespaces": interface{}(nil),
Expand All @@ -152,6 +155,7 @@ func TestCreds_wal_rollback(t *testing.T) {
"service_account_name": "",
"token_max_ttl": oneDay,
"token_default_ttl": oneHour,
"token_default_audiences": []interface{}{"foobar"},
}

_, err := client.Logical().Write(mountPath+"/roles/walrolebinding", roleConfig)
Expand Down
21 changes: 18 additions & 3 deletions path_creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type credsRequest struct {
ClusterRoleBinding bool `json:"cluster_role_binding"`
TTL time.Duration `json:"ttl"`
RoleName string `json:"role_name"`
Audiences []string `json:"audiences"`
}

// The fields in nameMetadata are used for templated name generation
Expand Down Expand Up @@ -73,6 +74,10 @@ func (b *backend) pathCredentials() *framework.Path {
Type: framework.TypeDurationSecond,
Description: "The TTL of the generated credentials",
},
"audiences": {
Type: framework.TypeCommaStringSlice,
Description: "The intended audiences of the generated credentials",
},
},

HelpSynopsis: pathCredsHelpSyn,
Expand Down Expand Up @@ -112,6 +117,11 @@ func (b *backend) pathCredentialsRead(ctx context.Context, req *logical.Request,
request.TTL = time.Duration(ttlRaw.(int)) * time.Second
}

audiences, ok := d.Get("audiences").([]string)
if ok {
request.Audiences = audiences
}

// Validate the request
isValidNs, err := b.isValidKubernetesNamespace(ctx, req, request.Namespace, roleEntry)
if err != nil {
Expand Down Expand Up @@ -206,6 +216,11 @@ func (b *backend) createCreds(ctx context.Context, req *logical.Request, role *r
theTTL = b.System().MaxLeaseTTL()
}

theAudiences := role.TokenDefaultAudiences
if len(reqPayload.Audiences) != 0 {
theAudiences = reqPayload.Audiences
}

// These are created items to save internally and/or return to the caller
token := ""
serviceAccountName := ""
Expand All @@ -218,7 +233,7 @@ func (b *backend) createCreds(ctx context.Context, req *logical.Request, role *r
switch {
case role.ServiceAccountName != "":
// Create token for existing service account
status, err := client.createToken(ctx, reqPayload.Namespace, role.ServiceAccountName, theTTL)
status, err := client.createToken(ctx, reqPayload.Namespace, role.ServiceAccountName, theTTL, theAudiences)
if err != nil {
return nil, fmt.Errorf("failed to create a service account token for %s/%s: %s", reqPayload.Namespace, role.ServiceAccountName, err)
}
Expand All @@ -240,7 +255,7 @@ func (b *backend) createCreds(ctx context.Context, req *logical.Request, role *r
return nil, err
}

status, err := client.createToken(ctx, reqPayload.Namespace, genName, theTTL)
status, err := client.createToken(ctx, reqPayload.Namespace, genName, theTTL, theAudiences)
if err != nil {
return nil, fmt.Errorf("failed to create a service account token for %s/%s: %s", reqPayload.Namespace, genName, err)
}
Expand All @@ -267,7 +282,7 @@ func (b *backend) createCreds(ctx context.Context, req *logical.Request, role *r
return nil, err
}

status, err := client.createToken(ctx, reqPayload.Namespace, genName, theTTL)
status, err := client.createToken(ctx, reqPayload.Namespace, genName, theTTL, theAudiences)
if err != nil {
return nil, fmt.Errorf("failed to create a service account token for %s/%s: %s", reqPayload.Namespace, genName, err)
}
Expand Down
33 changes: 21 additions & 12 deletions path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ const (
)

type roleEntry struct {
Name string `json:"name" mapstructure:"name"`
K8sNamespaces []string `json:"allowed_kubernetes_namespaces" mapstructure:"allowed_kubernetes_namespaces"`
K8sNamespaceSelector string `json:"allowed_kubernetes_namespace_selector" mapstructure:"allowed_kubernetes_namespace_selector"`
TokenMaxTTL time.Duration `json:"token_max_ttl" mapstructure:"token_max_ttl"`
TokenDefaultTTL time.Duration `json:"token_default_ttl" mapstructure:"token_default_ttl"`
ServiceAccountName string `json:"service_account_name" mapstructure:"service_account_name"`
K8sRoleName string `json:"kubernetes_role_name" mapstructure:"kubernetes_role_name"`
K8sRoleType string `json:"kubernetes_role_type" mapstructure:"kubernetes_role_type"`
RoleRules string `json:"generated_role_rules" mapstructure:"generated_role_rules"`
NameTemplate string `json:"name_template" mapstructure:"name_template"`
ExtraLabels map[string]string `json:"extra_labels" mapstructure:"extra_labels"`
ExtraAnnotations map[string]string `json:"extra_annotations" mapstructure:"extra_annotations"`
Name string `json:"name" mapstructure:"name"`
K8sNamespaces []string `json:"allowed_kubernetes_namespaces" mapstructure:"allowed_kubernetes_namespaces"`
K8sNamespaceSelector string `json:"allowed_kubernetes_namespace_selector" mapstructure:"allowed_kubernetes_namespace_selector"`
TokenMaxTTL time.Duration `json:"token_max_ttl" mapstructure:"token_max_ttl"`
TokenDefaultTTL time.Duration `json:"token_default_ttl" mapstructure:"token_default_ttl"`
TokenDefaultAudiences []string `json:"token_default_audiences" mapstructure:"token_default_audiences"`
ServiceAccountName string `json:"service_account_name" mapstructure:"service_account_name"`
K8sRoleName string `json:"kubernetes_role_name" mapstructure:"kubernetes_role_name"`
K8sRoleType string `json:"kubernetes_role_type" mapstructure:"kubernetes_role_type"`
RoleRules string `json:"generated_role_rules" mapstructure:"generated_role_rules"`
NameTemplate string `json:"name_template" mapstructure:"name_template"`
ExtraLabels map[string]string `json:"extra_labels" mapstructure:"extra_labels"`
ExtraAnnotations map[string]string `json:"extra_annotations" mapstructure:"extra_annotations"`
}

func (r *roleEntry) toResponseData() (map[string]interface{}, error) {
Expand Down Expand Up @@ -78,6 +79,11 @@ func (b *backend) pathRoles() []*framework.Path {
Description: "The default ttl for generated Kubernetes service account tokens. If not set or set to 0, will use system default.",
Required: false,
},
"token_default_audiences": {
Type: framework.TypeCommaStringSlice,
Description: "The default audiences for generated Kubernetes service account tokens. If not set or set to \"\", will use k8s cluster default.",
Required: false,
},
"service_account_name": {
Type: framework.TypeString,
Description: "The pre-existing service account to generate tokens for. Mutually exclusive with all role parameters. If set, only a Kubernetes service account token will be created.",
Expand Down Expand Up @@ -206,6 +212,9 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f
if tokenTTLRaw, ok := d.GetOk("token_default_ttl"); ok {
entry.TokenDefaultTTL = time.Duration(tokenTTLRaw.(int)) * time.Second
}
if tokenAudiencesRaw, ok := d.GetOk("token_default_audiences"); ok {
entry.TokenDefaultAudiences = strutil.RemoveDuplicates(tokenAudiencesRaw.([]string), false)
}
if svcAccount, ok := d.GetOk("service_account_name"); ok {
entry.ServiceAccountName = svcAccount.(string)
}
Expand Down
Loading