diff --git a/docs/pages/includes/role-spec.mdx b/docs/pages/includes/role-spec.mdx index 57b3f402f652e..c6279477f6905 100644 --- a/docs/pages/includes/role-spec.mdx +++ b/docs/pages/includes/role-spec.mdx @@ -282,8 +282,11 @@ spec: # workload_identity_labels: a user/bot with this role will be allowed to # issue Workload Identities with labels matching below. + # + # Supports role templating with traits. workload_identity_labels: 'env': 'prod' + 'team': '{{external.team}}' # node_labels_expression has the same purpose as node_labels but # supports predicate expressions to configure custom logic. diff --git a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go index 833f9b87322a0..0485ee0713519 100644 --- a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go +++ b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go @@ -419,6 +419,34 @@ func TestIssueWorkloadIdentity(t *testing.T) { specificAccessClient, err := tp.srv.NewClient(auth.TestUser(specificAccess.GetName())) require.NoError(t, err) + traitAccess, _, err := auth.CreateUserAndRole( + tp.srv.Auth(), + "traity", + []string{}, + []types.Rule{ + types.NewRule( + types.KindWorkloadIdentity, + []string{types.VerbRead, types.VerbList}, + ), + }, + auth.WithUserMutator(func(user types.User) { + tr := user.GetTraits() + if tr == nil { + tr = map[string][]string{} + } + tr["custom"] = []string{"trait-value-a", "trait-value-b"} + user.SetTraits(tr) + }), + auth.WithRoleMutator(func(role types.Role) { + role.SetWorkloadIdentityLabels(types.Allow, types.Labels{ + "trait-label": []string{"{{external.custom}}"}, + }) + }), + ) + require.NoError(t, err) + traitAccessClient, err := tp.srv.NewClient(auth.TestUser(traitAccess.GetName())) + require.NoError(t, err) + // Generate a keypair to generate x509 SVIDs for. workloadKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) require.NoError(t, err) @@ -556,6 +584,23 @@ func TestIssueWorkloadIdentity(t *testing.T) { }) require.NoError(t, err) + traitsRequired, err := tp.srv.Auth().CreateWorkloadIdentity(ctx, &workloadidentityv1pb.WorkloadIdentity{ + Kind: types.KindWorkloadIdentity, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "traits-required", + Labels: map[string]string{ + "trait-label": "trait-value-b", + }, + }, + Spec: &workloadidentityv1pb.WorkloadIdentitySpec{ + Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{ + Id: "/foo", + }, + }, + }) + require.NoError(t, err) + for policy, result := range map[string]error{ "foo": errors.New("missing artifact signature"), "bar": nil, @@ -968,6 +1013,23 @@ func TestIssueWorkloadIdentity(t *testing.T) { require.Equal(t, cert.NotAfter.Sub(cert.NotBefore), wantTTL+time.Minute) }, }, + { + name: "x509 svid - access via traits in labels", + client: traitAccessClient, + req: &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: traitsRequired.GetMetadata().GetName(), + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_X509SvidParams{ + X509SvidParams: &workloadidentityv1pb.X509SVIDParams{ + PublicKey: workloadKeyPubBytes, + }, + }, + WorkloadAttrs: workloadAttrs(nil), + }, + requireErr: require.NoError, + assert: func(t *testing.T, res *workloadidentityv1pb.IssueWorkloadIdentityResponse) { + require.NotNil(t, res.Credential) + }, + }, { name: "x509 svid - unspecified ttl", client: wilcardAccessClient, diff --git a/lib/services/role.go b/lib/services/role.go index 130cedc5a7a7e..4ee3918bd1763 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -564,6 +564,7 @@ func ApplyTraits(r types.Role, traits map[string][]string) (types.Role, error) { types.KindWindowsDesktop, types.KindUserGroup, types.KindSAMLIdPServiceProvider, + types.KindWorkloadIdentity, } { labelMatchers, err := r.GetLabelMatchers(condition, kind) if err != nil {