github.com/jenkins-x/jx/v2@v2.1.155/pkg/cloud/amazon/vault/vault_resources.go (about) 1 package vault 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "path/filepath" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws/request" 11 12 "github.com/aws/aws-sdk-go/service/iam" 13 14 "github.com/google/uuid" 15 16 "github.com/aws/aws-sdk-go/aws" 17 "github.com/aws/aws-sdk-go/aws/session" 18 "github.com/aws/aws-sdk-go/service/cloudformation" 19 goformation "github.com/awslabs/goformation/cloudformation" 20 "github.com/awslabs/goformation/cloudformation/resources" 21 "github.com/jenkins-x/jx-logging/pkg/log" 22 "github.com/jenkins-x/jx/v2/pkg/util" 23 "github.com/pkg/errors" 24 ) 25 26 const stackNamePrefix = "jenkins-x-vault-stack" 27 28 const ( 29 readCapacityUnits = 2 30 writeCapacityUnits = 2 31 vaultCloudFormationTemplateName = "vault_cf_tmpl.yml" 32 resourceSuffixParamName = "ResourcesSuffixParameter" 33 iamUserParamName = "IAMUser" 34 dynamoDBTableNameParamName = "DynamoDBTableName" 35 s3BucketNameParamName = "S3BucketName" 36 ) 37 38 // WaitConfigFunc function that configures a waiter for an AWS request with a 10 minute timeout 39 func WaitConfigFunc(waiter *request.Waiter) { 40 waiter.Delay = request.ConstantWaiterDelay(30 * time.Second) 41 waiter.MaxAttempts = 20 42 } 43 44 // ResourceCreationOpts The input parameters to create a vault by default on aws 45 type ResourceCreationOpts struct { 46 Region string 47 Domain string 48 Username string 49 TableName string 50 BucketName string 51 AWSTemplatesDir string 52 UniqueSuffix string 53 AccessKeyID string 54 SecretAccessKey string 55 } 56 57 // StackOutputs the CloudFormation stack outputs for Vault 58 type StackOutputs struct { 59 KMSKeyARN *string 60 S3BucketARN *string 61 DynamoDBTableARN *string 62 } 63 64 // CreateVaultResources will automatically create the prerequisites for vault on aws 65 // Deprecated 66 // Will be deleted once we don't support jx install 67 func CreateVaultResources(vaultParams ResourceCreationOpts) (*string, *string, *string, *string, *string, error) { 68 log.Logger().Infof("Creating vault prerequisite resources with following values, %s, %s, %s, %s", 69 util.ColorInfo(vaultParams.Region), 70 util.ColorInfo(vaultParams.Domain), 71 util.ColorInfo(vaultParams.Username), 72 util.ColorInfo(vaultParams.TableName)) 73 74 valueUUID, err := uuid.NewUUID() 75 if err != nil { 76 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating UUID failed") 77 } 78 79 // Create suffix to apply to resources 80 suffixString := valueUUID.String()[:7] 81 82 template := goformation.NewTemplate() 83 84 awsDynamoDbKey := "AWSDynamoDBTable" 85 dynamoDbTableName := vaultParams.TableName + "_" + suffixString 86 dynamoDbTable := createDynamoDbTable(dynamoDbTableName) 87 template.Resources[awsDynamoDbKey] = &dynamoDbTable 88 89 awsS3BucketKey := "AWSS3Bucket" 90 s3Name, s3Bucket := createS3Bucket(vaultParams.Region, vaultParams.Domain, suffixString) 91 template.Resources[awsS3BucketKey] = s3Bucket 92 93 awsIamUserKey := "AWSIAMUser" 94 iamUsername := vaultParams.Username + suffixString 95 iamUser := createIamUser(iamUsername) 96 template.Resources[awsIamUserKey] = &iamUser 97 98 awsKmsKey := "AWSKMSKey" 99 kmsKey, err := createKmsKey(iamUsername, []string{awsIamUserKey}) 100 if err != nil { 101 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating the vault CloudFormation template failed") 102 } 103 template.Resources[awsKmsKey] = kmsKey 104 105 awsIamPolicy := "AWSIAMPolicy" 106 policy := createIamUserPolicy(iamUsername, []string{awsDynamoDbKey, awsS3BucketKey, awsKmsKey, awsIamUserKey}) 107 template.Resources[awsIamPolicy] = &policy 108 109 log.Logger().Debugf("Generating the vault CloudFormation template") 110 111 // and also the YAML AWS CloudFormation template 112 yaml, err := template.JSON() 113 if err != nil { 114 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating the vault CloudFormation template failed") 115 } 116 117 log.Logger().Debugf("Generated the vault CloudFormation template successfully") 118 119 yamlProcessed := string(yaml) 120 yamlProcessed = setProperIntrinsics(yamlProcessed) 121 122 // Create dynamic stack name 123 stackName := stackNamePrefix + suffixString 124 125 err = runCloudFormationTemplate(&yamlProcessed, &stackName, nil) 126 if err != nil { 127 return nil, nil, nil, nil, nil, errors.Wrap(err, "there was a problem running the Vault CloudFormation stack") 128 } 129 130 kmsID, err := getKmsID(awsKmsKey, stackName) 131 if err != nil { 132 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating the vault CloudFormation template failed") 133 } 134 135 accessKey, keySecret, err := createAccessKey(iamUsername) 136 if err != nil { 137 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating the vault CloudFormation template failed") 138 } 139 140 return accessKey, keySecret, kmsID, s3Name, &dynamoDbTableName, nil 141 } 142 143 // CreateVaultResourcesBoot creates required Vault resources in AWS for a boot cluster 144 func CreateVaultResourcesBoot(vaultParams ResourceCreationOpts) (*string, *string, *string, *string, *string, error) { 145 log.Logger().Infof("Creating vault resources with following values, %s, %s, %s, %s", 146 util.ColorInfo(vaultParams.Region), 147 util.ColorInfo(vaultParams.Username), 148 util.ColorInfo(vaultParams.TableName), 149 util.ColorInfo(vaultParams.BucketName)) 150 151 templatePath := filepath.Join(vaultParams.AWSTemplatesDir, vaultCloudFormationTemplateName) 152 log.Logger().Debugf("Attempting to read Vault CloudFormation template from path %s", templatePath) 153 exists, err := util.FileExists(templatePath) 154 if err != nil { 155 return nil, nil, nil, nil, nil, errors.Wrap(err, "there was a problem loading the vault_cf_tmpl.yml file") 156 } else if !exists { 157 return nil, nil, nil, nil, nil, fmt.Errorf("vault cloud formation template %s doesn't exist", templatePath) 158 } 159 templateBytes, err := ioutil.ReadFile(templatePath) 160 if err != nil { 161 return nil, nil, nil, nil, nil, err 162 } 163 stackName := stackNamePrefix + vaultParams.UniqueSuffix 164 err = runCloudFormationTemplate(aws.String(string(templateBytes)), aws.String(stackName), []*cloudformation.Parameter{ 165 { 166 ParameterKey: aws.String(resourceSuffixParamName), 167 ParameterValue: aws.String(vaultParams.UniqueSuffix), 168 }, 169 { 170 ParameterKey: aws.String(iamUserParamName), 171 ParameterValue: aws.String(vaultParams.Username), 172 }, 173 { 174 ParameterKey: aws.String(dynamoDBTableNameParamName), 175 ParameterValue: aws.String(vaultParams.TableName), 176 }, 177 { 178 ParameterKey: aws.String(s3BucketNameParamName), 179 ParameterValue: aws.String(vaultParams.BucketName), 180 }, 181 }) 182 if err != nil { 183 return nil, nil, nil, nil, nil, errors.Wrapf(err, "executing the Vault CloudFormation ") 184 } 185 186 vaultStackOutputs, err := extractVaultStackOutputs(stackName) 187 if err != nil { 188 return nil, nil, nil, nil, nil, err 189 } 190 191 if vaultParams.AccessKeyID == "" || vaultParams.SecretAccessKey == "" { 192 log.Logger().Info("Creating secret access keys") 193 accessKey, keySecret, err := createAccessKey(vaultParams.Username) 194 if err != nil { 195 return nil, nil, nil, nil, nil, errors.Wrapf(err, "generating the vault CloudFormation template failed") 196 } 197 vaultParams.AccessKeyID = *accessKey 198 vaultParams.SecretAccessKey = *keySecret 199 } 200 201 return aws.String(vaultParams.AccessKeyID), aws.String(vaultParams.SecretAccessKey), vaultStackOutputs.KMSKeyARN, vaultStackOutputs.S3BucketARN, vaultStackOutputs.DynamoDBTableARN, nil 202 } 203 204 /* 205 Currently there was is no way to bypass the intrinsic functions 206 If Fn::Sub (with double colon) is used it is evaluated and removed from the templat output 207 However the evaluation did not seem to work and evaluated ${Name} based values back to "${Name}" 208 As a workaround Fn:Sub is used instead. 209 Reference : https://github.com/awslabs/goformation/issues/62 210 211 Add any more intrinsics here if required. 212 */ 213 func setProperIntrinsics(yamlTemplate string) string { 214 var yamlProcessed string 215 yamlProcessed = strings.Replace(yamlTemplate, "Fn:Sub", "Fn::Sub", -1) 216 return yamlProcessed 217 } 218 219 func getKmsID(kmsOutputName string, stackName string) (*string, error) { 220 log.Logger().Debugf("Retrieving the vault kms id") 221 sess := session.Must(session.NewSessionWithOptions(session.Options{ 222 SharedConfigState: session.SharedConfigEnable, 223 })) 224 225 svc := cloudformation.New(sess) 226 227 desInput := &cloudformation.DescribeStackResourceInput{ 228 LogicalResourceId: aws.String(kmsOutputName), 229 StackName: aws.String(stackName), 230 } 231 output, err := svc.DescribeStackResource(desInput) 232 233 if err != nil { 234 return nil, errors.Wrapf(err, "unable to retrieve kms Id") 235 } 236 237 log.Logger().Debugf("Retrieved the vault kms id successfully") 238 return output.StackResourceDetail.PhysicalResourceId, nil 239 } 240 241 func createDynamoDbTable(tableName string) resources.AWSDynamoDBTable { 242 provisionedThroughput := &resources.AWSDynamoDBTable_ProvisionedThroughput{ 243 ReadCapacityUnits: readCapacityUnits, 244 WriteCapacityUnits: writeCapacityUnits, 245 } 246 247 keyList := []resources.AWSDynamoDBTable_KeySchema{ 248 { 249 AttributeName: "Path", 250 KeyType: "HASH", 251 }, 252 { 253 AttributeName: "Key", 254 KeyType: "RANGE", 255 }, 256 } 257 258 attributeDefinitions := []resources.AWSDynamoDBTable_AttributeDefinition{ 259 { 260 AttributeName: "Path", 261 AttributeType: "S", 262 }, 263 { 264 AttributeName: "Key", 265 AttributeType: "S", 266 }, 267 } 268 269 configuation := resources.AWSDynamoDBTable{ 270 TableName: tableName, 271 ProvisionedThroughput: provisionedThroughput, 272 KeySchema: keyList, 273 AttributeDefinitions: attributeDefinitions, 274 Tags: []resources.Tag{ 275 { 276 Key: "Name", 277 Value: "vault-dynamo-db-table", 278 }, 279 }, 280 } 281 282 return configuation 283 } 284 285 func createS3Bucket(region string, domain string, suffixString string) (*string, *resources.AWSS3Bucket) { 286 bucketName := "vault-unseal." + region + "." + domain + "." + suffixString 287 log.Logger().Debugf(bucketName) 288 289 bucketConfig := resources.AWSS3Bucket{ 290 AccessControl: "Private", 291 BucketName: bucketName, 292 VersioningConfiguration: &resources.AWSS3Bucket_VersioningConfiguration{ 293 Status: "Suspended", 294 }, 295 } 296 297 return &bucketName, &bucketConfig 298 } 299 300 func createKmsKey(username string, depends []string) (*resources.AWSKMSKey, error) { 301 type FnSubWrapper struct { 302 FnSub string `json:"Fn:Sub"` 303 } 304 305 type Principal struct { 306 AWS []FnSubWrapper 307 } 308 309 type PolicyDocument struct { 310 Sid string 311 Effect string 312 Action string 313 Resource string 314 Principal Principal 315 } 316 317 type PolicyRoot struct { 318 Version string 319 Statement []PolicyDocument 320 } 321 322 document := PolicyRoot{ 323 Version: "2012-10-17", 324 Statement: []PolicyDocument{ 325 { 326 Sid: "Enable IAM User Permissions", 327 Effect: "Allow", 328 Principal: Principal{ 329 AWS: []FnSubWrapper{ 330 { 331 FnSub: "arn:aws:iam::${AWS::AccountId}:root", 332 }, 333 { 334 FnSub: "arn:aws:iam::${AWS::AccountId}:user/" + username, 335 }, 336 }, 337 }, 338 Action: "kms:*", 339 Resource: "*", 340 }, 341 }, 342 } 343 344 kmsKey := resources.AWSKMSKey{ 345 Description: "KMS Key for bank vault unseal", 346 KeyPolicy: document, 347 } 348 349 kmsKey.SetDependsOn(depends) 350 351 return &kmsKey, nil 352 } 353 354 func createIamUser(username string) resources.AWSIAMUser { 355 iamUser := resources.AWSIAMUser{ 356 UserName: username, 357 } 358 return iamUser 359 } 360 361 func createIamUserPolicy(username string, depends []string) resources.AWSIAMPolicy { 362 type FnSubWrapper struct { 363 FnSub string `json:"Fn:Sub"` 364 } 365 366 type PolicyDocument struct { 367 Sid string 368 Effect string 369 Action []string 370 Resource FnSubWrapper 371 } 372 373 type PolicyRoot struct { 374 Version string 375 Statement []PolicyDocument 376 } 377 378 policyName := "vault_" + username 379 380 policyDocument := resources.AWSIAMPolicy{ 381 PolicyName: policyName, 382 Users: []string{username}, 383 PolicyDocument: PolicyRoot{ 384 Version: "2012-10-17", 385 Statement: []PolicyDocument{ 386 { 387 Sid: "DynamoDB", 388 Effect: "Allow", 389 Action: []string{ 390 "dynamodb:DescribeLimits", 391 "dynamodb:DescribeTimeToLive", 392 "dynamodb:ListTagsOfResource", 393 "dynamodb:DescribeReservedCapacityOfferings", 394 "dynamodb:DescribeReservedCapacity", 395 "dynamodb:ListTables", 396 "dynamodb:BatchGetItem", 397 "dynamodb:BatchWriteItem", 398 "dynamodb:CreateTable", 399 "dynamodb:DeleteItem", 400 "dynamodb:GetItem", 401 "dynamodb:GetRecords", 402 "dynamodb:PutItem", 403 "dynamodb:Query", 404 "dynamodb:UpdateItem", 405 "dynamodb:Scan", 406 "dynamodb:DescribeTable", 407 }, 408 Resource: FnSubWrapper{ 409 FnSub: "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*", 410 }, 411 }, 412 { 413 Sid: "S3", 414 Effect: "Allow", 415 Action: []string{ 416 "s3:PutObject", 417 "s3:GetObject", 418 }, 419 Resource: FnSubWrapper{ 420 FnSub: "${" + depends[1] + ".Arn}/*", 421 }, 422 }, 423 { 424 Sid: "S3List", 425 Effect: "Allow", 426 Action: []string{ 427 "s3:ListBucket", 428 }, 429 Resource: FnSubWrapper{ 430 FnSub: "${" + depends[1] + ".Arn}", 431 }, 432 }, 433 { 434 Sid: "KMS", 435 Effect: "Allow", 436 Action: []string{ 437 "kms:Encrypt", 438 "kms:Decrypt", 439 }, 440 Resource: FnSubWrapper{ 441 FnSub: "${" + depends[2] + ".Arn}", 442 }, 443 }, 444 }, 445 }, 446 } 447 448 policyDocument.SetDependsOn(depends) 449 450 return policyDocument 451 } 452 453 func runCloudFormationTemplate(templateBody *string, stackName *string, parameters []*cloudformation.Parameter) error { 454 455 sess := session.Must(session.NewSessionWithOptions(session.Options{ 456 SharedConfigState: session.SharedConfigEnable, 457 })) 458 459 // Create CloudFormation client in region 460 svc := cloudformation.New(sess) 461 462 desInput := &cloudformation.DescribeStacksInput{StackName: stackName} 463 464 input := &cloudformation.CreateStackInput{ 465 TemplateBody: templateBody, 466 StackName: stackName, 467 Capabilities: []*string{ 468 aws.String("CAPABILITY_NAMED_IAM"), 469 }, 470 Parameters: parameters, 471 } 472 createOutput, err := svc.CreateStack(input) 473 if err != nil { 474 return errors.Wrapf(err, "unable to create vault prerequisite resources") 475 } 476 477 log.Logger().Info("Vault CloudFormation stack created") 478 cloudFormationURL := fmt.Sprintf("https://console.aws.amazon.com/cloudformation/home?region=%s#/stacks/stackinfo?stackId=%s", *sess.Config.Region, *createOutput.StackId) 479 log.Logger().Infof("You can watch progress in the CloudFormation console: %s", util.ColorInfo(cloudFormationURL)) 480 481 // Wait until stack is created 482 err = svc.WaitUntilStackCreateCompleteWithContext(aws.BackgroundContext(), desInput, WaitConfigFunc) 483 if err != nil { 484 return errors.Wrapf(err, "unable to create vault prerequisite resources") 485 } 486 log.Logger().Debugf("Ran the vault CloudFormation template successfully") 487 return nil 488 } 489 490 func extractVaultStackOutputs(stackName string) (*StackOutputs, error) { 491 sess := session.Must(session.NewSessionWithOptions(session.Options{ 492 SharedConfigState: session.SharedConfigEnable, 493 })) 494 495 // Create CloudFormation client in region 496 svc := cloudformation.New(sess) 497 498 desInput := &cloudformation.DescribeStacksInput{StackName: aws.String(stackName)} 499 500 output, err := svc.DescribeStacks(desInput) 501 if err != nil { 502 return nil, err 503 } 504 vsp := &StackOutputs{} 505 if len(output.Stacks) > 0 { 506 stack := output.Stacks[0] 507 for _, v := range stack.Outputs { 508 switch *v.OutputKey { 509 case "AWSS3Bucket": 510 vsp.S3BucketARN = v.OutputValue 511 case "AWSKMSKey": 512 vsp.KMSKeyARN = v.OutputValue 513 case "AWSDynamoDBTable": 514 vsp.DynamoDBTableARN = v.OutputValue 515 default: 516 log.Logger().Warnf("CloudFormation parameter %s not expected", *v.OutputKey) 517 } 518 } 519 } 520 return vsp, nil 521 } 522 523 /** 524 We can create an access key using cloudformation, however this results the secret being logged. 525 There is supposed to be a way using a custom type but for now using this 526 */ 527 func createAccessKey(username string) (*string, *string, error) { 528 sess, err := session.NewSession() 529 if err != nil { 530 return nil, nil, err 531 } 532 svc := iam.New(sess) 533 input := &iam.CreateAccessKeyInput{ 534 UserName: aws.String(username), 535 } 536 result, err := svc.CreateAccessKey(input) 537 538 if err != nil { 539 return nil, nil, errors.Wrapf(err, "unable to create access key") 540 } 541 542 log.Logger().Debugf("Created the vault access key successfully") 543 return result.AccessKey.AccessKeyId, result.AccessKey.SecretAccessKey, nil 544 }