From d7a10b75169c2664420271bb5daaf717119a2726 Mon Sep 17 00:00:00 2001 From: Andrew Ballman Date: Wed, 25 Mar 2020 11:08:52 -0700 Subject: [PATCH] terraform target now checks publicIP for allocation ID Generate new clientset after bastion model updated enable FS kops state storage iam/s3 permissions Grab second to last token glog to klog IAM S3 Path Generated clientset: make apimachinery One more fix for iamS3Path Fix bucket issue --- pkg/model/iam/iam_builder.go | 247 ++++++++++++--------- upup/pkg/fi/cloudup/awstasks/elastic_ip.go | 51 ++++- 2 files changed, 182 insertions(+), 116 deletions(-) diff --git a/pkg/model/iam/iam_builder.go b/pkg/model/iam/iam_builder.go index ec22bd6dde948..de846bd2e8e89 100644 --- a/pkg/model/iam/iam_builder.go +++ b/pkg/model/iam/iam_builder.go @@ -272,6 +272,119 @@ func (b *PolicyBuilder) IAMPrefix() string { } } +func (b *PolicyBuilder) buildAWSS3Policy(p *Policy, bucket string, key string) *Policy { + iamS3Path := bucket + "/" + key + iamS3Path = strings.TrimSuffix(iamS3Path, "/") + + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Of( + "s3:GetBucketLocation", + "s3:GetEncryptionConfiguration", + "s3:ListBucket", + "s3:ListBucketVersions", + ), + Resource: stringorslice.Slice([]string{ + strings.Join([]string{b.IAMPrefix(), ":s3:::", bucket}, ""), + }), + }) + + if b.Cluster.Spec.IAM.Legacy { + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:*"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), + ), + }) + } else { + if b.Role == kops.InstanceGroupRoleMaster { + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:Get*"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), + ), + }) + } else if b.Role == kops.InstanceGroupRoleNode { + resources := []string{ + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/addons/*"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/cluster.spec"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/config"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/instancegroup/*"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/issued/*"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kube-proxy/*"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/ssh/*"}, ""), + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/secrets/dockerconfig"}, ""), + } + + // @check if bootstrap tokens are enabled and if so enable access to client certificate + if b.UseBootstrapTokens() { + resources = append(resources, strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/node-authorizer-client/*"}, "")) + } else { + resources = append(resources, strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kubelet/*"}, "")) + } + + sort.Strings(resources) + + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:Get*"}), + Resource: stringorslice.Of(resources...), + }) + + networkingSpec := b.Cluster.Spec.Networking + + if networkingSpec != nil { + // @check if kuberoute is enabled and permit access to the private key + if networkingSpec.Kuberouter != nil { + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:Get*"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kube-router/*"}, ""), + ), + }) + } + + // @check if calico is enabled as the CNI provider and permit access to the client TLS certificate by default + if networkingSpec.Calico != nil { + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:Get*"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/calico-client/*"}, ""), + ), + }) + } + + // @check if cilium is enabled as the CNI provider and permit access to the cilium etc client TLS certificate by default + // As long as the Cilium Etcd cluster exists, we should do this + ciliumEtcd := false + + for _, cluster := range b.Cluster.Spec.EtcdClusters { + if cluster.Name == "cilium" { + ciliumEtcd = true + break + } + } + + if networkingSpec.Cilium != nil && ciliumEtcd { + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:Get*"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/etcd-clients-ca-cilium/*"}, ""), + ), + }) + } + } + } + } + + return p +} + // AddS3Permissions updates an IAM Policy with statements granting tailored // access to S3 assets, depending on the instance group role func (b *PolicyBuilder) AddS3Permissions(p *Policy) (*Policy, error) { @@ -323,117 +436,19 @@ func (b *PolicyBuilder) AddS3Permissions(p *Policy) (*Policy, error) { } if s3Path, ok := vfsPath.(*vfs.S3Path); ok { - iamS3Path := s3Path.Bucket() + "/" + s3Path.Key() - iamS3Path = strings.TrimSuffix(iamS3Path, "/") - - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Of( - "s3:GetBucketLocation", - "s3:GetEncryptionConfiguration", - "s3:ListBucket", - "s3:ListBucketVersions", - ), - Resource: stringorslice.Slice([]string{ - strings.Join([]string{b.IAMPrefix(), ":s3:::", s3Path.Bucket()}, ""), - }), - }) - - if b.Cluster.Spec.IAM.Legacy { - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:*"}), - Resource: stringorslice.Of( - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), - ), - }) - } else { - if b.Role == kops.InstanceGroupRoleMaster { - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:Get*"}), - Resource: stringorslice.Of( - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), - ), - }) - } else if b.Role == kops.InstanceGroupRoleNode { - resources := []string{ - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/addons/*"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/cluster.spec"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/config"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/instancegroup/*"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/issued/*"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kube-proxy/*"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/ssh/*"}, ""), - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/secrets/dockerconfig"}, ""), - } - - // @check if bootstrap tokens are enabled and if so enable access to client certificate - if b.UseBootstrapTokens() { - resources = append(resources, strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/node-authorizer-client/*"}, "")) - } else { - resources = append(resources, strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kubelet/*"}, "")) - } - - sort.Strings(resources) - - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:Get*"}), - Resource: stringorslice.Of(resources...), - }) - - networkingSpec := b.Cluster.Spec.Networking - - if networkingSpec != nil { - // @check if kuberoute is enabled and permit access to the private key - if networkingSpec.Kuberouter != nil { - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:Get*"}), - Resource: stringorslice.Of( - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/kube-router/*"}, ""), - ), - }) - } - - // @check if calico is enabled as the CNI provider and permit access to the client TLS certificate by default - if networkingSpec.Calico != nil { - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:Get*"}), - Resource: stringorslice.Of( - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/calico-client/*"}, ""), - ), - }) - } - - // @check if cilium is enabled as the CNI provider and permit access to the cilium etc client TLS certificate by default - // As long as the Cilium Etcd cluster exists, we should do this - ciliumEtcd := false - - for _, cluster := range b.Cluster.Spec.EtcdClusters { - if cluster.Name == "cilium" { - ciliumEtcd = true - break - } - } - - if networkingSpec.Cilium != nil && ciliumEtcd { - p.Statement = append(p.Statement, &Statement{ - Effect: StatementEffectAllow, - Action: stringorslice.Slice([]string{"s3:Get*"}), - Resource: stringorslice.Of( - strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/pki/private/etcd-clients-ca-cilium/*"}, ""), - ), - }) - } - } - } - } + p = b.buildAWSS3Policy(p, s3Path.Bucket(), s3Path.Key()) } else if _, ok := vfsPath.(*vfs.MemFSPath); ok { // Tests -ignore - nothing we can do in terms of IAM policy klog.Warningf("ignoring memfs path %q for IAM policy builder", vfsPath) + } else if vfs, ok := vfsPath.(*vfs.FSPath); ok { + if b.Cluster.Spec.CloudProvider == string(kops.CloudProviderAWS) { + splitPath := strings.Split(strings.TrimSuffix(vfs.Path(), "/"), "/") + bucket := splitPath[len(splitPath)-2] + + p = b.buildAWSS3Policy(p, bucket, vfs.Base()) + } else { + return nil, fmt.Errorf("path is not cluster readable: %v", root) + } } else { // We could implement this approach, but it seems better to // get all clouds using cluster-readable storage @@ -463,6 +478,22 @@ func (b *PolicyBuilder) AddS3Permissions(p *Policy) (*Policy, error) { strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), ), }) + } else if vfs, ok := vfsPath.(*vfs.FSPath); ok { + if b.Cluster.Spec.CloudProvider == string(kops.CloudProviderAWS) { + // Tests -ignore - nothing we can do in terms of IAM policy + splitPath := strings.Split(strings.TrimSuffix(vfs.Path(), "/"), "/") + + bucket := strings.TrimSuffix(splitPath[len(splitPath)-2], "/") + iamS3Path := strings.Join([]string{bucket, vfs.Base()}, "/") + + p.Statement = append(p.Statement, &Statement{ + Effect: StatementEffectAllow, + Action: stringorslice.Slice([]string{"s3:GetObject", "s3:DeleteObject", "s3:PutObject"}), + Resource: stringorslice.Of( + strings.Join([]string{b.IAMPrefix(), ":s3:::", iamS3Path, "/*"}, ""), + ), + }) + } } else { klog.Warningf("unknown writeable path, can't apply IAM policy: %q", vfsPath) } diff --git a/upup/pkg/fi/cloudup/awstasks/elastic_ip.go b/upup/pkg/fi/cloudup/awstasks/elastic_ip.go index 0a0491d5bfb94..4b5593460e93d 100644 --- a/upup/pkg/fi/cloudup/awstasks/elastic_ip.go +++ b/upup/pkg/fi/cloudup/awstasks/elastic_ip.go @@ -36,8 +36,9 @@ type ElasticIP struct { Name *string Lifecycle *fi.Lifecycle - ID *string - PublicIP *string + ID *string + PublicIP *string + AllocationID *string // ElasticIPs don't support tags. We instead find it via a related resource. @@ -284,17 +285,51 @@ type terraformElasticIP struct { Tags map[string]string `json:"tags,omitempty" cty:"tags"` } -func (_ *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error { - tf := &terraformElasticIP{ - VPC: aws.Bool(true), - Tags: e.Tags, +func (e *ElasticIP) FindAllocationID(publicIP *string, cloud awsup.AWSCloud) (*string, error) { + if publicIP == nil { + return nil, fmt.Errorf("cannot find allocation ID without ElasticIP publicIP") } - return t.RenderResource("aws_eip", *e.Name, tf) + request := &ec2.DescribeAddressesInput{Filters: []*ec2.Filter{awsup.NewEC2Filter("public-ip", *publicIP)}} + response, err := cloud.EC2().DescribeAddresses(request) + if err != nil { + return nil, fmt.Errorf("error listing ElasticIPs: %v", err) + } + + if response == nil || len(response.Addresses) == 0 { + return nil, nil + } + + if len(response.Addresses) != 1 { + return nil, fmt.Errorf("found multiple ElasticIPs for: %v", e) + } + + klog.V(2).Infof("Found ElasticIP Allocation ID for EIP: %q - %q", *publicIP, response.Addresses[0].AllocationId) + return response.Addresses[0].AllocationId, nil +} + +func (eip *ElasticIP) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ElasticIP) error { + if e.PublicIP == nil { + tf := &terraformElasticIP{ + VPC: aws.Bool(true), + Tags: e.Tags, + } + + return t.RenderResource("aws_eip", *e.Name, tf) + } + + e.AllocationID, _ = e.FindAllocationID(e.PublicIP, t.Cloud.(awsup.AWSCloud)) + return nil } func (e *ElasticIP) TerraformLink() *terraform.Literal { - return terraform.LiteralProperty("aws_eip", *e.Name, "id") + if e.PublicIP == nil { + return terraform.LiteralProperty("aws_eip", *e.Name, "id") + } else if e.AllocationID != nil { + return terraform.LiteralFromStringValue(*e.AllocationID) + } else { + return nil + } } type cloudformationElasticIP struct {