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  }