diff --git a/docs/authentication.md b/docs/authentication.md index f3de14ed5d..73800f4bad 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -55,10 +55,10 @@ spec: rbac: {} ``` -By default the creation of an AWS IAM authenticator config as a ConfigMap is also required. +If no `backendMode` is configured, by default the `aws-iam-authenticator` will require the creation of an AWS IAM authenticator config as a ConfigMap. For more details on AWS IAM authenticator please visit [kubernetes-sigs/aws-iam-authenticator](https://github.com/kubernetes-sigs/aws-iam-authenticator) -Example config: +Example config using a ConfigMap: ```yaml --- @@ -124,13 +124,34 @@ authentication: clusterID: demo.cluster.us-west-2 ``` +When setting the `backendMode` configuration to `CRD`, it is possible to provide a list of inline AWS IAM identity mappings in the cluster template. + +```yaml +authentication: + aws: + backendMode: CRD + clusterID: demo.cluster.us-west-2 + identityMappings: + - arn: arn:aws:iam::000000000000:role/KubernetesAdmin + username: admin:{{SessionName}} + groups: + - system:masters + - arn: arn:aws:iam::000000000000:user/Alice + username: alice + groups: + - system:masters +``` + ### Creating a new cluster with IAM Authenticator on. * Create a cluster following the [AWS getting started guide](getting_started/aws.md) * When you reach the "Customize Cluster Configuration" section of the guide modify the cluster spec and add the Authentication and Authorization configs to the YAML config. +* Optionally set the `backendMode: CRD` and configure the identityMappings inline. * Continue following the cluster creation guide to build the cluster. - * :warning: When the cluster first comes up the aws-iam-authenticator PODs will be in a bad state. -as it is trying to find the aws-iam-authenticator ConfigMap and we have not yet created it. + * :warning: When no `backendMode` is configured (or it is set to `MountedFile`) and the cluster first comes up the aws-iam-authenticator PODs will be in a bad state as it is trying to find the aws-iam-authenticator ConfigMap and we have not yet created it. + +If no `backendMode` is configured, or it is set to `MountedFile, the following additional steps are necessary: + * Once the cluster is up, you'll need to create an aws-iam-authenticator configMap on the cluster `kubectl apply -f aws-iam-authenticator_example-config.yaml` * Once the configuration is created you need to delete the initially created aws-iam-authenticator PODs, this will force new ones to come and correctly find the ConfigMap. ``` @@ -144,4 +165,4 @@ kubectl get pods -n kube-system | grep aws-iam-authenticator | awk '{print $1}' * Update the clusters configuration `kops update cluster ${CLUSTER_NAME} --yes` * Temporarily disable aws-iam-authenticator DaemonSet `kubectl patch daemonset -n kube-system aws-iam-authenticator -p '{"spec": {"template": {"spec": {"nodeSelector": {"disable-aws-iam-authenticator": "true"}}}}}'` * Perform a rolling update of the masters `kops rolling-update cluster ${CLUSTER_NAME} --instance-group-roles=Master --force --yes` -* Re-enable aws-iam-authenticator DaemonSet `kubectl patch daemonset -n kube-system aws-iam-authenticator --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/disable-aws-iam-authenticator"}]'` +* Re-enable aws-iam-authenticator DaemonSet `kubectl patch daemonset -n kube-system aws-iam-authenticator --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/disable-aws-iam-authenticator"}]'` diff --git a/k8s/crds/kops.k8s.io_clusters.yaml b/k8s/crds/kops.k8s.io_clusters.yaml index 3e01814733..285bad2848 100644 --- a/k8s/crds/kops.k8s.io_clusters.yaml +++ b/k8s/crds/kops.k8s.io_clusters.yaml @@ -205,6 +205,26 @@ spec: container. Default 10m pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + identityMappings: + description: IdentityMappings maps IAM Identities to Kubernetes + users/groups + items: + properties: + arn: + description: Arn of the IAM User or IAM Role to be allowed + to authenticate + type: string + groups: + description: Groups to be attached to your users/roles + items: + type: string + type: array + username: + description: Username that Kubernetes will see the user + as + type: string + type: object + type: array image: description: Image is the AWS IAM Authenticator docker image to uses diff --git a/pkg/apis/kops/cluster.go b/pkg/apis/kops/cluster.go index f8670f8dc9..50157dc10b 100644 --- a/pkg/apis/kops/cluster.go +++ b/pkg/apis/kops/cluster.go @@ -373,6 +373,17 @@ type AwsAuthenticationSpec struct { MemoryLimit *resource.Quantity `json:"memoryLimit,omitempty"` // CPULimit CPU limit of AWS IAM Authenticator container. Default 10m CPULimit *resource.Quantity `json:"cpuLimit,omitempty"` + // IdentityMappings maps IAM Identities to Kubernetes users/groups + IdentityMappings []AwsAuthenticationIdentityMappingSpec `json:"identityMappings,omitempty"` +} + +type AwsAuthenticationIdentityMappingSpec struct { + // Arn of the IAM User or IAM Role to be allowed to authenticate + ARN string `json:"arn,omitempty"` + // Username that Kubernetes will see the user as + Username string `json:"username,omitempty"` + // Groups to be attached to your users/roles + Groups []string `json:"groups,omitempty"` } type AuthorizationSpec struct { diff --git a/pkg/apis/kops/v1alpha2/cluster.go b/pkg/apis/kops/v1alpha2/cluster.go index 394e854978..b6aa62284e 100644 --- a/pkg/apis/kops/v1alpha2/cluster.go +++ b/pkg/apis/kops/v1alpha2/cluster.go @@ -371,6 +371,17 @@ type AwsAuthenticationSpec struct { MemoryLimit *resource.Quantity `json:"memoryLimit,omitempty"` // CPULimit CPU limit of AWS IAM Authenticator container. Default 10m CPULimit *resource.Quantity `json:"cpuLimit,omitempty"` + // IdentityMappings maps IAM Identities to Kubernetes users/groups + IdentityMappings []AwsAuthenticationIdentityMappingSpec `json:"identityMappings,omitempty"` +} + +type AwsAuthenticationIdentityMappingSpec struct { + // Arn of the IAM User or IAM Role to be allowed to authenticate + ARN string `json:"arn,omitempty"` + // Username that Kubernetes will see the user as + Username string `json:"username,omitempty"` + // Groups to be attached to your users/roles + Groups []string `json:"groups,omitempty"` } type AuthorizationSpec struct { diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index a4426a9e75..cf9cb04535 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -144,6 +144,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*AwsAuthenticationIdentityMappingSpec)(nil), (*kops.AwsAuthenticationIdentityMappingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(a.(*AwsAuthenticationIdentityMappingSpec), b.(*kops.AwsAuthenticationIdentityMappingSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*kops.AwsAuthenticationIdentityMappingSpec)(nil), (*AwsAuthenticationIdentityMappingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec(a.(*kops.AwsAuthenticationIdentityMappingSpec), b.(*AwsAuthenticationIdentityMappingSpec), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*AwsAuthenticationSpec)(nil), (*kops.AwsAuthenticationSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(a.(*AwsAuthenticationSpec), b.(*kops.AwsAuthenticationSpec), scope) }); err != nil { @@ -1507,6 +1517,30 @@ func Convert_kops_AuthorizationSpec_To_v1alpha2_AuthorizationSpec(in *kops.Autho return autoConvert_kops_AuthorizationSpec_To_v1alpha2_AuthorizationSpec(in, out, s) } +func autoConvert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in *AwsAuthenticationIdentityMappingSpec, out *kops.AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + out.ARN = in.ARN + out.Username = in.Username + out.Groups = in.Groups + return nil +} + +// Convert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec is an autogenerated conversion function. +func Convert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in *AwsAuthenticationIdentityMappingSpec, out *kops.AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + return autoConvert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in, out, s) +} + +func autoConvert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec(in *kops.AwsAuthenticationIdentityMappingSpec, out *AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + out.ARN = in.ARN + out.Username = in.Username + out.Groups = in.Groups + return nil +} + +// Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec is an autogenerated conversion function. +func Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec(in *kops.AwsAuthenticationIdentityMappingSpec, out *AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + return autoConvert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec(in, out, s) +} + func autoConvert_v1alpha2_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(in *AwsAuthenticationSpec, out *kops.AwsAuthenticationSpec, s conversion.Scope) error { out.Image = in.Image out.BackendMode = in.BackendMode @@ -1515,6 +1549,17 @@ func autoConvert_v1alpha2_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(in out.CPURequest = in.CPURequest out.MemoryLimit = in.MemoryLimit out.CPULimit = in.CPULimit + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]kops.AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + if err := Convert_v1alpha2_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IdentityMappings = nil + } return nil } @@ -1531,6 +1576,17 @@ func autoConvert_kops_AwsAuthenticationSpec_To_v1alpha2_AwsAuthenticationSpec(in out.CPURequest = in.CPURequest out.MemoryLimit = in.MemoryLimit out.CPULimit = in.CPULimit + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + if err := Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha2_AwsAuthenticationIdentityMappingSpec(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IdentityMappings = nil + } return nil } diff --git a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go index 6bd35a2674..fc5d0cbea4 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go @@ -284,6 +284,27 @@ func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopyInto(out *AwsAuthenticationIdentityMappingSpec) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AwsAuthenticationIdentityMappingSpec. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopy() *AwsAuthenticationIdentityMappingSpec { + if in == nil { + return nil + } + out := new(AwsAuthenticationIdentityMappingSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { *out = *in @@ -307,6 +328,13 @@ func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { x := (*in).DeepCopy() *out = &x } + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/apis/kops/v1alpha3/cluster.go b/pkg/apis/kops/v1alpha3/cluster.go index 3eaa74ef15..ed6abb77da 100644 --- a/pkg/apis/kops/v1alpha3/cluster.go +++ b/pkg/apis/kops/v1alpha3/cluster.go @@ -345,6 +345,17 @@ type AwsAuthenticationSpec struct { MemoryLimit *resource.Quantity `json:"memoryLimit,omitempty"` // CPULimit CPU limit of AWS IAM Authenticator container. Default 10m CPULimit *resource.Quantity `json:"cpuLimit,omitempty"` + // IdentityMappings maps IAM Identities to Kubernetes users/groups + IdentityMappings []AwsAuthenticationIdentityMappingSpec `json:"identityMappings,omitempty"` +} + +type AwsAuthenticationIdentityMappingSpec struct { + // Arn of the IAM User or IAM Role to be allowed to authenticate + ARN string `json:"arn,omitempty"` + // Username that Kubernetes will see the user as + Username string `json:"username,omitempty"` + // Groups to be attached to your users/roles + Groups []string `json:"groups,omitempty"` } type AuthorizationSpec struct { diff --git a/pkg/apis/kops/v1alpha3/zz_generated.conversion.go b/pkg/apis/kops/v1alpha3/zz_generated.conversion.go index c284104559..8ba11f2474 100644 --- a/pkg/apis/kops/v1alpha3/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha3/zz_generated.conversion.go @@ -144,6 +144,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*AwsAuthenticationIdentityMappingSpec)(nil), (*kops.AwsAuthenticationIdentityMappingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(a.(*AwsAuthenticationIdentityMappingSpec), b.(*kops.AwsAuthenticationIdentityMappingSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*kops.AwsAuthenticationIdentityMappingSpec)(nil), (*AwsAuthenticationIdentityMappingSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec(a.(*kops.AwsAuthenticationIdentityMappingSpec), b.(*AwsAuthenticationIdentityMappingSpec), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*AwsAuthenticationSpec)(nil), (*kops.AwsAuthenticationSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(a.(*AwsAuthenticationSpec), b.(*kops.AwsAuthenticationSpec), scope) }); err != nil { @@ -1457,6 +1467,30 @@ func Convert_kops_AuthorizationSpec_To_v1alpha3_AuthorizationSpec(in *kops.Autho return autoConvert_kops_AuthorizationSpec_To_v1alpha3_AuthorizationSpec(in, out, s) } +func autoConvert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in *AwsAuthenticationIdentityMappingSpec, out *kops.AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + out.ARN = in.ARN + out.Username = in.Username + out.Groups = in.Groups + return nil +} + +// Convert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec is an autogenerated conversion function. +func Convert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in *AwsAuthenticationIdentityMappingSpec, out *kops.AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + return autoConvert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(in, out, s) +} + +func autoConvert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec(in *kops.AwsAuthenticationIdentityMappingSpec, out *AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + out.ARN = in.ARN + out.Username = in.Username + out.Groups = in.Groups + return nil +} + +// Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec is an autogenerated conversion function. +func Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec(in *kops.AwsAuthenticationIdentityMappingSpec, out *AwsAuthenticationIdentityMappingSpec, s conversion.Scope) error { + return autoConvert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec(in, out, s) +} + func autoConvert_v1alpha3_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(in *AwsAuthenticationSpec, out *kops.AwsAuthenticationSpec, s conversion.Scope) error { out.Image = in.Image out.BackendMode = in.BackendMode @@ -1465,6 +1499,17 @@ func autoConvert_v1alpha3_AwsAuthenticationSpec_To_kops_AwsAuthenticationSpec(in out.CPURequest = in.CPURequest out.MemoryLimit = in.MemoryLimit out.CPULimit = in.CPULimit + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]kops.AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + if err := Convert_v1alpha3_AwsAuthenticationIdentityMappingSpec_To_kops_AwsAuthenticationIdentityMappingSpec(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IdentityMappings = nil + } return nil } @@ -1481,6 +1526,17 @@ func autoConvert_kops_AwsAuthenticationSpec_To_v1alpha3_AwsAuthenticationSpec(in out.CPURequest = in.CPURequest out.MemoryLimit = in.MemoryLimit out.CPULimit = in.CPULimit + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + if err := Convert_kops_AwsAuthenticationIdentityMappingSpec_To_v1alpha3_AwsAuthenticationIdentityMappingSpec(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.IdentityMappings = nil + } return nil } diff --git a/pkg/apis/kops/v1alpha3/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha3/zz_generated.deepcopy.go index 2c9e8e7018..f438983341 100644 --- a/pkg/apis/kops/v1alpha3/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha3/zz_generated.deepcopy.go @@ -285,6 +285,27 @@ func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopyInto(out *AwsAuthenticationIdentityMappingSpec) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AwsAuthenticationIdentityMappingSpec. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopy() *AwsAuthenticationIdentityMappingSpec { + if in == nil { + return nil + } + out := new(AwsAuthenticationIdentityMappingSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { *out = *in @@ -308,6 +329,13 @@ func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { x := (*in).DeepCopy() *out = &x } + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/apis/kops/validation/aws.go b/pkg/apis/kops/validation/aws.go index 0796336459..a4667e7361 100644 --- a/pkg/apis/kops/validation/aws.go +++ b/pkg/apis/kops/validation/aws.go @@ -22,6 +22,7 @@ import ( "strconv" "strings" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" @@ -43,6 +44,10 @@ func awsValidateCluster(c *kops.Cluster) field.ErrorList { allErrs = append(allErrs, awsValidateExternalCloudControllerManager(c)...) + if c.Spec.Authentication != nil && c.Spec.Authentication.Aws != nil { + allErrs = append(allErrs, awsValidateIAMAuthenticator(field.NewPath("spec", "authentication", "aws"), c.Spec.Authentication.Aws)...) + } + return allErrs } @@ -319,6 +324,22 @@ func awsValidateCPUCredits(fieldPath *field.Path, spec *kops.InstanceGroupSpec, return allErrs } +func awsValidateIAMAuthenticator(fieldPath *field.Path, spec *kops.AwsAuthenticationSpec) field.ErrorList { + allErrs := field.ErrorList{} + + if !strings.Contains(spec.BackendMode, "CRD") && len(spec.IdentityMappings) > 0 { + allErrs = append(allErrs, field.Forbidden(fieldPath.Child("backendMode"), "backendMode must be CRD if identityMappings is set")) + } + for i, mapping := range spec.IdentityMappings { + parsedARN, err := arn.Parse(mapping.ARN) + if err != nil || (!strings.HasPrefix(parsedARN.Resource, "role/") && !strings.HasPrefix(parsedARN.Resource, "user/")) { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("identityMappings").Index(i).Child("arn"), mapping.ARN, + "arn must be a valid IAM Role or User ARN such as arn:aws:iam::123456789012:role/KopsExampleRole")) + } + } + return allErrs +} + func hasAWSEBSCSIDriver(c kops.ClusterSpec) bool { // AWSEBSCSIDriver will have a default value, so if this is all false, it will be populated on next pass if c.CloudConfig == nil || c.CloudConfig.AWSEBSCSIDriver == nil || c.CloudConfig.AWSEBSCSIDriver.Enabled == nil { diff --git a/pkg/apis/kops/validation/aws_test.go b/pkg/apis/kops/validation/aws_test.go index b5c84b6b37..d11c6fd33c 100644 --- a/pkg/apis/kops/validation/aws_test.go +++ b/pkg/apis/kops/validation/aws_test.go @@ -575,3 +575,73 @@ func TestLoadBalancerSubnets(t *testing.T) { testErrors(t, test, errs, test.expected) } } + +func TestAWSAuthentication(t *testing.T) { + tests := []struct { + backendMode string + identityMappings []kops.AwsAuthenticationIdentityMappingSpec + expected []string + }{ + { // valid + backendMode: "CRD", + identityMappings: []kops.AwsAuthenticationIdentityMappingSpec{ + { + ARN: "arn:aws:iam::123456789012:role/KopsExampleRole", + Username: "foo", + }, + { + ARN: "arn:aws:iam::123456789012:user/KopsExampleUser", + Username: "foo", + }, + }, + }, + { // valid, multiple backendModes + backendMode: "CRD,MountedFile", + identityMappings: []kops.AwsAuthenticationIdentityMappingSpec{ + { + ARN: "arn:aws:iam::123456789012:role/KopsExampleRole", + Username: "foo", + }, + { + ARN: "arn:aws:iam::123456789012:user/KopsExampleUser", + Username: "foo", + }, + }, + }, + { // forbidden backendMode + backendMode: "MountedFile", + identityMappings: []kops.AwsAuthenticationIdentityMappingSpec{ + { + ARN: "arn:aws:iam::123456789012:role/KopsExampleRole", + Username: "foo", + }, + }, + expected: []string{"Forbidden::spec.authentication.aws.backendMode"}, + }, + { // invalid identity ARN + backendMode: "CRD", + identityMappings: []kops.AwsAuthenticationIdentityMappingSpec{ + { + ARN: "arn:aws:iam::123456789012:policy/KopsExampleRole", + Username: "foo", + }, + }, + expected: []string{"Invalid value::spec.authentication.aws.identityMappings[0].arn"}, + }, + } + + for _, test := range tests { + cluster := kops.Cluster{ + Spec: kops.ClusterSpec{ + Authentication: &kops.AuthenticationSpec{ + Aws: &kops.AwsAuthenticationSpec{ + BackendMode: test.backendMode, + IdentityMappings: test.identityMappings, + }, + }, + }, + } + errs := awsValidateCluster(&cluster) + testErrors(t, test, errs, test.expected) + } +} diff --git a/pkg/apis/kops/zz_generated.deepcopy.go b/pkg/apis/kops/zz_generated.deepcopy.go index bc38d415c2..0c3b799412 100644 --- a/pkg/apis/kops/zz_generated.deepcopy.go +++ b/pkg/apis/kops/zz_generated.deepcopy.go @@ -284,6 +284,27 @@ func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopyInto(out *AwsAuthenticationIdentityMappingSpec) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AwsAuthenticationIdentityMappingSpec. +func (in *AwsAuthenticationIdentityMappingSpec) DeepCopy() *AwsAuthenticationIdentityMappingSpec { + if in == nil { + return nil + } + out := new(AwsAuthenticationIdentityMappingSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { *out = *in @@ -307,6 +328,13 @@ func (in *AwsAuthenticationSpec) DeepCopyInto(out *AwsAuthenticationSpec) { x := (*in).DeepCopy() *out = &x } + if in.IdentityMappings != nil { + in, out := &in.IdentityMappings, &out.IdentityMappings + *out = make([]AwsAuthenticationIdentityMappingSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/upup/models/cloudup/resources/addons/authentication.aws/k8s-1.12.yaml.template b/upup/models/cloudup/resources/addons/authentication.aws/k8s-1.12.yaml.template index ba8dbcbe73..2f0090b128 100644 --- a/upup/models/cloudup/resources/addons/authentication.aws/k8s-1.12.yaml.template +++ b/upup/models/cloudup/resources/addons/authentication.aws/k8s-1.12.yaml.template @@ -205,3 +205,19 @@ spec: - name: state hostPath: path: /srv/kubernetes/aws-iam-authenticator/ +{{- if and (and (.Authentication.Aws.BackendMode) (contains "CRD" .Authentication.Aws.BackendMode)) (.Authentication.Aws.IdentityMappings) }} +--- +{{- range $i, $mapping := .Authentication.Aws.IdentityMappings }} +apiVersion: iamauthenticator.k8s.aws/v1alpha1 +kind: IAMIdentityMapping +metadata: + name: iam-identity-mapping-{{ $i }} + labels: + k8s-app: aws-iam-authenticator +spec: + arn: {{ $mapping.ARN }} + username: {{ $mapping.Username }} + groups: +{{ ToYAML $mapping.Groups | indent 4 }} +{{- end }} +{{- end }} diff --git a/upup/pkg/fi/cloudup/bootstrapchannelbuilder_test.go b/upup/pkg/fi/cloudup/bootstrapchannelbuilder_test.go index 00fa5e6b53..e36cb9adb2 100644 --- a/upup/pkg/fi/cloudup/bootstrapchannelbuilder_test.go +++ b/upup/pkg/fi/cloudup/bootstrapchannelbuilder_test.go @@ -50,7 +50,8 @@ func TestBootstrapChannelBuilder_BuildTasks(t *testing.T) { runChannelBuilderTest(t, "weave", []string{}) runChannelBuilderTest(t, "amazonvpc", []string{"networking.amazon-vpc-routed-eni-k8s-1.16"}) runChannelBuilderTest(t, "amazonvpc-containerd", []string{"networking.amazon-vpc-routed-eni-k8s-1.16"}) - runChannelBuilderTest(t, "awsiamauthenticator", []string{"authentication.aws-k8s-1.12"}) + runChannelBuilderTest(t, "awsiamauthenticator/crd", []string{"authentication.aws-k8s-1.12"}) + runChannelBuilderTest(t, "awsiamauthenticator/mappings", []string{"authentication.aws-k8s-1.12"}) runChannelBuilderTest(t, "metrics-server/insecure-1.18", []string{"metrics-server.addons.k8s.io-k8s-1.11"}) runChannelBuilderTest(t, "metrics-server/insecure-1.19", []string{"metrics-server.addons.k8s.io-k8s-1.11"}) runChannelBuilderTest(t, "metrics-server/secure-1.18", []string{"metrics-server.addons.k8s.io-k8s-1.11"}) diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/authentication.aws-k8s-1.12.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/authentication.aws-k8s-1.12.yaml similarity index 100% rename from upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/authentication.aws-k8s-1.12.yaml rename to upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/authentication.aws-k8s-1.12.yaml diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/cluster.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/cluster.yaml similarity index 100% rename from upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/cluster.yaml rename to upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/cluster.yaml diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/manifest.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/manifest.yaml similarity index 100% rename from upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/manifest.yaml rename to upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/crd/manifest.yaml diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/authentication.aws-k8s-1.12.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/authentication.aws-k8s-1.12.yaml new file mode 100644 index 0000000000..054bb584b6 --- /dev/null +++ b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/authentication.aws-k8s-1.12.yaml @@ -0,0 +1,226 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + role.kubernetes.io/authentication: "1" + name: iamidentitymappings.iamauthenticator.k8s.aws +spec: + group: iamauthenticator.k8s.aws + names: + categories: + - all + kind: IAMIdentityMapping + plural: iamidentitymappings + singular: iamidentitymapping + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + arn: + type: string + groups: + items: + type: string + type: array + username: + type: string + required: + - arn + - username + type: object + type: object + served: true + storage: true + subresources: + status: {} + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + role.kubernetes.io/authentication: "1" + name: aws-iam-authenticator +rules: +- apiGroups: + - iamauthenticator.k8s.aws + resources: + - iamidentitymappings + verbs: + - get + - list + - watch +- apiGroups: + - iamauthenticator.k8s.aws + resources: + - iamidentitymappings/status + verbs: + - patch + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update + - patch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - list + - watch +- apiGroups: + - "" + resourceNames: + - aws-auth + resources: + - configmaps + verbs: + - get + +--- + +apiVersion: v1 +kind: ServiceAccount +metadata: + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + role.kubernetes.io/authentication: "1" + name: aws-iam-authenticator + namespace: kube-system + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + role.kubernetes.io/authentication: "1" + name: aws-iam-authenticator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: aws-iam-authenticator +subjects: +- kind: ServiceAccount + name: aws-iam-authenticator + namespace: kube-system + +--- + +apiVersion: apps/v1 +kind: DaemonSet +metadata: + annotations: + seccomp.security.alpha.kubernetes.io/pod: runtime/default + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + k8s-app: aws-iam-authenticator + role.kubernetes.io/authentication: "1" + name: aws-iam-authenticator + namespace: kube-system +spec: + selector: + matchLabels: + k8s-app: aws-iam-authenticator + template: + metadata: + annotations: + scheduler.alpha.kubernetes.io/critical-pod: "" + labels: + k8s-app: aws-iam-authenticator + spec: + containers: + - args: + - server + - --cluster-id=minimal.example.com + - --state-dir=/var/aws-iam-authenticator + - --kubeconfig-pregenerated=true + - --backend-mode=CRD + image: 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon/aws-iam-authenticator:v0.5.3-debian-stretch + livenessProbe: + httpGet: + host: 127.0.0.1 + path: /healthz + port: 21362 + scheme: HTTPS + name: aws-iam-authenticator + resources: + limits: + cpu: 100m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - mountPath: /var/aws-iam-authenticator/ + name: state + - mountPath: /etc/kubernetes/aws-iam-authenticator/ + name: output + hostNetwork: true + nodeSelector: + node-role.kubernetes.io/master: "" + priorityClassName: system-node-critical + serviceAccountName: aws-iam-authenticator + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + - effect: NoSchedule + key: node-role.kubernetes.io/api-server + - key: node.cloudprovider.kubernetes.io/uninitialized + operator: Exists + - key: CriticalAddonsOnly + operator: Exists + volumes: + - hostPath: + path: /srv/kubernetes/aws-iam-authenticator/ + name: output + - hostPath: + path: /srv/kubernetes/aws-iam-authenticator/ + name: state + updateStrategy: + type: RollingUpdate + +--- + +apiVersion: iamauthenticator.k8s.aws/v1alpha1 +kind: IAMIdentityMapping +metadata: + creationTimestamp: null + labels: + addon.kops.k8s.io/name: authentication.aws + app.kubernetes.io/managed-by: kops + k8s-app: aws-iam-authenticator + role.kubernetes.io/authentication: "1" + name: iam-identity-mapping-0 +spec: + arn: arn:aws:iam::00000000:role/AdminRole + groups: + - system:masters + username: administrators:{{SessionName}} diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/cluster.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/cluster.yaml new file mode 100644 index 0000000000..7906b92aa5 --- /dev/null +++ b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/cluster.yaml @@ -0,0 +1,50 @@ +apiVersion: kops.k8s.io/v1alpha2 +kind: Cluster +metadata: + creationTimestamp: "2016-12-10T22:42:27Z" + name: minimal.example.com +spec: + addons: + - manifest: s3://somebucket/example.yaml + authentication: + aws: + backendMode: CRD + identityMappings: + - arn: arn:aws:iam::00000000:role/AdminRole + username: administrators:{{SessionName}} + groups: + - system:masters + kubernetesApiAccess: + - 0.0.0.0/0 + channel: stable + cloudProvider: aws + configBase: memfs://clusters.example.com/minimal.example.com + etcdClusters: + - etcdMembers: + - instanceGroup: master-us-test-1a + name: master-us-test-1a + name: main + - etcdMembers: + - instanceGroup: master-us-test-1a + name: master-us-test-1a + name: events + iam: {} + kubernetesVersion: v1.20.0 + masterInternalName: api.internal.minimal.example.com + masterPublicName: api.minimal.example.com + additionalSans: + - proxy.api.minimal.example.com + networkCIDR: 172.20.0.0/16 + networking: + cni: {} + nonMasqueradeCIDR: 100.64.0.0/10 + sshAccess: + - 0.0.0.0/0 + topology: + masters: public + nodes: public + subnets: + - cidr: 172.20.32.0/19 + name: us-test-1a + type: Public + zone: us-test-1a diff --git a/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/manifest.yaml b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/manifest.yaml new file mode 100644 index 0000000000..1ef47f2b78 --- /dev/null +++ b/upup/pkg/fi/cloudup/tests/bootstrapchannelbuilder/awsiamauthenticator/mappings/manifest.yaml @@ -0,0 +1,61 @@ +kind: Addons +metadata: + creationTimestamp: null + name: bootstrap +spec: + addons: + - id: k8s-1.16 + manifest: kops-controller.addons.k8s.io/k8s-1.16.yaml + manifestHash: 46120dbd8151e5dffad79c48273eb910ff42fa4e05f40f76a39d554d77497f6d + name: kops-controller.addons.k8s.io + needsRollingUpdate: control-plane + selector: + k8s-addon: kops-controller.addons.k8s.io + version: 9.99.0 + - manifest: core.addons.k8s.io/v1.4.0.yaml + manifestHash: 18233793a8442224d052e44891e737c67ccfb4e051e95216392319653f4cb0e5 + name: core.addons.k8s.io + selector: + k8s-addon: core.addons.k8s.io + version: 9.99.0 + - id: k8s-1.12 + manifest: coredns.addons.k8s.io/k8s-1.12.yaml + manifestHash: 88ffe1a3752cf290450cc94bd53aea49a665e411dbf4cfe9c1a2cc5b027f12ef + name: coredns.addons.k8s.io + selector: + k8s-addon: coredns.addons.k8s.io + version: 9.99.0 + - id: k8s-1.9 + manifest: kubelet-api.rbac.addons.k8s.io/k8s-1.9.yaml + manifestHash: 01c120e887bd98d82ef57983ad58a0b22bc85efb48108092a24c4b82e4c9ea81 + name: kubelet-api.rbac.addons.k8s.io + selector: + k8s-addon: kubelet-api.rbac.addons.k8s.io + version: 9.99.0 + - manifest: limit-range.addons.k8s.io/v1.5.0.yaml + manifestHash: 2d55c3bc5e354e84a3730a65b42f39aba630a59dc8d32b30859fcce3d3178bc2 + name: limit-range.addons.k8s.io + selector: + k8s-addon: limit-range.addons.k8s.io + version: 9.99.0 + - id: k8s-1.12 + manifest: dns-controller.addons.k8s.io/k8s-1.12.yaml + manifestHash: 08d576cf7e30936c5a077d9d8439f1a7a1245e737722faa477eb734e87b292aa + name: dns-controller.addons.k8s.io + selector: + k8s-addon: dns-controller.addons.k8s.io + version: 9.99.0 + - id: v1.15.0 + manifest: storage-aws.addons.k8s.io/v1.15.0.yaml + manifestHash: 065ae832ddac8d0931e9992d6a76f43a33a36975a38003b34f4c5d86a7d42780 + name: storage-aws.addons.k8s.io + selector: + k8s-addon: storage-aws.addons.k8s.io + version: 9.99.0 + - id: k8s-1.12 + manifest: authentication.aws/k8s-1.12.yaml + manifestHash: 0cfdcbd7be8aa8a99c0e23b9417bf1c0e23bdbb5a5702168ff26f9248284ce8d + name: authentication.aws + selector: + role.kubernetes.io/authentication: "1" + version: 9.99.0