Skip to content

Commit b56d954

Browse files
Merge pull request openshift#8688 from r4f4/aws-iam-role-perms
OCPBUGS-36390: aws: do not require create permissions when BYO IAM role
2 parents dcc5d86 + 5de0dd4 commit b56d954

File tree

4 files changed

+783
-64
lines changed

4 files changed

+783
-64
lines changed

pkg/asset/cluster/aws/aws.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77

88
"github.com/aws/aws-sdk-go/aws"
99
"github.com/aws/aws-sdk-go/service/ec2"
10+
"github.com/aws/aws-sdk-go/service/iam"
1011
"github.com/aws/aws-sdk-go/service/route53"
1112
"github.com/pkg/errors"
13+
"github.com/sirupsen/logrus"
14+
"k8s.io/apimachinery/pkg/util/sets"
1215

1316
"github.com/openshift/installer/pkg/asset/installconfig"
1417
awsic "github.com/openshift/installer/pkg/asset/installconfig/aws"
@@ -34,12 +37,11 @@ func Metadata(clusterID, infraID string, config *types.InstallConfig) *awstypes.
3437
// PreTerraform performs any infrastructure initialization which must
3538
// happen before Terraform creates the remaining infrastructure.
3639
func PreTerraform(ctx context.Context, clusterID string, installConfig *installconfig.InstallConfig) error {
37-
3840
if err := tagSharedVPCResources(ctx, clusterID, installConfig); err != nil {
3941
return err
4042
}
4143

42-
return nil
44+
return tagSharedIAMRoles(ctx, clusterID, installConfig)
4345
}
4446

4547
func tagSharedVPCResources(ctx context.Context, clusterID string, installConfig *installconfig.InstallConfig) error {
@@ -95,6 +97,64 @@ func tagSharedVPCResources(ctx context.Context, clusterID string, installConfig
9597
return nil
9698
}
9799

100+
func tagSharedIAMRoles(ctx context.Context, clusterID string, installConfig *installconfig.InstallConfig) error {
101+
iamRoles := sets.New[string]()
102+
{
103+
mpool := awstypes.MachinePool{}
104+
mpool.Set(installConfig.Config.AWS.DefaultMachinePlatform)
105+
if mp := installConfig.Config.ControlPlane; mp != nil {
106+
mpool.Set(mp.Platform.AWS)
107+
}
108+
if len(mpool.IAMRole) > 0 {
109+
iamRoles.Insert(mpool.IAMRole)
110+
}
111+
}
112+
113+
for _, compute := range installConfig.Config.Compute {
114+
mpool := awstypes.MachinePool{}
115+
mpool.Set(installConfig.Config.AWS.DefaultMachinePlatform)
116+
mpool.Set(compute.Platform.AWS)
117+
if len(mpool.IAMRole) > 0 {
118+
iamRoles.Insert(mpool.IAMRole)
119+
}
120+
}
121+
122+
// If compute stanza was not defined, it will inherit from DefaultMachinePlatform later on.
123+
if installConfig.Config.Compute == nil {
124+
mpool := installConfig.Config.AWS.DefaultMachinePlatform
125+
if mpool != nil && len(mpool.IAMRole) > 0 {
126+
iamRoles.Insert(mpool.IAMRole)
127+
}
128+
}
129+
130+
if iamRoles.Len() == 0 {
131+
return nil
132+
}
133+
134+
logrus.Debugf("Tagging shared instance roles: %v", sets.List(iamRoles))
135+
136+
session, err := installConfig.AWS.Session(ctx)
137+
if err != nil {
138+
return fmt.Errorf("could not create AWS session: %w", err)
139+
}
140+
141+
tagKey, tagValue := sharedTag(clusterID)
142+
143+
iamClient := iam.New(session, aws.NewConfig().WithRegion(installConfig.Config.Platform.AWS.Region))
144+
for role := range iamRoles {
145+
if _, err := iamClient.TagRoleWithContext(ctx, &iam.TagRoleInput{
146+
RoleName: aws.String(role),
147+
Tags: []*iam.Tag{
148+
{Key: aws.String(tagKey), Value: aws.String(tagValue)},
149+
},
150+
}); err != nil {
151+
return fmt.Errorf("could not tag %q instance role: %w", role, err)
152+
}
153+
}
154+
155+
return nil
156+
}
157+
98158
func sharedTag(clusterID string) (string, string) {
99159
return fmt.Sprintf("kubernetes.io/cluster/%s", clusterID), "shared"
100160
}

pkg/asset/installconfig/aws/permissions.go

Lines changed: 145 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77

88
"github.com/aws/aws-sdk-go/aws/session"
99
"github.com/sirupsen/logrus"
10+
"k8s.io/apimachinery/pkg/util/sets"
1011

1112
ccaws "github.com/openshift/cloud-credential-operator/pkg/aws"
13+
"github.com/openshift/installer/pkg/types"
14+
"github.com/openshift/installer/pkg/types/aws"
1215
)
1316

