Skip to content

Commit

Permalink
Kops now can create specific IAM policies.
Browse files Browse the repository at this point in the history
  • Loading branch information
chrislovecnm committed May 22, 2017
1 parent 0dd346f commit 9c442f6
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 117 deletions.
7 changes: 0 additions & 7 deletions cmd/kops/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,7 @@ func NewCmdCreate(f *util.Factory, out io.Writer) *cobra.Command {
}

cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename to use to create the resource")
//usage := "to use to create the resource"
//cmdutil.AddFilenameOptionFlags(cmd, options, usage)
cmd.MarkFlagRequired("filename")
//cmdutil.AddValidateFlags(cmd)
//cmdutil.AddOutputFlagsForMutation(cmd)
//cmdutil.AddApplyAnnotationFlags(cmd)
//cmdutil.AddRecordFlag(cmd)
//cmdutil.AddInclude3rdPartyFlags(cmd)

// create subcommands
cmd.AddCommand(NewCmdCreateCluster(f, out))
Expand Down
239 changes: 173 additions & 66 deletions pkg/model/iam/iam_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

// TODO: We have a couple different code paths until with do lifecycles, and
// TODO: when we have a cluster or refactor some s3 code. The only code that
// TODO: is not shared by the different path is the s3 / state store stuff

// TODO: We may want to look at https://aws.amazon.com/blogs/security/how-to-help-lock-down-a-users-amazon-ec2-capabilities-to-a-single-vpc/
// TODO: But that gets complicated fast. I would like to lock the policy down to a single VPC.

package iam

import (
Expand Down Expand Up @@ -48,6 +55,7 @@ func (p *IAMPolicy) AsJSON() (string, error) {
}

type IAMStatementEffect string
type IAMSid string

const IAMStatementEffectAllow IAMStatementEffect = "Allow"
const IAMStatementEffectDeny IAMStatementEffect = "Deny"
Expand All @@ -56,6 +64,7 @@ type IAMStatement struct {
Effect IAMStatementEffect
Action stringorslice.StringOrSlice
Resource stringorslice.StringOrSlice
Sid IAMSid
}

func (l *IAMStatement) Equal(r *IAMStatement) bool {
Expand All @@ -73,13 +82,19 @@ func (l *IAMStatement) Equal(r *IAMStatement) bool {

type IAMPolicyBuilder struct {
Cluster *api.Cluster
ClusterName string
Role api.InstanceGroupRole
Region string
HostedZoneID string
ResourceARN *string
// We probably implement this
// have the capability to shut off ECR perms
//CreateECRPerms bool
}

// BuildAWSIAMPolicy generates the IAM policies for a bastion, node or master
func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
wildcard := stringorslice.Slice([]string{"*"})
resource := createResource(b)

iamPrefix := b.IAMPrefix()

Expand All @@ -91,64 +106,125 @@ func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
if b.Role == api.InstanceGroupRoleBastion {
p.Statement = append(p.Statement, &IAMStatement{
// We grant a trivial (?) permission (DescribeRegions), because empty policies are not allowed
Sid: "kopsK8sBastion",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"ec2:DescribeRegions"}),
Resource: wildcard,
Resource: resource,
})

return p, nil
}

// TODO - I think we can just have GetAuthorizationToken here, as we are not
// TODO - making any API calls except for GetAuthorizationToken.

// We provide ECR access on the nodes (naturally), but we also provide access on the master.
// We shouldn't be running lots of pods on the master, but it is perfectly reasonable to run
// a private logging pod or similar.
// At this point we allow all regions with ECR, since ECR is region specific.

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sECR",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:DescribeRepositories",
"ecr:ListImages",
"ecr:BatchGetImage",
),
Resource: stringorslice.Slice([]string{"*"}),
})

if b.Role == api.InstanceGroupRoleNode {
// protokube makes a describe instance call
p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sNodeEC2Perms",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"ec2:Describe*"}),
Resource: wildcard,
Action: stringorslice.Slice([]string{"ec2:DescribeInstances"}),
Resource: resource,
})

}

