/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package alimodel import ( "bytes" "encoding/json" "fmt" "io" "sort" "strings" "k8s.io/klog/v2" "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/model/iam" "k8s.io/kops/pkg/util/stringorslice" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/util/pkg/vfs" ) // PolicyDefaultVersion is the default version included in all policy documents const PolicyDefaultVersion = "1" // Policy Struct is a collection of fields that form a valid Alicloud policy document type Policy struct { Version string Statement []*Statement } // AsJSON converts the policy document to JSON format (parsable by Alicloud) func (p *Policy) AsJSON() (string, error) { j, err := json.MarshalIndent(p, "", " ") if err != nil { return "", fmt.Errorf("error marshaling policy to JSON: %v", err) } return string(j), nil } // StatementEffect is required and specifies what type of access the statement results in type StatementEffect string // StatementEffectAllow allows access for the given resources in the statement (based on conditions) const StatementEffectAllow StatementEffect = "Allow" // StatementEffectDeny allows access for the given resources in the statement (based on conditions) const StatementEffectDeny StatementEffect = "Deny" // Condition is a map of Conditions to be evaluated for a given RAM Statement type Condition map[string]interface{} // Statement is an Alicloud RAM Policy Statement Object: // https://https://help.aliyun.com/document_detail/93739.html type Statement struct { Effect StatementEffect Action stringorslice.StringOrSlice Resource stringorslice.StringOrSlice Condition Condition `json:",omitempty"` } // PolicyResource defines the PolicyBuilder and DNSZone to use when building the // RAM policy document for a given instance group role type PolicyResource struct { Builder *PolicyBuilder } var _ fi.Resource = &PolicyResource{} var _ fi.HasDependencies = &PolicyResource{} // GetDependencies adds the DNSZone task to the list of dependencies if set func (b *PolicyResource) GetDependencies(tasks map[string]fi.Task) []fi.Task { return nil } // Open produces the Alicloud RAM policy for the given role func (b *PolicyResource) Open() (io.Reader, error) { // Defensive copy before mutation pb := *b.Builder policy, err := pb.BuildAlicloudPolicy() if err != nil { return nil, fmt.Errorf("error building RAM policy: %v", err) } j, err := policy.AsJSON() if err != nil { return nil, fmt.Errorf("error building RAM policy: %v", err) } return bytes.NewReader([]byte(j)), nil } // PolicyBuilder struct defines all valid fields to be used when building the // Alicloud RAM policy document for a given instance group role. type PolicyBuilder struct { Cluster *kops.Cluster HostedZoneID string KMSKeys []string Region string ResourceARN *string Role kops.InstanceGroupRole } // BuildAlicloudPolicy builds a set of RAM policy statements based on the // instance group type. func (b *PolicyBuilder) BuildAlicloudPolicy() (*Policy, error) { var p *Policy var err error // Retrieve all the KMS Keys in use for _, e := range b.Cluster.Spec.EtcdClusters { for _, m := range e.Members { if m.KmsKeyId != nil { b.KMSKeys = append(b.KMSKeys, *m.KmsKeyId) } } } switch b.Role { // case kops.InstanceGroupRoleBastion: // p, err = b.BuildAlicloudPolicyBastion() // if err != nil { // return nil, fmt.Errorf("failed to generate Alicloud RAM Policy for Bastion Instance Group: %v", err) // } case kops.InstanceGroupRoleNode: p, err = b.BuildAlicloudPolicyNode() if err != nil { return nil, fmt.Errorf("failed to generate Alicloud RAM Policy for Node Instance Group: %v", err) } case kops.InstanceGroupRoleMaster: p, err = b.BuildAlicloudPolicyMaster() if err != nil { return nil, fmt.Errorf("failed to generate Alicloud RAM Policy for Master Instance Group: %v", err) } default: return nil, fmt.Errorf("unrecognised instance group type: %s", b.Role) } return p, nil } // BuildAlicloudPolicyMaster generates a custom policy for a Kubernetes master. func (b *PolicyBuilder) BuildAlicloudPolicyMaster() (*Policy, error) { resource := createResource(b) p := &Policy{ Version: PolicyDefaultVersion, } addMasterECSPolicies(p, resource, b.Cluster.GetName()) addMasterESSPolicies(p, resource, b.Cluster.GetName()) addMasterSLBPolicies(p, resource) addVPCPermissions(p, resource, b.Cluster.GetName()) var err error if p, err = b.AddOSSPermissions(p); err != nil { return nil, fmt.Errorf("failed to generate Alicloud RAM OSS access statements: %v", err) } if b.Cluster.Spec.IAM != nil && b.Cluster.Spec.IAM.AllowContainerRegistry { addCRPermissions(p) } return p, nil } // BuildAlicloudPolicyNode generates a custom policy for a Kubernetes node. func (b *PolicyBuilder) BuildAlicloudPolicyNode() (*Policy, error) { resource := createResource(b) p := &Policy{ Version: PolicyDefaultVersion, } addNodeECSPolicies(p, resource) var err error if p, err = b.AddOSSPermissions(p); err != nil { return nil, fmt.Errorf("failed to generate Alicloud RAM OSS access statements: %v", err) } if b.Cluster.Spec.IAM != nil && b.Cluster.Spec.IAM.AllowContainerRegistry { addCRPermissions(p) } return p, nil } // RAMPrefix returns the prefix for Alicloud ARNs in the current region, for use with RAM // It is arn everywhere for now func (b *PolicyBuilder) RAMPrefix() string { return "acs" } // AddOSSPermissions updates an RAM Policy with statements granting tailored // access to OSS assets, depending on the instance group role func (b *PolicyBuilder) AddOSSPermissions(p *Policy) (*Policy, error) { // For OSS RAM permissions we grant permissions to subtrees, so find the parents; // we don't need to grant mypath and mypath/child. var roots []string { var locations []string for _, p := range []string{ b.Cluster.Spec.KeyStore, b.Cluster.Spec.SecretStore, b.Cluster.Spec.ConfigStore, } { if p == "" { continue } if !strings.HasSuffix(p, "/") { p = p + "/" } locations = append(locations, p) } for i, l := range locations { isTopLevel := true for j := range locations { if i == j { continue } if strings.HasPrefix(l, locations[j]) { klog.V(4).Infof("Ignoring location %q because found parent %q", l, locations[j]) isTopLevel = false } } if isTopLevel { klog.V(4).Infof("Found root location %q", l) roots = append(roots, l) } } } sort.Strings(roots) for _, root := range roots { vfsPath, err := vfs.Context.BuildVfsPath(root) if err != nil { return nil, fmt.Errorf("cannot parse VFS path %q: %v", root, err) } if ossPath, ok := vfsPath.(*vfs.OSSPath); ok { ramOSSPath := ossPath.Bucket() + "/" + ossPath.Key() ramOSSPath = strings.TrimSuffix(ramOSSPath, "/") p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Of("oss:GetBucketLocation", "oss:List*"), Resource: stringorslice.Slice([]string{ strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ossPath.Bucket()}, ""), }), }) if b.Role == kops.InstanceGroupRoleMaster { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"oss:Get*"}), Resource: stringorslice.Of( strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/*"}, ""), ), }) } else if b.Role == kops.InstanceGroupRoleNode { resources := []string{ strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/addons/*"}, ""), strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/cluster-completed.spec"}, ""), strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/config"}, ""), strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/instancegroup/*"}, ""), strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/pki/private/kube-proxy/*"}, ""), strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/secrets/dockerconfig"}, ""), } resources = append(resources, strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/pki/private/kubelet/*"}, "")) sort.Strings(resources) p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"oss:Get*"}), Resource: stringorslice.Of(resources...), }) if b.Cluster.Spec.Networking != nil { // @check if kuberoute is enabled and permit access to the private key if b.Cluster.Spec.Networking.Kuberouter != nil { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"oss:Get*"}), Resource: stringorslice.Of( strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/pki/private/kube-router/*"}, ""), ), }) } // @check if calico is enabled as the CNI provider and permit access to the client TLS certificate by default if b.Cluster.Spec.Networking.Calico != nil { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"oss:Get*"}), Resource: stringorslice.Of( strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/pki/private/calico-client/*"}, ""), ), }) } } } } else if _, ok := vfsPath.(*vfs.MemFSPath); ok { // Tests -ignore - nothing we can do in terms of RAM policy klog.Warningf("ignoring memfs path %q for RAM policy builder", vfsPath) } else { // We could implement this approach, but it seems better to // get all clouds using cluster-readable storage return nil, fmt.Errorf("path is not cluster readable: %v", root) } } nodeRole, err := iam.BuildNodeRoleSubject(b.Role, false) if err != nil { return nil, err } writeablePaths, err := iam.WriteableVFSPaths(b.Cluster, nodeRole) if err != nil { return nil, err } for _, vfsPath := range writeablePaths { if ossPath, ok := vfsPath.(*vfs.OSSPath); ok { ramOSSPath := ossPath.Bucket() + "/" + ossPath.Key() ramOSSPath = strings.TrimSuffix(ramOSSPath, "/") p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"oss:GetObject", "oss:DeleteObject", "oss:PutObject"}), Resource: stringorslice.Of( strings.Join([]string{b.RAMPrefix(), ":oss:*:*:", ramOSSPath, "/*"}, ""), ), }) } else { klog.Warningf("unknown writeable path, can't apply RAM policy: %q", vfsPath) } } return p, nil } func addMasterECSPolicies(p *Policy, resource stringorslice.StringOrSlice, clusterName string) { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{ "ecs:Describe*", "ecs:AttachDisk", "ecs:CreateDisk", "ecs:CreateSnapshot", "ecs:DeleteDisk", "ecs:DeleteSnapshot", "ecs:DetachDisk", "ecs:ModifyAutoSnapshotPolicy", "ecs:ModifyDiskAttribute", }), Resource: resource, }) } func addNodeECSPolicies(p *Policy, resource stringorslice.StringOrSlice) { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{"ecs:DescribeInstances"}), Resource: resource, }) } func addMasterSLBPolicies(p *Policy, resource stringorslice.StringOrSlice) { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Of( "slb:*", ), Resource: resource, }) } func addMasterESSPolicies(p *Policy, resource stringorslice.StringOrSlice, clusterName string) { // Comments are which cloudprovider / autoscaler code file makes the call // TODO: Make optional only if using autoscalers p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Of( "ess:Describe*", ), Resource: resource, }, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Of( "ess:ModifyScalingGroup", ), Resource: resource, Condition: Condition{ "StringEquals": map[string]string{ "ess:ResourceTag/KubernetesCluster": clusterName, }, }, }, ) } func addVPCPermissions(p *Policy, resource stringorslice.StringOrSlice, clusterName string) { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Slice([]string{ "vpc:*", }), Resource: resource, }, ) } func addCRPermissions(p *Policy) { p.Statement = append(p.Statement, &Statement{ Effect: StatementEffectAllow, Action: stringorslice.Of( "cr:Get*", "cr:List*", "cr:PullRepository", ), Resource: stringorslice.Slice([]string{"*"}), }) } func createResource(b *PolicyBuilder) stringorslice.StringOrSlice { var resource stringorslice.StringOrSlice if b.ResourceARN != nil { resource = stringorslice.Slice([]string{*b.ResourceARN}) } else { resource = stringorslice.Slice([]string{"*"}) } return resource }