1417
// PermissionGroup is the group of permissions needed by cluster creation, operation, or teardown.
@@ -30,6 +33,9 @@ const (
3033
// PermissionDeleteSharedNetworking is a set of permissions required when the installer destroys resources from a shared-network cluster.
3134
PermissionDeleteSharedNetworking PermissionGroup = "delete-shared-networking"
3235

36+
// PermissionCreateInstanceRole is a set of permissions required when the installer creates instance roles.
37+
PermissionCreateInstanceRole PermissionGroup = "create-instance-role"
38+
3339
// PermissionDeleteSharedInstanceRole is a set of permissions required when the installer destroys resources from a
3440
// cluster with user-supplied IAM roles for instances.
3541
PermissionDeleteSharedInstanceRole PermissionGroup = "delete-shared-instance-role"
@@ -129,10 +135,7 @@ var permissions = map[PermissionGroup][]string{
129135
// IAM related perms
130136
"iam:AddRoleToInstanceProfile",
131137
"iam:CreateInstanceProfile",
132-
"iam:CreateRole",
133138
"iam:DeleteInstanceProfile",
134-
"iam:DeleteRole",
135-
"iam:DeleteRolePolicy",
136139
"iam:GetInstanceProfile",
137140
"iam:GetRole",
138141
"iam:GetRolePolicy",
@@ -141,7 +144,6 @@ var permissions = map[PermissionGroup][]string{
141144
"iam:ListRoles",
142145
"iam:ListUsers",
143146
"iam:PassRole",
144-
"iam:PutRolePolicy",
145147
"iam:RemoveRoleFromInstanceProfile",
146148
"iam:SimulatePrincipalPolicy",
147149
"iam:TagInstanceProfile",
@@ -246,6 +248,13 @@ var permissions = map[PermissionGroup][]string{
246248
PermissionDeleteSharedNetworking: {
247249
"tag:UnTagResources",
248250
},
251+
// Permissions required for creating an instance role
252+
PermissionCreateInstanceRole: {
253+
"iam:CreateRole",
254+
"iam:DeleteRole",
255+
"iam:DeleteRolePolicy",
256+
"iam:PutRolePolicy",
257+
},
249258
// Permissions required for deleting a cluster with shared instance roles
250259
PermissionDeleteSharedInstanceRole: {
251260
"iam:UntagRole",
@@ -285,14 +294,9 @@ var permissions = map[PermissionGroup][]string{
285294
// as either capable of creating new credentials for components that interact with the cloud or
286295
// being able to be passed through as-is to the components that need cloud credentials
287296
func ValidateCreds(ssn *session.Session, groups []PermissionGroup, region string) error {
288-
// Compile a list of permissions based on the permission groups provided
289-
requiredPermissions := []string{}
290-
for _, group := range groups {
291-
groupPerms, ok := permissions[group]
292-
if !ok {
293-
return fmt.Errorf("unable to access permissions group %s", group)
294-
}
295-
requiredPermissions = append(requiredPermissions, groupPerms...)
297+
requiredPermissions, err := PermissionsList(groups)
298+
if err != nil {
299+
return err
296300
}
297301

298302
client := ccaws.NewClientFromSession(ssn)
@@ -332,3 +336,132 @@ func ValidateCreds(ssn *session.Session, groups []PermissionGroup, region string
332336

333337
return errors.New("AWS credentials cannot be used to either create new creds or use as-is")
334338
}
339+
340+
// RequiredPermissionGroups returns a set of required permissions for a given cluster configuration.
341+
func RequiredPermissionGroups(ic *types.InstallConfig) []PermissionGroup {
342+
permissionGroups := []PermissionGroup{PermissionCreateBase}
343+
usingExistingVPC := len(ic.AWS.Subnets) != 0
344+
usingExistingPrivateZone := len(ic.AWS.HostedZone) != 0
345+
346+
if !usingExistingVPC {
347+
permissionGroups = append(permissionGroups, PermissionCreateNetworking)
348+
}
349+
350+
if !usingExistingPrivateZone {
351+
permissionGroups = append(permissionGroups, PermissionCreateHostedZone)
352+
}
353+
354+
if includesKMSEncryptionKey(ic) {
355+
logrus.Debugf("Adding %s to the group of permissions", PermissionKMSEncryptionKeys)
356+
permissionGroups = append(permissionGroups, PermissionKMSEncryptionKeys)
357+
}
358+
359+
// Add delete permissions for non-C2S installs.
360+
if !aws.IsSecretRegion(ic.AWS.Region) {
361+
permissionGroups = append(permissionGroups, PermissionDeleteBase)
362+
if usingExistingVPC {
363+
permissionGroups = append(permissionGroups, PermissionDeleteSharedNetworking)
364+
} else {
365+
permissionGroups = append(permissionGroups, PermissionDeleteNetworking)
366+
}
367+
if !usingExistingPrivateZone {
368+
permissionGroups = append(permissionGroups, PermissionDeleteHostedZone)
369+
}
370+
}
371+
372+
if ic.AWS.PublicIpv4Pool != "" {
373+
permissionGroups = append(permissionGroups, PermissionPublicIpv4Pool)
374+
}
375+
376+
if !ic.AWS.BestEffortDeleteIgnition {
377+
permissionGroups = append(permissionGroups, PermissionDeleteIgnitionObjects)
378+
}
379+
380+
if includesCreateInstanceRole(ic) {
381+
permissionGroups = append(permissionGroups, PermissionCreateInstanceRole)
382+
}
383+
384+
if includesExistingInstanceRole(ic) {
385+
permissionGroups = append(permissionGroups, PermissionDeleteSharedInstanceRole)
386+
}
387+
388+
return permissionGroups
389+
}
390+
391+
// PermissionsList compiles a list of permissions based on the permission groups provided.
392+
func PermissionsList(required []PermissionGroup) ([]string, error) {
393+
requiredPermissions := sets.New[string]()
394+
for _, group := range required {
395+
groupPerms, ok := permissions[group]
396+
if !ok {
397+
return nil, fmt.Errorf("unable to access permissions group %s", group)
398+
}
399+
requiredPermissions.Insert(groupPerms...)
400+
}
401+
402+
return sets.List(requiredPermissions), nil
403+
}
404+
405+
// includesExistingInstanceRole checks if at least one BYO instance role is included in the install-config.
406+
func includesExistingInstanceRole(installConfig *types.InstallConfig) bool {
407+
mpool := aws.MachinePool{}
408+
mpool.Set(installConfig.AWS.DefaultMachinePlatform)
409+
410+
if mp := installConfig.ControlPlane; mp != nil {
411+
mpool.Set(mp.Platform.AWS)
412+
}
413+
414+
for _, compute := range installConfig.Compute {
415+
mpool.Set(compute.Platform.AWS)
416+
}
417+
418+
return len(mpool.IAMRole) > 0
419+
}
420+
421+
// includesCreateInstanceRole checks if at least one instance role will be created by the installer.
422+
func includesCreateInstanceRole(installConfig *types.InstallConfig) bool {
423+
{
424+
mpool := aws.MachinePool{}
425+
mpool.Set(installConfig.AWS.DefaultMachinePlatform)
426+
if mp := installConfig.ControlPlane; mp != nil {
427+
mpool.Set(mp.Platform.AWS)
428+
}
429+
if len(mpool.IAMRole) == 0 {
430+
return true
431+
}
432+
}
433+
434+
for _, compute := range installConfig.Compute {
435+
mpool := aws.MachinePool{}
436+
mpool.Set(installConfig.AWS.DefaultMachinePlatform)
437+
mpool.Set(compute.Platform.AWS)
438+
if len(mpool.IAMRole) == 0 {
439+
return true
440+
}
441+
}
442+
443+
if len(installConfig.Compute) > 0 {
444+
return false
445+
}
446+
447+
// If compute stanza is not defined, we know it'll inherit the value from DefaultMachinePlatform
448+
mpool := aws.MachinePool{}
449+
mpool.Set(installConfig.AWS.DefaultMachinePlatform)
450+
return len(mpool.IAMRole) == 0
451+
}
452+
453+
// includesKMSEncryptionKey checks if any KMS encryption keys are included in the install-config.
454+
func includesKMSEncryptionKey(installConfig *types.InstallConfig) bool {
455+
mpool := aws.MachinePool{}
456+
mpool.Set(installConfig.AWS.DefaultMachinePlatform)
457+
458+
if mp := installConfig.ControlPlane; mp != nil {
459+
mpool.Set(mp.Platform.AWS)
460+
}
461+
462+
for _, compute := range installConfig.Compute {
463+
mpool.Set(compute.Platform.AWS)
464+
}
465+
466+
return len(mpool.KMSKeyARN) > 0
467+
}

0 commit comments

Comments
 (0)