{
// We provide ECR access on the nodes (naturally), but we also provide access on the master.
// We shouldn't be running lots of pods on the master, but it is perfectly reasonable to run
// a private logging pod or similar.
if b.Role == api.InstanceGroupRoleMaster {

// comments are which cloudprovider code file makes the call
p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sMasterEC2Perms",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:DescribeRepositories",
"ecr:ListImages",
"ecr:BatchGetImage",
"ec2:AttachVolume", // aws.go
"ec2:AuthorizeSecurityGroupIngress", // aws.go
"ec2:CreateTags", // tag.go
"ec2:CreateVolume", // aws.go
"ec2:CreateRoute", // aws.go
"ec2:CreateSecurityGroup", // aws.go
"ec2:DeleteSecurityGroup", // aws.go
"ec2:DeleteRoute", // aws.go
"ec2:DeleteVolume", // aws.go
"ec2:DescribeInstances", // aws.go
"ec2:DescribeRouteTables", // aws.go
"ec2:DescribeSubnets", // aws.go
"ec2:DescribeSecurityGroups", // aws.go
"ec2:DescribeVolumes", // aws.go
"ec2:DetachVolume", // aws.go
"ec2:ModifyInstanceAttribute", // aws.go
"ec2:RevokeSecurityGroupIngress", // aws.go
),
Resource: wildcard,
Resource: resource,
})
}

if b.Role == api.InstanceGroupRoleMaster {
// comments are which cloudprovider code file makes the call
p.Statement = append(p.Statement, &IAMStatement{
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"ec2:*"}),
Resource: wildcard,
Sid: "kopsElbPerms",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"elasticloadbalancing:AttachLoadBalancerToSubnets", // aws_loadbalanacer.go
"elasticloadbalancing:ApplySecurityGroupsToLoadBalancer", // aws_loadbalanacer.go
"elasticloadbalancing:CreateLoadBalancer", // aws_loadbalanacer.go
"elasticloadbalancing:CreateLoadBalancerPolicy", // aws_loadbalanacer.go
"elasticloadbalancing:CreateLoadBalancerListeners", // aws_loadbalanacer.go
"elasticloadbalancing:ConfigureHealthCheck", // aws_loadbalanacer.go
"elasticloadbalancing:DeleteLoadBalancer", // aws.go
"elasticloadbalancing:DeleteLoadBalancerListeners", // aws_loadbalanacer.go
"elasticloadbalancing:DescribeLoadBalancers", // aws.go
"elasticloadbalancing:DescribeLoadBalancerAttributes", // aws.go
"elasticloadbalancing:DetachLoadBalancerFromSubnets", // aws_loadbalancer.go
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer", // aws_loadbalanacer.go
"elasticloadbalancing:ModifyLoadBalancerAttributes", // aws_loadbalanacer.go
"elasticloadbalancing:RegisterInstancesWithLoadBalancer", // aws_loadbalanacer.go
"elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer", // aws_loadbalanacer.go
),
Resource: resource,
})

// comments are which cloudprovider / autoscaler code file makes the call
p.Statement = append(p.Statement, &IAMStatement{
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"elasticloadbalancing:*"}),
Resource: wildcard,
Sid: "kopsMasterASPerms",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"autoscaling:DescribeAutoScalingGroups", // aws_instancegroups.go
"autoscaling:GetAsgForInstance", // aws_manager.go
"autoscaling:SetDesiredCapacity", // aws_manager.go
"autoscaling:TerminateInstanceInAutoScalingGroup", // aws_manager.go
"autoscaling:UpdateAutoScalingGroup", // aws_instancegroups.go
),
Resource: resource,
})

// This is needed if we are using iam ssl certs
// on ELBs
// TODO need to test this
p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsMasterCertIAMPerms",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup",
"iam:ListServerCertificates",
"iam:GetServerCertificate",
),
Resource: wildcard,
Resource: resource,
})

// Restrict the KMS permissions to only the keys that are being used
Expand All @@ -162,7 +238,21 @@ func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
}

if kmsKeyIDs.Len() > 0 {
// TODO should we add conditions?
// "Condition": {
// "StringEquals": {
// "kms:ViaService": [
// "ec2.us-west-2.amazonaws.com",
// ]
// }
// }

// I removed these perms and testing is fine with encrypted volumes
// "kms:ListGrants",
// "kms:RevokeGrant",

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sKMSEncryptedVolumes",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of(
"kms:Encrypt",
Expand All @@ -171,16 +261,45 @@ func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant",
),
Resource: stringorslice.Slice(kmsKeyIDs.List()),
Resource: resource,
})
}
}

if b.HostedZoneID != "" {
addRoute53Permissions(p, b.HostedZoneID)
if b.HostedZoneID != "" {
// TODO we should test if we are in China, and not just return
// TODO no Route53 in China

// Remove /hostedzone/ prefix (if present)
hostedZoneID := strings.TrimPrefix(b.HostedZoneID, "/")
hostedZoneID = strings.TrimPrefix(hostedZoneID, "hostedzone/")

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sRoute53Change",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of("route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets",
"route53:ListHostedZones",
"route53:ListHostedZonesByName",
"route53:GetHostedZone"),
Resource: stringorslice.Slice([]string{"arn:aws:route53:::hostedzone/" + hostedZoneID}),
})

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sRoute53GetChanges",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"route53:GetChange"}),
Resource: stringorslice.Slice([]string{"arn:aws:route53:::change/*"}),
})

