mirror of https://github.com/kubernetes/kops.git
457 lines
13 KiB
Go
457 lines
13 KiB
Go
/*
|
|
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
|
|
}
|