github.com/grailbio/base@v0.0.11/cmd/ticket-server/ec2blesser.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache-2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"net"
    10  	"os"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/awserr"
    16  	"github.com/aws/aws-sdk-go/aws/client"
    17  	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
    18  	"github.com/aws/aws-sdk-go/aws/session"
    19  	"github.com/aws/aws-sdk-go/service/dynamodb"
    20  	"github.com/aws/aws-sdk-go/service/ec2"
    21  	"github.com/grailbio/base/cloud/ec2util"
    22  	"github.com/grailbio/base/common/log"
    23  	"v.io/v23/context"
    24  	"v.io/v23/rpc"
    25  	"v.io/v23/security"
    26  )
    27  
    28  const pendingTimeWindow = time.Hour
    29  
    30  // setupEc2Blesser creates the DynamoDB table used for enforcing the uniqueness
    31  // of the EC2-based blessing requests. For each VM we only want to handle
    32  // blessings only to the first request. This prevents replay attacks in the case
    33  // when the EC2 instance document was leaked to an adversary.
    34  //
    35  // The schema of the table is the following:
    36  //
    37  //   ID: (string, hash key) '/'-separated of (account, region, instance, IP)
    38  //   IdentityDocument: (string) JSON of the IdentityDocument from the request
    39  //   DescribeInstance: (string) JSON response for the DescribeInstance call
    40  //   Timestamp: (string) Timestamp in RFC3339Nano when the record was created
    41  func setupEc2Blesser(ctx *context.T, s *session.Session, table string) {
    42  	if table == "" {
    43  		return
    44  	}
    45  
    46  	client := dynamodb.New(s)
    47  	out, err := client.DescribeTable(&dynamodb.DescribeTableInput{
    48  		TableName: aws.String(table),
    49  	})
    50  
    51  	if err == nil {
    52  		log.Error(ctx, "DynamoDB table already exists", "table", out)
    53  		return
    54  	}
    55  
    56  	want := dynamodb.ErrCodeResourceNotFoundException
    57  	if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != want {
    58  		log.Error(ctx, "unexpected DynamoDB error", "got", err, "want", want)
    59  		os.Exit(255)
    60  	}
    61  
    62  	_, err = client.CreateTable(&dynamodb.CreateTableInput{
    63  		TableName: aws.String(table),
    64  		AttributeDefinitions: []*dynamodb.AttributeDefinition{
    65  			{
    66  				AttributeName: aws.String("ID"),
    67  				AttributeType: aws.String("S"),
    68  			},
    69  		},
    70  		KeySchema: []*dynamodb.KeySchemaElement{
    71  			{
    72  				AttributeName: aws.String("ID"),
    73  				KeyType:       aws.String("HASH"),
    74  			},
    75  		},
    76  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
    77  			ReadCapacityUnits:  aws.Int64(int64(1)),
    78  			WriteCapacityUnits: aws.Int64(int64(1)),
    79  		},
    80  	})
    81  	if err != nil {
    82  		log.Error(ctx, err.Error())
    83  		os.Exit(255)
    84  	}
    85  	log.Debug(ctx, "created DynamoDB table", "table", table)
    86  	// TODO(razvanm): wait for the table to reach ACTIVE state?
    87  	// TODO(razvanm): enable the auto scaling?
    88  }
    89  
    90  type ec2Blesser struct {
    91  	ctx                context.T
    92  	expirationInterval time.Duration
    93  	role               string
    94  	table              string
    95  	session            *session.Session
    96  }
    97  
    98  func newEc2Blesser(ctx *context.T, s *session.Session, expiration time.Duration, role string, table string) *ec2Blesser {
    99  	setupEc2Blesser(ctx, s, ec2DynamoDBTableFlag)
   100  	return &ec2Blesser{
   101  		ctx:                *ctx,
   102  		expirationInterval: expiration,
   103  		role:               role,
   104  		table:              table,
   105  		session:            s,
   106  	}
   107  }
   108  
   109  func (blesser *ec2Blesser) checkUniqueness(ctx *context.T, doc *ec2util.IdentityDocument, remoteAddr string, jsonDoc string, jsonInstance string) error {
   110  	if blesser.table == "" {
   111  		return nil
   112  	}
   113  	ipAddr, _, err := net.SplitHostPort(remoteAddr)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	key := strings.Join([]string{doc.AccountID, doc.Region, doc.InstanceID, ipAddr}, "/")
   118  	log.Debug(ctx, "DynamoDB info", "key", key, "remoteAddr", remoteAddr)
   119  	cond := aws.String("attribute_not_exists(ID)")
   120  	if ec2DisableUniquenessCheckFlag {
   121  		cond = nil
   122  	}
   123  	_, err = dynamodb.New(blesser.session).PutItem(&dynamodb.PutItemInput{
   124  		TableName:           aws.String(blesser.table),
   125  		ConditionExpression: cond,
   126  		Item: map[string]*dynamodb.AttributeValue{
   127  			"ID":               {S: aws.String(key)},
   128  			"IdentityDocument": {S: aws.String(jsonDoc)},
   129  			"DescribeInstance": {S: aws.String(jsonInstance)},
   130  			"Timestamp":        {S: aws.String(time.Now().UTC().Format(time.RFC3339Nano))},
   131  		},
   132  	})
   133  	return err
   134  }
   135  
   136  func checkPendingTime(doc *ec2util.IdentityDocument) error {
   137  	pendingTime := doc.PendingTime
   138  	if time.Since(doc.PendingTime) > pendingTimeWindow {
   139  		return fmt.Errorf("launch time is too old: %s should be within %s", pendingTime, pendingTimeWindow)
   140  	}
   141  	return nil
   142  }
   143  
   144  func (blesser *ec2Blesser) BlessEc2(ctx *context.T, call rpc.ServerCall, pkcs7b64 string) (security.Blessings, error) {
   145  	var empty security.Blessings
   146  
   147  	remoteAddress := call.RemoteAddr().String()
   148  	doc, jsonDoc, err := ec2util.ParseAndVerifyIdentityDocument(pkcs7b64)
   149  	log.Info(ctx, "bless EC2 request", "remoteAddr", remoteAddress, "remoteEndpoint", call.RemoteEndpoint().Addr(),
   150  		"pkcs7b64Bytes", len(pkcs7b64), "doc", doc)
   151  	if err != nil {
   152  		log.Error(ctx, "Error parsing and verifying identity document.", "err", err)
   153  		return empty, err
   154  	}
   155  
   156  	if !ec2DisablePendingTimeCheckFlag {
   157  		if err := checkPendingTime(doc); err != nil {
   158  			log.Error(ctx, err.Error())
   159  			return empty, err
   160  		}
   161  	}
   162  
   163  	config := aws.Config{
   164  		Credentials: stscreds.NewCredentials(blesser.session, fmt.Sprintf("arn:aws:iam::%s:role/%s", doc.AccountID, blesser.role)),
   165  		Retryer: client.DefaultRetryer{
   166  			NumMaxRetries: 100,
   167  		},
   168  		Region: aws.String(doc.Region),
   169  	}
   170  	validateRemoteAddr := remoteAddress
   171  	if ec2DisableAddrCheckFlag {
   172  		validateRemoteAddr = ""
   173  	}
   174  
   175  	output, err := ec2.New(blesser.session, &config).DescribeInstances(&ec2.DescribeInstancesInput{
   176  		InstanceIds: []*string{aws.String(doc.InstanceID)},
   177  	})
   178  
   179  	if err != nil {
   180  		log.Error(ctx, err.Error())
   181  		return empty, err
   182  	}
   183  
   184  	role, err := ec2util.ValidateInstance(output, *doc, validateRemoteAddr)
   185  	if err != nil {
   186  		log.Error(ctx, err.Error())
   187  		return empty, err
   188  	}
   189  
   190  	if err = blesser.checkUniqueness(ctx, doc, remoteAddress, jsonDoc, output.String()); err != nil {
   191  		log.Error(ctx, err.Error())
   192  		return empty, err
   193  	}
   194  
   195  	ext := fmt.Sprintf("ec2:%s:%s:%s", doc.AccountID, role, doc.InstanceID)
   196  
   197  	securityCall := call.Security()
   198  	if securityCall.LocalPrincipal() == nil {
   199  		return empty, fmt.Errorf("server misconfiguration: no authentication happened")
   200  	}
   201  
   202  	pubKey := securityCall.RemoteBlessings().PublicKey()
   203  	caveat, err := security.NewExpiryCaveat(time.Now().Add(blesser.expirationInterval))
   204  	// TODO(razvanm): using a PublicKeyThirdPartyCaveat we could also invalidate
   205  	// the older blessings. This will force the clients to talk to the
   206  	// ticket-server more frequently though.
   207  	if err != nil {
   208  		return empty, err
   209  	}
   210  	return securityCall.LocalPrincipal().Bless(pubKey, securityCall.LocalBlessings(), ext, caveat)
   211  }