sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/eks/iam/iam.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package iam 18 19 import ( 20 "crypto/sha1" 21 "encoding/hex" 22 "encoding/json" 23 "net/http" 24 "net/url" 25 26 "github.com/aws/aws-sdk-go/aws" 27 "github.com/aws/aws-sdk-go/service/eks" 28 "github.com/aws/aws-sdk-go/service/iam" 29 "github.com/aws/aws-sdk-go/service/iam/iamiface" 30 "github.com/go-logr/logr" 31 "github.com/google/go-cmp/cmp" 32 "github.com/pkg/errors" 33 34 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 35 "sigs.k8s.io/cluster-api-provider-aws/cmd/clusterawsadm/converters" 36 iamv1 "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1" 37 ) 38 39 const ( 40 // EKSFargateService is the service to trust for fargate pod execution roles. 41 EKSFargateService = "eks-fargate-pods.amazonaws.com" 42 ) 43 44 // IAMService defines the specs for an IAM service. 45 type IAMService struct { 46 logr.Logger 47 IAMClient iamiface.IAMAPI 48 } 49 50 // GetIAMRole will return the IAM role for the IAMService. 51 func (s *IAMService) GetIAMRole(name string) (*iam.Role, error) { 52 input := &iam.GetRoleInput{ 53 RoleName: aws.String(name), 54 } 55 56 out, err := s.IAMClient.GetRole(input) 57 if err != nil { 58 return nil, err 59 } 60 61 return out.Role, nil 62 } 63 64 func (s *IAMService) getIAMPolicy(policyArn string) (*iam.Policy, error) { 65 input := &iam.GetPolicyInput{ 66 PolicyArn: &policyArn, 67 } 68 69 out, err := s.IAMClient.GetPolicy(input) 70 if err != nil { 71 return nil, err 72 } 73 74 return out.Policy, nil 75 } 76 77 func (s *IAMService) getIAMRolePolicies(roleName string) ([]*string, error) { 78 input := &iam.ListAttachedRolePoliciesInput{ 79 RoleName: &roleName, 80 } 81 82 out, err := s.IAMClient.ListAttachedRolePolicies(input) 83 if err != nil { 84 return nil, errors.Wrapf(err, "error listing role polices for %s", roleName) 85 } 86 87 policies := []*string{} 88 for _, policy := range out.AttachedPolicies { 89 policies = append(policies, policy.PolicyArn) 90 } 91 92 return policies, nil 93 } 94 95 func (s *IAMService) detachIAMRolePolicy(roleName string, policyARN string) error { 96 input := &iam.DetachRolePolicyInput{ 97 RoleName: aws.String(roleName), 98 PolicyArn: aws.String(policyARN), 99 } 100 101 if _, err := s.IAMClient.DetachRolePolicy(input); err != nil { 102 return errors.Wrapf(err, "error detaching policy %s from role %s", policyARN, roleName) 103 } 104 105 return nil 106 } 107 108 func (s *IAMService) attachIAMRolePolicy(roleName string, policyARN string) error { 109 input := &iam.AttachRolePolicyInput{ 110 RoleName: aws.String(roleName), 111 PolicyArn: aws.String(policyARN), 112 } 113 114 if _, err := s.IAMClient.AttachRolePolicy(input); err != nil { 115 return errors.Wrapf(err, "error attaching policy %s to role %s", policyARN, roleName) 116 } 117 118 return nil 119 } 120 121 // EnsurePoliciesAttached will ensure the IAMService has policies attached. 122 func (s *IAMService) EnsurePoliciesAttached(role *iam.Role, policies []*string) (bool, error) { 123 s.V(2).Info("Ensuring Polices are attached to role") 124 existingPolices, err := s.getIAMRolePolicies(*role.RoleName) 125 if err != nil { 126 return false, err 127 } 128 129 var updatedPolicies bool 130 // Remove polices that aren't in the list 131 for _, existingPolicy := range existingPolices { 132 found := findStringInSlice(policies, *existingPolicy) 133 if !found { 134 updatedPolicies = true 135 err = s.detachIAMRolePolicy(*role.RoleName, *existingPolicy) 136 if err != nil { 137 return false, err 138 } 139 s.V(2).Info("Detached policy from role", "role", role.RoleName, "policy", existingPolicy) 140 } 141 } 142 143 // Add any policies that aren't currently attached 144 for _, policy := range policies { 145 found := findStringInSlice(existingPolices, *policy) 146 if !found { 147 // Make sure policy exists before attaching 148 _, err := s.getIAMPolicy(*policy) 149 if err != nil { 150 return false, errors.Wrapf(err, "error getting policy %s", *policy) 151 } 152 153 updatedPolicies = true 154 err = s.attachIAMRolePolicy(*role.RoleName, *policy) 155 if err != nil { 156 return false, err 157 } 158 s.V(2).Info("Attached policy to role", "role", role.RoleName, "policy", *policy) 159 } 160 } 161 162 return updatedPolicies, nil 163 } 164 165 // RoleTags returns the tags for the given role. 166 func RoleTags(key string, additionalTags infrav1.Tags) []*iam.Tag { 167 additionalTags[infrav1.ClusterAWSCloudProviderTagKey(key)] = string(infrav1.ResourceLifecycleOwned) 168 tags := []*iam.Tag{} 169 for k, v := range additionalTags { 170 tags = append(tags, &iam.Tag{ 171 Key: aws.String(k), 172 Value: aws.String(v), 173 }) 174 } 175 return tags 176 } 177 178 // CreateRole will create a role from the IAMService. 179 func (s *IAMService) CreateRole( 180 roleName string, 181 key string, 182 trustRelationship *iamv1.PolicyDocument, 183 additionalTags infrav1.Tags, 184 ) (*iam.Role, error) { 185 tags := RoleTags(key, additionalTags) 186 187 trustRelationshipJSON, err := converters.IAMPolicyDocumentToJSON(*trustRelationship) 188 if err != nil { 189 return nil, errors.Wrap(err, "error converting trust relationship to json") 190 } 191 192 input := &iam.CreateRoleInput{ 193 RoleName: aws.String(roleName), 194 Tags: tags, 195 AssumeRolePolicyDocument: aws.String(trustRelationshipJSON), 196 } 197 198 out, err := s.IAMClient.CreateRole(input) 199 if err != nil { 200 return nil, errors.Wrap(err, "failed to call CreateRole") 201 } 202 203 return out.Role, nil 204 } 205 206 // EnsureTagsAndPolicy will ensure any tags and policies against the IAMService. 207 func (s *IAMService) EnsureTagsAndPolicy( 208 role *iam.Role, 209 key string, 210 trustRelationship *iamv1.PolicyDocument, 211 additionalTags infrav1.Tags, 212 ) (bool, error) { 213 s.V(2).Info("Ensuring tags and AssumeRolePolicyDocument are set on role") 214 215 rolePolicyDocumentRaw, err := url.PathUnescape(*role.AssumeRolePolicyDocument) 216 if err != nil { 217 return false, errors.Wrap(err, "couldn't decode AssumeRolePolicyDocument") 218 } 219 220 var rolePolicyDocument iamv1.PolicyDocument 221 err = json.Unmarshal([]byte(rolePolicyDocumentRaw), &rolePolicyDocument) 222 if err != nil { 223 return false, errors.Wrap(err, "couldn't unmarshal AssumeRolePolicyDocument") 224 } 225 226 var updated bool 227 if !cmp.Equal(*trustRelationship, rolePolicyDocument) { 228 trustRelationshipJSON, err := converters.IAMPolicyDocumentToJSON(*trustRelationship) 229 if err != nil { 230 return false, errors.Wrap(err, "error converting trust relationship to json") 231 } 232 policyInput := &iam.UpdateAssumeRolePolicyInput{ 233 RoleName: role.RoleName, 234 PolicyDocument: aws.String(trustRelationshipJSON), 235 } 236 updated = true 237 if _, err := s.IAMClient.UpdateAssumeRolePolicy(policyInput); err != nil { 238 return updated, err 239 } 240 } 241 242 tagInput := &iam.TagRoleInput{ 243 RoleName: role.RoleName, 244 } 245 untagInput := &iam.UntagRoleInput{ 246 RoleName: role.RoleName, 247 } 248 currentTags := make(map[string]string) 249 for _, tag := range role.Tags { 250 currentTags[*tag.Key] = *tag.Value 251 if *tag.Key == infrav1.ClusterAWSCloudProviderTagKey(key) { 252 continue 253 } 254 if _, ok := additionalTags[*tag.Key]; !ok { 255 untagInput.TagKeys = append(untagInput.TagKeys, tag.Key) 256 } 257 } 258 for key, value := range additionalTags { 259 if currentV, ok := currentTags[key]; !ok || value != currentV { 260 tagInput.Tags = append(tagInput.Tags, &iam.Tag{ 261 Key: aws.String(key), 262 Value: aws.String(value), 263 }) 264 } 265 } 266 267 if len(tagInput.Tags) > 0 { 268 updated = true 269 _, err = s.IAMClient.TagRole(tagInput) 270 if err != nil { 271 return updated, err 272 } 273 } 274 275 if len(untagInput.TagKeys) > 0 { 276 updated = true 277 _, err = s.IAMClient.UntagRole(untagInput) 278 if err != nil { 279 return updated, err 280 } 281 } 282 283 return updated, nil 284 } 285 286 func (s *IAMService) detachAllPoliciesForRole(name string) error { 287 s.V(3).Info("Detaching all policies for role", "role", name) 288 input := &iam.ListAttachedRolePoliciesInput{ 289 RoleName: &name, 290 } 291 policies, err := s.IAMClient.ListAttachedRolePolicies(input) 292 if err != nil { 293 return errors.Wrapf(err, "error fetching policies for role %s", name) 294 } 295 for _, p := range policies.AttachedPolicies { 296 s.V(2).Info("Detaching policy", "policy", *p) 297 if err := s.detachIAMRolePolicy(name, *p.PolicyArn); err != nil { 298 return err 299 } 300 } 301 return nil 302 } 303 304 // DeleteRole will delete a role from the IAMService. 305 func (s *IAMService) DeleteRole(name string) error { 306 if err := s.detachAllPoliciesForRole(name); err != nil { 307 return errors.Wrapf(err, "error detaching policies for role %s", name) 308 } 309 310 input := &iam.DeleteRoleInput{ 311 RoleName: aws.String(name), 312 } 313 314 if _, err := s.IAMClient.DeleteRole(input); err != nil { 315 return errors.Wrapf(err, "error deleting role %s", name) 316 } 317 318 return nil 319 } 320 321 // IsUnmanaged will check if a given role and tag are unmanaged against the IAMService. 322 func (s *IAMService) IsUnmanaged(role *iam.Role, key string) bool { 323 keyToFind := infrav1.ClusterAWSCloudProviderTagKey(key) 324 for _, tag := range role.Tags { 325 if *tag.Key == keyToFind && *tag.Value == string(infrav1.ResourceLifecycleOwned) { 326 return false 327 } 328 } 329 330 return true 331 } 332 333 // ControlPlaneTrustRelationship will generate a ControlPlane PolicyDocument. 334 func ControlPlaneTrustRelationship(enableFargate bool) *iamv1.PolicyDocument { 335 identity := make(iamv1.Principals) 336 identity["Service"] = []string{"eks.amazonaws.com"} 337 if enableFargate { 338 identity["Service"] = append(identity["Service"], EKSFargateService) 339 } 340 341 policy := &iamv1.PolicyDocument{ 342 Version: "2012-10-17", 343 Statement: []iamv1.StatementEntry{ 344 { 345 Effect: "Allow", 346 Action: []string{ 347 "sts:AssumeRole", 348 }, 349 Principal: identity, 350 }, 351 }, 352 } 353 354 return policy 355 } 356 357 // FargateTrustRelationship will generate a Fargate PolicyDocument. 358 func FargateTrustRelationship() *iamv1.PolicyDocument { 359 identity := make(iamv1.Principals) 360 identity["Service"] = []string{EKSFargateService} 361 362 policy := &iamv1.PolicyDocument{ 363 Version: "2012-10-17", 364 Statement: []iamv1.StatementEntry{ 365 { 366 Effect: "Allow", 367 Action: []string{ 368 "sts:AssumeRole", 369 }, 370 Principal: identity, 371 }, 372 }, 373 } 374 375 return policy 376 } 377 378 // NodegroupTrustRelationship will generate a Nodegroup PolicyDocument. 379 func NodegroupTrustRelationship() *iamv1.PolicyDocument { 380 identity := make(iamv1.Principals) 381 identity["Service"] = []string{"ec2.amazonaws.com"} 382 383 policy := &iamv1.PolicyDocument{ 384 Version: "2012-10-17", 385 Statement: []iamv1.StatementEntry{ 386 { 387 Effect: "Allow", 388 Action: []string{ 389 "sts:AssumeRole", 390 }, 391 Principal: identity, 392 }, 393 }, 394 } 395 396 return policy 397 } 398 399 func findStringInSlice(slice []*string, toFind string) bool { 400 for _, item := range slice { 401 if *item == toFind { 402 return true 403 } 404 } 405 406 return false 407 } 408 409 const stsAWSAudience = "sts.amazonaws.com" 410 411 // CreateOIDCProvider will create an OIDC provider. 412 func (s *IAMService) CreateOIDCProvider(cluster *eks.Cluster) (string, error) { 413 issuerURL, err := url.Parse(*cluster.Identity.Oidc.Issuer) 414 if err != nil { 415 return "", err 416 } 417 if issuerURL.Scheme != "https" { 418 return "", errors.Errorf("invalid scheme for issuer URL %s", issuerURL.String()) 419 } 420 421 thumbprint, err := fetchRootCAThumbprint(issuerURL.String()) 422 if err != nil { 423 return "", err 424 } 425 input := iam.CreateOpenIDConnectProviderInput{ 426 ClientIDList: aws.StringSlice([]string{stsAWSAudience}), 427 ThumbprintList: aws.StringSlice([]string{thumbprint}), 428 Url: aws.String(issuerURL.String()), 429 } 430 provider, err := s.IAMClient.CreateOpenIDConnectProvider(&input) 431 if err != nil { 432 return "", errors.Wrap(err, "error creating provider") 433 } 434 return *provider.OpenIDConnectProviderArn, nil 435 } 436 437 func fetchRootCAThumbprint(issuerURL string) (string, error) { 438 response, err := http.Get(issuerURL) 439 if err != nil { 440 return "", err 441 } 442 defer response.Body.Close() 443 444 rootCA := response.TLS.PeerCertificates[len(response.TLS.PeerCertificates)-1] 445 sha1Sum := sha1.Sum(rootCA.Raw) //nolint:gosec 446 return hex.EncodeToString(sha1Sum[:]), nil 447 } 448 449 // DeleteOIDCProvider will delete an OIDC provider. 450 func (s *IAMService) DeleteOIDCProvider(arn *string) error { 451 input := iam.DeleteOpenIDConnectProviderInput{ 452 OpenIDConnectProviderArn: arn, 453 } 454 455 _, err := s.IAMClient.DeleteOpenIDConnectProvider(&input) 456 if err != nil { 457 return errors.Wrap(err, "error deleting provider") 458 } 459 return nil 460 }