diff --git a/k8s/crds/kops.k8s.io_clusters.yaml b/k8s/crds/kops.k8s.io_clusters.yaml index e172c3050a..27fea3238f 100644 --- a/k8s/crds/kops.k8s.io_clusters.yaml +++ b/k8s/crds/kops.k8s.io_clusters.yaml @@ -889,6 +889,8 @@ spec: type: boolean legacy: type: boolean + permissionsBoundary: + type: string required: - legacy type: object diff --git a/pkg/apis/kops/cluster.go b/pkg/apis/kops/cluster.go index f2e070539b..30781f6c1a 100644 --- a/pkg/apis/kops/cluster.go +++ b/pkg/apis/kops/cluster.go @@ -252,8 +252,9 @@ type Assets struct { // IAMSpec adds control over the IAM security policies applied to resources type IAMSpec struct { // TODO: remove Legacy in next APIVersion - Legacy bool `json:"legacy"` - AllowContainerRegistry bool `json:"allowContainerRegistry,omitempty"` + Legacy bool `json:"legacy"` + AllowContainerRegistry bool `json:"allowContainerRegistry,omitempty"` + PermissionsBoundary *string `json:"permissionsBoundary,omitempty"` } // HookSpec is a definition hook diff --git a/pkg/apis/kops/v1alpha2/cluster.go b/pkg/apis/kops/v1alpha2/cluster.go index 78e31e6070..1e0cdaf9f8 100644 --- a/pkg/apis/kops/v1alpha2/cluster.go +++ b/pkg/apis/kops/v1alpha2/cluster.go @@ -249,8 +249,9 @@ type Assets struct { // IAMSpec adds control over the IAM security policies applied to resources type IAMSpec struct { - Legacy bool `json:"legacy"` - AllowContainerRegistry bool `json:"allowContainerRegistry,omitempty"` + Legacy bool `json:"legacy"` + AllowContainerRegistry bool `json:"allowContainerRegistry,omitempty"` + PermissionsBoundary *string `json:"permissionsBoundary,omitempty"` } // HookSpec is a definition hook diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index a3cc5d323a..549d6c3509 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -3203,6 +3203,7 @@ func Convert_kops_IAMProfileSpec_To_v1alpha2_IAMProfileSpec(in *kops.IAMProfileS func autoConvert_v1alpha2_IAMSpec_To_kops_IAMSpec(in *IAMSpec, out *kops.IAMSpec, s conversion.Scope) error { out.Legacy = in.Legacy out.AllowContainerRegistry = in.AllowContainerRegistry + out.PermissionsBoundary = in.PermissionsBoundary return nil } @@ -3214,6 +3215,7 @@ func Convert_v1alpha2_IAMSpec_To_kops_IAMSpec(in *IAMSpec, out *kops.IAMSpec, s func autoConvert_kops_IAMSpec_To_v1alpha2_IAMSpec(in *kops.IAMSpec, out *IAMSpec, s conversion.Scope) error { out.Legacy = in.Legacy out.AllowContainerRegistry = in.AllowContainerRegistry + out.PermissionsBoundary = in.PermissionsBoundary return nil } diff --git a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go index 9aed37a7e5..397dd1bd56 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go @@ -836,7 +836,7 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { if in.IAM != nil { in, out := &in.IAM, &out.IAM *out = new(IAMSpec) - **out = **in + (*in).DeepCopyInto(*out) } if in.EncryptionConfig != nil { in, out := &in.EncryptionConfig, &out.EncryptionConfig @@ -1581,6 +1581,11 @@ func (in *IAMProfileSpec) DeepCopy() *IAMProfileSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IAMSpec) DeepCopyInto(out *IAMSpec) { *out = *in + if in.PermissionsBoundary != nil { + in, out := &in.PermissionsBoundary, &out.PermissionsBoundary + *out = new(string) + **out = **in + } return } diff --git a/pkg/apis/kops/zz_generated.deepcopy.go b/pkg/apis/kops/zz_generated.deepcopy.go index 7bbfb22d31..5d4b3c30d5 100644 --- a/pkg/apis/kops/zz_generated.deepcopy.go +++ b/pkg/apis/kops/zz_generated.deepcopy.go @@ -936,7 +936,7 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { if in.IAM != nil { in, out := &in.IAM, &out.IAM *out = new(IAMSpec) - **out = **in + (*in).DeepCopyInto(*out) } if in.EncryptionConfig != nil { in, out := &in.EncryptionConfig, &out.EncryptionConfig @@ -1747,6 +1747,11 @@ func (in *IAMProfileSpec) DeepCopy() *IAMProfileSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IAMSpec) DeepCopyInto(out *IAMSpec) { *out = *in + if in.PermissionsBoundary != nil { + in, out := &in.PermissionsBoundary, &out.PermissionsBoundary + *out = new(string) + **out = **in + } return } diff --git a/pkg/model/iam.go b/pkg/model/iam.go index ae6fdce0a1..bef02fae1c 100644 --- a/pkg/model/iam.go +++ b/pkg/model/iam.go @@ -110,6 +110,11 @@ func (b *IAMModelBuilder) buildIAMTasks(igRole kops.InstanceGroupRole, iamName s RolePolicyDocument: fi.WrapResource(rolePolicy), ExportWithID: s(strings.ToLower(string(igRole)) + "s"), } + + if b.Cluster.Spec.IAM != nil && b.Cluster.Spec.IAM.PermissionsBoundary != nil { + iamRole.PermissionsBoundary = b.Cluster.Spec.IAM.PermissionsBoundary + } + c.AddTask(iamRole) } diff --git a/tests/integration/update_cluster/complex/cloudformation.json b/tests/integration/update_cluster/complex/cloudformation.json index fddbb10634..6288c9c3a5 100644 --- a/tests/integration/update_cluster/complex/cloudformation.json +++ b/tests/integration/update_cluster/complex/cloudformation.json @@ -1363,7 +1363,8 @@ } ], "Version": "2012-10-17" - } + }, + "PermissionsBoundary": "arn:aws:iam:00000000000:policy/boundaries" } }, "AWSIAMRolenodescomplexexamplecom": { @@ -1381,7 +1382,8 @@ } ], "Version": "2012-10-17" - } + }, + "PermissionsBoundary": "arn:aws:iam:00000000000:policy/boundaries" } }, "AWSRoute53RecordSetapicomplexexamplecom": { diff --git a/tests/integration/update_cluster/complex/in-legacy-v1alpha2.yaml b/tests/integration/update_cluster/complex/in-legacy-v1alpha2.yaml index 9016b21b7d..b976067dfa 100644 --- a/tests/integration/update_cluster/complex/in-legacy-v1alpha2.yaml +++ b/tests/integration/update_cluster/complex/in-legacy-v1alpha2.yaml @@ -29,7 +29,8 @@ spec: - instanceGroup: master-us-test-1a name: us-test-1a name: events - iam: {} + iam: + permissionsBoundary: arn:aws:iam:00000000000:policy/boundaries kubeAPIServer: serviceNodePortRange: 28000-32767 auditWebhookBatchThrottleQps: 3.14 diff --git a/tests/integration/update_cluster/complex/in-v1alpha2.yaml b/tests/integration/update_cluster/complex/in-v1alpha2.yaml index 68a0ae9583..279bb8705e 100644 --- a/tests/integration/update_cluster/complex/in-v1alpha2.yaml +++ b/tests/integration/update_cluster/complex/in-v1alpha2.yaml @@ -29,7 +29,8 @@ spec: - instanceGroup: master-us-test-1a name: us-test-1a name: events - iam: {} + iam: + permissionsBoundary: arn:aws:iam:00000000000:policy/boundaries kubeAPIServer: serviceNodePortRange: 28000-32767 auditWebhookBatchThrottleQps: 3.14 diff --git a/tests/integration/update_cluster/complex/kubernetes.tf b/tests/integration/update_cluster/complex/kubernetes.tf index 1b1b7466e9..ab2a130c3b 100644 --- a/tests/integration/update_cluster/complex/kubernetes.tf +++ b/tests/integration/update_cluster/complex/kubernetes.tf @@ -266,13 +266,15 @@ resource "aws_iam_role_policy" "nodes-complex-example-com" { } resource "aws_iam_role" "masters-complex-example-com" { - assume_role_policy = file("${path.module}/data/aws_iam_role_masters.complex.example.com_policy") - name = "masters.complex.example.com" + assume_role_policy = file("${path.module}/data/aws_iam_role_masters.complex.example.com_policy") + name = "masters.complex.example.com" + permissions_boundary = "arn:aws:iam:00000000000:policy/boundaries" } resource "aws_iam_role" "nodes-complex-example-com" { - assume_role_policy = file("${path.module}/data/aws_iam_role_nodes.complex.example.com_policy") - name = "nodes.complex.example.com" + assume_role_policy = file("${path.module}/data/aws_iam_role_nodes.complex.example.com_policy") + name = "nodes.complex.example.com" + permissions_boundary = "arn:aws:iam:00000000000:policy/boundaries" } resource "aws_internet_gateway" "complex-example-com" { diff --git a/upup/pkg/fi/cloudup/awstasks/iamrole.go b/upup/pkg/fi/cloudup/awstasks/iamrole.go index bd31506d21..c9f0cb31a4 100644 --- a/upup/pkg/fi/cloudup/awstasks/iamrole.go +++ b/upup/pkg/fi/cloudup/awstasks/iamrole.go @@ -40,8 +40,9 @@ type IAMRole struct { ID *string Lifecycle *fi.Lifecycle - Name *string - RolePolicyDocument *fi.ResourceHolder // "inline" IAM policy + Name *string + RolePolicyDocument *fi.ResourceHolder // "inline" IAM policy + PermissionsBoundary *string // ExportWithId will expose the name & ARN for reuse as part of a larger system. Only supported by terraform currently. ExportWithID *string @@ -72,6 +73,9 @@ func (e *IAMRole) Find(c *fi.Context) (*IAMRole, error) { actual := &IAMRole{} actual.ID = r.RoleId actual.Name = r.RoleName + if r.PermissionsBoundary != nil { + actual.PermissionsBoundary = r.PermissionsBoundary.PermissionsBoundaryArn + } if r.AssumeRolePolicyDocument != nil { // The AssumeRolePolicyDocument is URI encoded (?) actualPolicy := *r.AssumeRolePolicyDocument @@ -147,6 +151,10 @@ func (_ *IAMRole) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRole) error request.AssumeRolePolicyDocument = aws.String(policy) request.RoleName = e.Name + if e.PermissionsBoundary != nil { + request.PermissionsBoundary = e.PermissionsBoundary + } + response, err := t.Cloud.IAM().CreateRole(request) if err != nil { return fmt.Errorf("error creating IAMRole: %v", err) @@ -183,6 +191,31 @@ func (_ *IAMRole) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRole) error return fmt.Errorf("error updating IAMRole: %v", err) } } + if changes.PermissionsBoundary != nil { + klog.V(2).Infof("Updating IAMRole PermissionsBoundary %q", *e.Name) + + var err error + + if e.PermissionsBoundary == nil { + request := &iam.DeleteRolePermissionsBoundaryInput{} + request.RoleName = e.Name + + _, err = t.Cloud.IAM().DeleteRolePermissionsBoundary(request) + if err != nil { + return fmt.Errorf("error updating IAMRole: %v", err) + } + } else { + request := &iam.PutRolePermissionsBoundaryInput{} + request.RoleName = e.Name + request.PermissionsBoundary = e.PermissionsBoundary + + _, err = t.Cloud.IAM().PutRolePermissionsBoundary(request) + if err != nil { + return fmt.Errorf("error updating IAMRole: %v", err) + } + } + + } } // TODO: Should we use path as our tag? @@ -190,8 +223,9 @@ func (_ *IAMRole) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRole) error } type terraformIAMRole struct { - Name *string `json:"name" cty:"name"` - AssumeRolePolicy *terraform.Literal `json:"assume_role_policy" cty:"assume_role_policy"` + Name *string `json:"name" cty:"name"` + AssumeRolePolicy *terraform.Literal `json:"assume_role_policy" cty:"assume_role_policy"` + PermissionsBoundary *string `json:"permissions_boundary,omitempty" cty:"permissions_boundary"` } func (_ *IAMRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMRole) error { @@ -205,6 +239,10 @@ func (_ *IAMRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *I AssumeRolePolicy: policy, } + if e.PermissionsBoundary != nil { + tf.PermissionsBoundary = e.PermissionsBoundary + } + if fi.StringValue(e.ExportWithID) != "" { t.AddOutputVariable(*e.ExportWithID+"_role_arn", terraform.LiteralProperty("aws_iam_role", *e.Name, "arn")) t.AddOutputVariable(*e.ExportWithID+"_role_name", e.TerraformLink()) @@ -220,6 +258,7 @@ func (e *IAMRole) TerraformLink() *terraform.Literal { type cloudformationIAMRole struct { RoleName *string `json:"RoleName"` AssumeRolePolicyDocument map[string]interface{} + PermissionsBoundary *string `json:"PermissionsBoundary,omitempty"` } func (_ *IAMRole) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *IAMRole) error { @@ -239,6 +278,10 @@ func (_ *IAMRole) RenderCloudformation(t *cloudformation.CloudformationTarget, a AssumeRolePolicyDocument: data, } + if e.PermissionsBoundary != nil { + cf.PermissionsBoundary = e.PermissionsBoundary + } + return t.RenderResource("AWS::IAM::Role", *e.Name, cf) }