wildcard := stringorslice.Slice([]string{"*"})
p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sRoute53ListZones",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"route53:ListHostedZones"}),
Resource: wildcard,
})
}
}

// For S3 IAM permissions, we grant permissions to subtrees. So find the parents;
Expand Down Expand Up @@ -234,15 +353,20 @@ func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
iamS3Path = strings.TrimSuffix(iamS3Path, "/")

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sStateStoreAccess",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"s3:*"}),
Action: stringorslice.Of(
"s3:GetObject",
"s3:ListObject",
),
Resource: stringorslice.Of(
iamPrefix+":s3:::"+iamS3Path,
iamPrefix+":s3:::"+iamS3Path+"/*",
),
})

p.Statement = append(p.Statement, &IAMStatement{
Sid: "kopsK8sStateStoreAccessList",
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of("s3:GetBucketLocation", "s3:ListBucket"),
Resource: stringorslice.Slice([]string{
Expand All @@ -261,33 +385,6 @@ func (b *IAMPolicyBuilder) BuildAWSIAMPolicy() (*IAMPolicy, error) {
return p, nil
}

func addRoute53Permissions(p *IAMPolicy, hostedZoneID string) {
// Remove /hostedzone/ prefix (if present)
hostedZoneID = strings.TrimPrefix(hostedZoneID, "/")
hostedZoneID = strings.TrimPrefix(hostedZoneID, "hostedzone/")

p.Statement = append(p.Statement, &IAMStatement{
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of("route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets",
"route53:GetHostedZone"),
Resource: stringorslice.Slice([]string{"arn:aws:route53:::hostedzone/" + hostedZoneID}),
})

p.Statement = append(p.Statement, &IAMStatement{
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"route53:GetChange"}),
Resource: stringorslice.Slice([]string{"arn:aws:route53:::change/*"}),
})

wildcard := stringorslice.Slice([]string{"*"})
p.Statement = append(p.Statement, &IAMStatement{
Effect: IAMStatementEffectAllow,
Action: stringorslice.Slice([]string{"route53:ListHostedZones"}),
Resource: wildcard,
})
}

// IAMPrefix returns the prefix for AWS ARNs in the current region, for use with IAM
// it is arn:aws everywhere but in cn-north, where it is arn:aws-cn
func (b *IAMPolicyBuilder) IAMPrefix() string {
Expand Down Expand Up @@ -329,9 +426,19 @@ func (b *IAMPolicyResource) Open() (io.Reader, error) {
if err != nil {
return nil, fmt.Errorf("error building IAM policy: %v", err)
}
json, err := policy.AsJSON()
j, err := policy.AsJSON()
if err != nil {
return nil, fmt.Errorf("error building IAM policy: %v", err)
}
return bytes.NewReader([]byte(json)), nil
return bytes.NewReader([]byte(j)), nil
}

func createResource(b *IAMPolicyBuilder) stringorslice.StringOrSlice {
var resource stringorslice.StringOrSlice
if b.ResourceARN != nil {
resource = stringorslice.Slice([]string{*b.ResourceARN})
} else {
resource = stringorslice.Slice([]string{"*"})
}
return resource
}
6 changes: 4 additions & 2 deletions pkg/model/iam/iam_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,18 @@ func TestRoundTrip(t *testing.T) {
Effect: IAMStatementEffectAllow,
Action: stringorslice.Of("ec2:DescribeRegions"),
Resource: stringorslice.Of("*"),
Sid: "foo",
},
JSON: "{\"Effect\":\"Allow\",\"Action\":\"ec2:DescribeRegions\",\"Resource\":\"*\"}",
JSON: "{\"Effect\":\"Allow\",\"Action\":\"ec2:DescribeRegions\",\"Resource\":\"*\",\"Sid\":\"foo\"}",
},
{
IAM: &IAMStatement{
Effect: IAMStatementEffectDeny,
Action: stringorslice.Of("ec2:DescribeRegions", "ec2:DescribeInstances"),
Resource: stringorslice.Of("a", "b"),
Sid: "foo",
},
JSON: "{\"Effect\":\"Deny\",\"Action\":[\"ec2:DescribeRegions\",\"ec2:DescribeInstances\"],\"Resource\":[\"a\",\"b\"]}",
JSON: "{\"Effect\":\"Deny\",\"Action\":[\"ec2:DescribeRegions\",\"ec2:DescribeInstances\"],\"Resource\":[\"a\",\"b\"],\"Sid\":\"foo\"}",
},
}
for _, g := range grid {
Expand Down
Loading

0 comments on commit 9c442f6

Please sign in to comment.