diff --git a/README.md b/README.md index fd5f21f8..4c29974b 100644 --- a/README.md +++ b/README.md @@ -894,6 +894,26 @@ Service accounts are best as they can be further constrained and not be associated with your overall authenticated access. Do not ship your own credentials to remote systems. +A service account can be attached to a created instances using the following +configuration: + +``` +(...) + +backends: + google: + key: $(HOST:echo $GOOGLE_JSON_FILENAME) + ... + systems: + - system-with-service-account: + attach-service-account: true +... +``` + +Service account can only be attached to an instance if the authentication key is +of `service_acccount` type, and the IAM role associated with the account has the +necessary permissions. + Images are located by first attempting to match the provided value exactly against the image name, and then some processing is done to verify if an image with the individual tokens in its description exists. Images are diff --git a/spread/google.go b/spread/google.go index d1b08a5b..1c9fe5bb 100644 --- a/spread/google.go +++ b/spread/google.go @@ -16,7 +16,6 @@ import ( "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" - "golang.org/x/oauth2/jwt" "github.com/niemeyer/pretty" "regexp" @@ -44,6 +43,8 @@ type googleProvider struct { client *http.Client + serviceAccount string + mu sync.Mutex keyChecked bool @@ -459,6 +460,20 @@ func (p *googleProvider) createMachine(ctx context.Context, system *System) (*go }, } + if system.AttachServiceAccount { + if p.serviceAccount == "" { + return nil, &FatalError{fmt.Errorf("no service account to attach")} + } + // XXX the service account could be set from google key + // credentials, but the account used in the context of the + // request may not have the permissions to attach a service + // account to the instance + params["serviceAccounts"] = []googleParams{{ + "email": p.serviceAccount, + "scopes": []string{"https://www.googleapis.com/auth/cloud-platform"}, + }} + } + if system.SecureBoot { params["shieldedInstanceConfig"] = googleParams{ "enableSecureBoot": true, @@ -755,6 +770,28 @@ func (p *googleProvider) waitOperation(ctx context.Context, s *googleServer, ver panic("unreachable") } +func serviceAccountFromKey(raw []byte) (string, error) { + const serviceAccountKey = "service_account" + + // taken from golang.org/x/oauth/google + var credentials struct { + Type string `json:"type"` + ClientEmail string `json:"client_email"` + } + + if err := json.Unmarshal(raw, &credentials); err != nil { + return "", err + } + + if credentials.Type != serviceAccountKey { + // email is only relevant if dealing with service_account + // credentials type + return "", nil + } + + return credentials.ClientEmail, nil +} + func (p *googleProvider) checkKey() error { p.mu.Lock() defer p.mu.Unlock() @@ -771,15 +808,29 @@ func (p *googleProvider) checkKey() error { if err == nil && p.client == nil { ctx := context.Background() - if strings.HasPrefix(p.backend.Key, "{") { - var cfg *jwt.Config - cfg, err = google.JWTConfigFromJSON([]byte(p.backend.Key), googleScope) + var creds *google.Credentials + if p.backend.Key != "" { + var raw []byte + if strings.HasPrefix(p.backend.Key, "{") { + // already a raw JSON blob + raw = []byte(p.backend.Key) + } else { + raw, err = ioutil.ReadFile(p.backend.Key) + } + if err == nil { - p.client = oauth2.NewClient(ctx, cfg.TokenSource(ctx)) + creds, err = google.CredentialsFromJSON(ctx, raw, googleScope) } + + // identify service account if possible + p.serviceAccount, err = serviceAccountFromKey(raw) } else { - os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", p.backend.Key) - p.client, err = google.DefaultClient(context.Background(), googleScope) + // none provided, let the google library find whatever + // is appropriate + creds, err = google.FindDefaultCredentials(ctx, googleScope) + } + if err == nil { + p.client = oauth2.NewClient(ctx, creds.TokenSource) } } if err == nil { diff --git a/spread/project.go b/spread/project.go index e70470f0..b8dd2cf1 100644 --- a/spread/project.go +++ b/spread/project.go @@ -139,6 +139,9 @@ type System struct { Priority OptionalInt Manual bool + + // Only for Google + AttachServiceAccount bool `yaml:"attach-service-account"` } func (system *System) String() string { return system.Backend + ":" + system.Name }