Skip to content

Commit

Permalink
feat: add audiences option on token create and token_default_audience…
Browse files Browse the repository at this point in the history
…s option on role create (openbao#24)
  • Loading branch information
thyton committed Mar 10, 2023
1 parent 1f51056 commit fb49b9f
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 16 deletions.
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

0 comments on commit fb49b9f

Please sign in to comment.