Skip to content

Commit d965e8e

Browse files
authored
Allow wildcard matching of service account subject (#8629)
* Allow wildcard matching for service account subject * Add subjectPattern property to iamserviceaccounts.md * Remove misleading comment in iamserviceaccount.go
1 parent 4ca1fa2 commit d965e8e

10 files changed

Lines changed: 215 additions & 1 deletion

File tree

pkg/apis/eksctl.io/v1alpha5/assets/schema.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,11 @@
975975
"status": {
976976
"$ref": "#/definitions/ClusterIAMServiceAccountStatus"
977977
},
978+
"subjectPattern": {
979+
"type": "string",
980+
"description": "Subject pattern to use in the trust policy condition. When set, this pattern is used instead of the service account name, and StringLike is used instead of StringEquals to allow wildcard matching.",
981+
"x-intellij-html-description": "Subject pattern to use in the trust policy condition. When set, this pattern is used instead of the service account name, and StringLike is used instead of StringEquals to allow wildcard matching."
982+
},
978983
"tags": {
979984
"additionalProperties": {
980985
"type": "string"
@@ -998,7 +1003,8 @@
9981003
"status",
9991004
"roleName",
10001005
"roleOnly",
1001-
"tags"
1006+
"tags",
1007+
"subjectPattern"
10021008
],
10031009
"additionalProperties": false,
10041010
"description": "holds an IAM service account metadata and configuration",

pkg/apis/eksctl.io/v1alpha5/iam.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ type ClusterIAMServiceAccount struct {
124124
// AWS tags for the service account
125125
// +optional
126126
Tags map[string]string `json:"tags,omitempty"`
127+
128+
// Subject pattern to use in the trust policy condition. When set, this pattern is used
129+
// instead of the service account name, and StringLike is used instead of StringEquals
130+
// to allow wildcard matching.
131+
// +optional
132+
SubjectPattern string `json:"subjectPattern,omitempty"`
127133
}
128134

129135
// ClusterIAMServiceAccountStatus holds status of the IAM service account

pkg/cfn/builder/iam.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ func NewIAMRoleResourceSetForServiceAccount(spec *api.ClusterIAMServiceAccount,
344344
wellKnownPolicies: spec.WellKnownPolicies,
345345
roleName: spec.RoleName,
346346
permissionsBoundary: spec.PermissionsBoundary,
347+
subjectPattern: spec.SubjectPattern,
347348
description: fmt.Sprintf(
348349
"IAM role for serviceaccount %q %s",
349350
spec.NameString(),
@@ -427,6 +428,7 @@ type IAMRoleResourceSet struct {
427428
namespace string
428429
permissionsBoundary string
429430
description string
431+
subjectPattern string
430432
}
431433

432434
// NewIAMRoleResourceSetWithAttachPolicyARNs builds IAM Role stack from the give spec
@@ -525,6 +527,9 @@ func (rs *IAMRoleResourceSet) makeAssumeRolePolicyDocument() cft.MapOfInterfaces
525527
}
526528
if rs.serviceAccount != "" && rs.namespace != "" {
527529
logger.Debug("service account location provided: %s/%s, adding sub condition", api.AWSNodeMeta.Namespace, api.AWSNodeMeta.Name)
530+
if rs.subjectPattern != "" {
531+
return rs.oidc.MakeAssumeRolePolicyDocumentWithServiceAccountConditionsAllowingWildcard(rs.namespace, rs.subjectPattern)
532+
}
528533
return rs.oidc.MakeAssumeRolePolicyDocumentWithServiceAccountConditions(rs.namespace, rs.serviceAccount)
529534
}
530535
return rs.oidc.MakeAssumeRolePolicyDocument()

pkg/cfn/builder/iam_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,42 @@ var _ = Describe("template builder for IAM", func() {
254254
Expect(t).To(HaveOutputWithValue(outputs.IAMServiceAccountRoleName, `{ "Fn::GetAtt": "Role1.Arn" }`))
255255
})
256256

257+
It("can construct an iamserviceaccount addon template with subject pattern using wildcards", func() {
258+
serviceAccount := &api.ClusterIAMServiceAccount{}
259+
260+
serviceAccount.Name = "sa-1"
261+
serviceAccount.SubjectPattern = "app-*"
262+
263+
serviceAccount.AttachPolicyARNs = []string{"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"}
264+
265+
appendServiceAccountToClusterConfig(cfg, serviceAccount)
266+
267+
rs := builder.NewIAMRoleResourceSetForServiceAccount(serviceAccount, oidc)
268+
269+
templateBody := []byte{}
270+
271+
Expect(rs).To(RenderWithoutErrors(&templateBody))
272+
273+
t := cft.NewTemplate()
274+
275+
Expect(t).To(LoadBytesWithoutErrors(templateBody))
276+
277+
Expect(t.Description).To(Equal("IAM role for serviceaccount \"default/sa-1\" [created and managed by eksctl]"))
278+
279+
Expect(t.Resources).To(HaveLen(1))
280+
Expect(t.Outputs).To(HaveLen(1))
281+
282+
Expect(t).To(HaveResource(outputs.IAMServiceAccountRoleName, "AWS::IAM::Role"))
283+
284+
// Verify that the assume role policy uses StringLike for subject pattern
285+
Expect(t).To(HaveResourceWithPropertyValue(outputs.IAMServiceAccountRoleName, "AssumeRolePolicyDocument", expectedServiceAccountAssumeRolePolicyDocumentWithWildcard))
286+
Expect(t).To(HaveResourceWithPropertyValue(outputs.IAMServiceAccountRoleName, "ManagedPolicyArns", `[
287+
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
288+
]`))
289+
290+
Expect(t).To(HaveOutputWithValue(outputs.IAMServiceAccountRoleName, `{ "Fn::GetAtt": "Role1.Arn" }`))
291+
})
292+
257293
It("can construct an iamserviceaccount addon template with all the wellKnownPolicies", func() {
258294
serviceAccount := &api.ClusterIAMServiceAccount{}
259295

@@ -450,6 +486,29 @@ const expectedServiceAccountAssumeRolePolicyDocument = `{
450486
"Version": "2012-10-17"
451487
}`
452488

489+
const expectedServiceAccountAssumeRolePolicyDocumentWithWildcard = `{
490+
"Statement": [
491+
{
492+
"Action": [
493+
"sts:AssumeRoleWithWebIdentity"
494+
],
495+
"Condition": {
496+
"StringEquals": {
497+
"oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E:aud": "sts.amazonaws.com"
498+
},
499+
"StringLike": {
500+
"oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E:sub": "system:serviceaccount:default:app-*"
501+
}
502+
},
503+
"Effect": "Allow",
504+
"Principal": {
505+
"Federated": "arn:aws:iam::456123987123:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E"
506+
}
507+
}
508+
],
509+
"Version": "2012-10-17"
510+
}`
511+
453512
const expectedAssumeRolePolicyDocument = `{
454513
"Statement": [
455514
{

pkg/ctl/cmdutils/configfile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ func NewCreateIAMServiceAccountLoader(cmd *Cmd, saFilter *filter.IAMServiceAccou
868868

869869
l.flagsIncompatibleWithConfigFile.Insert(
870870
"policy-arn",
871+
"subject-pattern",
871872
)
872873

873874
l.validateWithConfigFile = func() error {

pkg/ctl/cmdutils/configfile_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,59 @@ var _ = Describe("cmdutils configfile", func() {
649649
})
650650
})
651651

652+
Describe("NewCreateIAMServiceAccountLoader", func() {
653+
When("subject-pattern flag is used with config file", func() {
654+
It("should return an error", func() {
655+
cfg := api.NewClusterConfig()
656+
cobraCmd := newCmd()
657+
cobraCmd.Flags().String("subject-pattern", "", "")
658+
cobraCmd.Flags().String("cluster", "", "")
659+
Expect(cobraCmd.ParseFlags([]string{"--subject-pattern", "app-*"})).To(Succeed())
660+
661+
cmd := &Cmd{
662+
ClusterConfig: cfg,
663+
CobraCommand: cobraCmd,
664+
ClusterConfigFile: examplesDir + "01-simple-cluster.yaml",
665+
ProviderConfig: api.ProviderConfig{},
666+
}
667+
668+
err := NewCreateIAMServiceAccountLoader(cmd, filter.NewIAMServiceAccountFilter()).Load()
669+
Expect(err).To(HaveOccurred())
670+
Expect(err.Error()).To(ContainSubstring("--subject-pattern"))
671+
Expect(err.Error()).To(ContainSubstring("cannot use --subject-pattern when --config-file/-f is set"))
672+
})
673+
})
674+
675+
When("subject-pattern flag is used without config file", func() {
676+
It("should succeed", func() {
677+
cfg := api.NewClusterConfig()
678+
cfg.Metadata.Name = "test-cluster"
679+
serviceAccount := &api.ClusterIAMServiceAccount{
680+
ClusterIAMMeta: api.ClusterIAMMeta{
681+
Name: "test-sa",
682+
Namespace: "default",
683+
},
684+
AttachPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"},
685+
SubjectPattern: "app-*",
686+
}
687+
cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{serviceAccount}
688+
689+
cobraCmd := newCmd()
690+
cobraCmd.Flags().String("cluster", "", "")
691+
cobraCmd.Flags().String("subject-pattern", "", "")
692+
693+
cmd := &Cmd{
694+
ClusterConfig: cfg,
695+
CobraCommand: cobraCmd,
696+
ProviderConfig: api.ProviderConfig{},
697+
}
698+
699+
err := NewCreateIAMServiceAccountLoader(cmd, filter.NewIAMServiceAccountFilter()).Load()
700+
Expect(err).NotTo(HaveOccurred())
701+
})
702+
})
703+
})
704+
652705
Context("makeManagedNodegroup with node repair config", func() {
653706
var (
654707
ng *api.NodeGroup

pkg/ctl/create/iamserviceaccount.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ func createIAMServiceAccountCmdWithRunFunc(cmd *cmdutils.Cmd, runFunc func(cmd *
5757

5858
fs.BoolVar(&overrideExistingServiceAccounts, "override-existing-serviceaccounts", false, "create IAM roles for existing serviceaccounts and update the serviceaccount")
5959

60+
fs.StringVar(&serviceAccount.SubjectPattern, "subject-pattern", "", "subject pattern to use in the trust policy (supports wildcards like '*' with StringLike condition)")
61+
6062
cmdutils.AddIAMServiceAccountFilterFlags(fs, &cmd.Include, &cmd.Exclude)
6163
cmdutils.AddApproveFlag(fs, cmd)
6264
cmdutils.AddRegionFlag(fs, &cmd.ProviderConfig)

pkg/ctl/create/iamserviceaccount_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ var _ = Describe("create iamserviceaccount", func() {
3030
Entry("with optional flags", "--cluster", "clusterName", "--name", "serviceAccountName", "--attach-policy-arn", "dummyPolicyArn", "--override-existing-serviceaccounts", "--role-name", "custom-role-name"),
3131
)
3232

33+
DescribeTable("create service account with subject pattern",
34+
func(args ...string) {
35+
commandArgs := append([]string{"iamserviceaccount"}, args...)
36+
cmd := newMockEmptyCmd(commandArgs...)
37+
count := 0
38+
cmdutils.AddResourceCmd(cmdutils.NewGrouping(), cmd.parentCmd, func(cmd *cmdutils.Cmd) {
39+
createIAMServiceAccountCmdWithRunFunc(cmd, func(cmd *cmdutils.Cmd, _, _ bool) error {
40+
Expect(cmd.ClusterConfig.Metadata.Name).To(Equal("clusterName"))
41+
Expect(cmd.ClusterConfig.IAM.ServiceAccounts[0].Name).To(Equal("serviceAccountName"))
42+
Expect(cmd.ClusterConfig.IAM.ServiceAccounts[0].SubjectPattern).To(Equal("app-*"))
43+
Expect(cmd.ClusterConfig.IAM.ServiceAccounts[0].AttachPolicyARNs).To(ContainElement("dummyPolicyArn"))
44+
count++
45+
return nil
46+
})
47+
})
48+
_, err := cmd.execute()
49+
Expect(err).To(Not(HaveOccurred()))
50+
Expect(count).To(Equal(1))
51+
},
52+
Entry("with subject-pattern flag", "--cluster", "clusterName", "--name", "serviceAccountName", "--attach-policy-arn", "dummyPolicyArn", "--subject-pattern", "app-*"),
53+
)
54+
3355
DescribeTable("invalid flags or arguments",
3456
func(c invalidParamsCase) {
3557
cmd := newDefaultCmd(c.args...)

pkg/iam/oidc/api.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,21 @@ func (m *OpenIDConnectManager) MakeAssumeRolePolicyDocumentWithServiceAccountCon
180180
})
181181
}
182182

183+
// MakeAssumeRolePolicyDocumentWithServiceAccountConditionsAllowingWildcard constructs a trust policy document
184+
// that allows wildcard pattern matching in the subject condition. The subjectPattern should be in the format
185+
// "system:serviceaccount:namespace:name-pattern" where name-pattern can include wildcards like "*".
186+
func (m *OpenIDConnectManager) MakeAssumeRolePolicyDocumentWithServiceAccountConditionsAllowingWildcard(serviceAccountNamespace, subjectPattern string) cft.MapOfInterfaces {
187+
subject := fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccountNamespace, subjectPattern)
188+
return cft.MakeAssumeRoleWithWebIdentityPolicyDocument(m.ProviderARN, cft.MapOfInterfaces{
189+
"StringLike": map[string]string{
190+
m.hostnameAndPath() + ":sub": subject,
191+
},
192+
"StringEquals": map[string]string{
193+
m.hostnameAndPath() + ":aud": m.audience,
194+
},
195+
})
196+
}
197+
183198
func (m *OpenIDConnectManager) MakeAssumeRolePolicyDocument() cft.MapOfInterfaces {
184199
return cft.MakeAssumeRoleWithWebIdentityPolicyDocument(m.ProviderARN, cft.MapOfInterfaces{
185200
"StringEquals": map[string]string{

userdocs/src/usage/iamserviceaccounts.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,50 @@ To update a service accounts roles permissions you can run `eksctl update iamser
9191
???+ note
9292
`eksctl delete iamserviceaccount` deletes Kubernetes `ServiceAccounts` even if they were not created by `eksctl`.
9393

94+
#### Using wildcard patterns with `--subject-pattern`
95+
96+
When you need to grant IAM permissions to multiple service accounts that follow a naming pattern, you can use the `--subject-pattern` flag to create an IAM role that trusts service accounts matching a wildcard pattern.
97+
98+
This is useful for scenarios such as:
99+
- Multiple deployment replicas with dynamic service account names
100+
- Applications that create service accounts with predictable prefixes
101+
- Multi-tenant environments where service accounts share a naming convention
102+
103+
When using `--subject-pattern`, the IAM trust policy will use the `StringLike` condition operator instead of `StringEquals`, allowing wildcards like `*` to match multiple service accounts:
104+
105+
```console
106+
eksctl create iamserviceaccount \
107+
--cluster=<clusterName> \
108+
--name=<serviceAccountName> \
109+
--namespace=<serviceAccountNamespace> \
110+
--attach-policy-arn=<policyARN> \
111+
--subject-pattern="app-*"
112+
```
113+
114+
For example, to allow all service accounts starting with `app-` in the `default` namespace to assume the role:
115+
116+
```console
117+
eksctl create iamserviceaccount \
118+
--cluster=<clusterName> \
119+
--name=app-base \
120+
--namespace=default \
121+
--attach-policy-arn=arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
122+
--subject-pattern="app-*"
123+
```
124+
125+
This creates an IAM trust policy condition like:
126+
127+
```json
128+
"StringLike": {
129+
"oidc.eks.region.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:app-*"
130+
}
131+
```
132+
133+
???+ warning "Security Considerations"
134+
Use wildcard patterns carefully. A broad pattern like `*` would allow any service account in the namespace to assume the IAM role. Always use the most restrictive pattern possible for your use case.
135+
136+
The Subject Pattern property can be defined in the configuration file.
137+
94138
### Usage with config files
95139

96140
To manage `iamserviceaccounts` using config file, you will be looking to set `iam.withOIDC: true` and list account you want under `iam.serviceAccount`.
@@ -140,6 +184,7 @@ iam:
140184
tags:
141185
Owner: "John Doe"
142186
Team: "Some Team"
187+
subjectPattern: "app-*"
143188
- metadata:
144189
name: cache-access
145190
namespace: backend-apps

0 commit comments

Comments
 (0)