diff --git a/command/commands.go b/command/commands.go index 9e092ed479..553e13aebb 100644 --- a/command/commands.go +++ b/command/commands.go @@ -201,6 +201,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { "aws": &credAws.CLIHandler{}, "centrify": &credCentrify.CLIHandler{}, "cert": &credCert.CLIHandler{}, + "gcp": &credGcp.CLIHandler{}, "github": &credGitHub.CLIHandler{}, "ldap": &credLdap.CLIHandler{}, "okta": &credOkta.CLIHandler{}, diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/backend.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/backend.go index 4460663464..6e011ff229 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/backend.go @@ -7,13 +7,11 @@ import ( "runtime" "sync" - "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/vault-plugin-auth-gcp/plugin/util" + "github.com/hashicorp/go-gcp-common/gcputil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" "github.com/hashicorp/vault/version" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" "google.golang.org/api/compute/v1" "google.golang.org/api/iam/v1" ) @@ -143,15 +141,14 @@ func (b *GcpAuthBackend) initClients(ctx context.Context, s logical.Storage) (er var httpClient *http.Client if config == nil || config.Credentials == nil { - // Use Application Default Credentials - reqCtx := context.WithValue(ctx, oauth2.HTTPClient, cleanhttp.DefaultClient()) - - httpClient, err = google.DefaultClient(reqCtx, b.oauthScopes...) + _, tknSrc, err := gcputil.FindCredentials("", ctx, b.oauthScopes...) if err != nil { return fmt.Errorf("credentials were not configured and fallback to application default credentials failed: %v", err) } + + httpClient = oauth2.NewClient(ctx, tknSrc) } else { - httpClient, err = util.GetHttpClient(config.Credentials, b.oauthScopes...) + httpClient, err = gcputil.GetHttpClient(config.Credentials, b.oauthScopes...) if err != nil { return err } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/cli.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/cli.go new file mode 100644 index 0000000000..dc17f54ccb --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/cli.go @@ -0,0 +1,172 @@ +package gcpauth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-gcp-common/gcputil" + "github.com/hashicorp/vault/api" + "golang.org/x/oauth2" + "google.golang.org/api/iam/v1" + "strconv" + "strings" + "time" +) + +type CLIHandler struct{} + +func getSignedJwt(role string, m map[string]string) (string, error) { + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient()) + + credentials, tokenSource, err := gcputil.FindCredentials(m["credentials"], ctx, iam.CloudPlatformScope) + if err != nil { + return "", fmt.Errorf("could not obtain credentials: %v", err) + } + + httpClient := oauth2.NewClient(ctx, tokenSource) + + serviceAccount, ok := m["service_account"] + if !ok && credentials != nil { + serviceAccount = credentials.ClientEmail + } + if serviceAccount == "" { + return "", errors.New("could not obtain service account from credentials (are you using Application Default Credentials?). You must provide a service account to authenticate as") + } + + project, ok := m["project"] + if !ok { + if credentials != nil { + project = credentials.ProjectId + } else { + project = "-" + } + } + + var ttlMin = int64(defaultIamMaxJwtExpMinutes) + jwtExpStr, ok := m["jwt_exp"] + if ok { + ttlMin, err = strconv.ParseInt(jwtExpStr, 10, 64) + if err != nil { + return "", fmt.Errorf("could not parse jwt_exp '%s' into integer value", jwtExpStr) + } + } + ttl := time.Minute * time.Duration(ttlMin) + + jwtPayload := map[string]interface{}{ + "aud": fmt.Sprintf("http://vault/%s", role), + "sub": serviceAccount, + "exp": time.Now().Add(ttl).Unix(), + } + payloadBytes, err := json.Marshal(jwtPayload) + if err != nil { + return "", fmt.Errorf("could not convert JWT payload to JSON string: %v", err) + } + + jwtReq := &iam.SignJwtRequest{ + Payload: string(payloadBytes), + } + + iamClient, err := iam.New(httpClient) + if err != nil { + return "", fmt.Errorf("could not create IAM client: %v", err) + } + + resourceName := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount) + resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do() + if err != nil { + return "", fmt.Errorf("unable to sign JWT for %s using given Vault credentials: %v", resourceName, err) + } + + return resp.SignedJwt, nil +} + +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { + role, ok := m["role"] + if !ok { + return nil, errors.New("role is required") + } + + mount, ok := m["mount"] + if !ok { + mount = "gcp" + } + + loginToken, err := getSignedJwt(role, m) + if err != nil { + return nil, err + } + + path := fmt.Sprintf("auth/%s/login", mount) + secret, err := c.Logical().Write( + path, + map[string]interface{}{ + "role": role, + "jwt": loginToken, + }) + + if err != nil { + return nil, err + } + if secret == nil { + return nil, fmt.Errorf("empty response from credential provider") + } + + return secret, nil +} + +func (h *CLIHandler) Help() string { + help := ` +Usage: vault login -method=gcp [CONFIG K=V...] + +The GCP credential provider allows you to authenticate with +a GCP IAM service account key. To use it, you provide a valid GCP IAM +credential JSON either explicitly on the command line (not recommended), +or through any GCP Application Default Credentials. + +Authenticate using Application Default Credentials: + + Example: vault login -method=gcp role=my-iam-role + +Authenticate using explicitly passed-in credentials: + + Example: + vault login -method=gcp role=my-iam-role -credentials=@path/to/creds role=my-iam-role + +This tool generates a signed JWT signed using the given credentials. + +Configuration: + + role + Required. The name of the role you're requesting a token for. + + mount=gcp + This is usually provided via the -path flag in the "vault login" command, + but it can be specified here as well. If specified here, it takes + precedence over the value for -path. + + credentials= + Explicitly specified GCP credentials in JSON string format (not recommended) + + jwt_exp= + Time until the generated JWT expires in minutes. + The given IAM role will have a max_jwt_exp field, the + time in minutes that all valid authentication JWTs + must expire within (from time of authentication). + Defaults to 15 minutes, the default max_jwt_exp for a role. + Must be less than an hour. + + service_account= + Service account to generate a JWT for. Defaults to credentials + "client_email" if "credentials" specified and this value is not. + The actual credential must have the "iam.serviceAccounts.signJWT" + permissions on this service account. + + project= + Project the service account belongs to. Defaults to credentials + "project_id" if "credentials" specified and this value is not. +` + + return strings.TrimSpace(help) +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/login_util.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/login_util.go new file mode 100644 index 0000000000..5d6c85453e --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/login_util.go @@ -0,0 +1,27 @@ +package gcpauth + +import ( + "encoding/json" + "fmt" + "github.com/hashicorp/go-gcp-common/gcputil" + "google.golang.org/api/iam/v1" + "time" +) + +func ServiceAccountLoginJwt( + iamClient *iam.Service, exp time.Time, aud, project, serviceAccount string) (*iam.SignJwtResponse, error) { + accountResource := fmt.Sprintf(gcputil.ServiceAccountTemplate, project, serviceAccount) + + payload, err := json.Marshal(map[string]interface{}{ + "sub": serviceAccount, + "aud": aud, + "exp": exp.Unix(), + }) + if err != nil { + return nil, err + } + jwtReq := &iam.SignJwtRequest{ + Payload: string(payload), + } + return iamClient.Projects.ServiceAccounts.SignJwt(accountResource, jwtReq).Do() +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_config.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_config.go index 3fbc6b8613..7a2b7db2d0 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_config.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/hashicorp/vault-plugin-auth-gcp/plugin/util" + "github.com/hashicorp/go-gcp-common/gcputil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" ) @@ -100,15 +100,15 @@ iam AUTH: // gcpConfig contains all config required for the GCP backend. type gcpConfig struct { - Credentials *util.GcpCredentials `json:"credentials" structs:"credentials" mapstructure:"credentials"` - GoogleCertsEndpoint string `json:"google_certs_endpoint" structs:"google_certs_endpoint" mapstructure:"google_certs_endpoint"` + Credentials *gcputil.GcpCredentials `json:"credentials" structs:"credentials" mapstructure:"credentials"` + GoogleCertsEndpoint string `json:"google_certs_endpoint" structs:"google_certs_endpoint" mapstructure:"google_certs_endpoint"` } // Update sets gcpConfig values parsed from the FieldData. func (config *gcpConfig) Update(data *framework.FieldData) error { credentialsJson := data.Get("credentials").(string) if credentialsJson != "" { - creds, err := util.Credentials(credentialsJson) + creds, err := gcputil.Credentials(credentialsJson) if err != nil { return fmt.Errorf("error reading google credentials from given JSON: %v", err) } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_login.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_login.go index 58b2dedbab..c148585d37 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_login.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_login.go @@ -8,7 +8,8 @@ import ( "strings" "time" - "github.com/hashicorp/vault-plugin-auth-gcp/plugin/util" + "github.com/SermoDigital/jose/jws" + "github.com/hashicorp/go-gcp-common/gcputil" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" @@ -117,7 +118,7 @@ type gcpLoginInfo struct { JWTClaims *jwt.Claims // Metadata from a GCE instance identity token. - GceMetadata *util.GCEIdentityMetadata + GceMetadata *gcputil.GCEIdentityMetadata } func (b *GcpAuthBackend) parseAndValidateJwt(ctx context.Context, req *logical.Request, data *framework.FieldData) (*gcpLoginInfo, error) { @@ -156,7 +157,7 @@ func (b *GcpAuthBackend) parseAndValidateJwt(ctx context.Context, req *logical.R // Parse claims and verify signature. baseClaims := &jwt.Claims{} - customClaims := &util.CustomJWTClaims{} + customClaims := &gcputil.CustomJWTClaims{} if err = jwtVal.Claims(key, baseClaims, customClaims); err != nil { return nil, err @@ -197,12 +198,16 @@ func (b *GcpAuthBackend) getSigningKey(ctx context.Context, token *jwt.JSONWebTo return nil, err } - serviceAccountId, err := util.ParseServiceAccountFromIAMJWT(rawToken) + serviceAccountId, err := parseServiceAccountFromIAMJWT(rawToken) if err != nil { return nil, err } - accountKey, err := util.ServiceAccountKey(iamClient, keyId, serviceAccountId, role.ProjectId) + accountKey, err := gcputil.ServiceAccountKey(iamClient, &gcputil.ServiceAccountKeyId{ + Project: role.ProjectId, + EmailOrId: serviceAccountId, + Key: keyId, + }) if err != nil { // Attempt to get a normal Google Oauth cert in case of GCE inferrence. key, err := b.getGoogleOauthCert(ctx, keyId, s) @@ -211,7 +216,7 @@ func (b *GcpAuthBackend) getSigningKey(ctx context.Context, token *jwt.JSONWebTo } return key, nil } - return util.PublicKey(accountKey.PublicKeyData) + return gcputil.PublicKey(accountKey.PublicKeyData) case gceRoleType: return b.getGoogleOauthCert(ctx, keyId, s) default: @@ -219,6 +224,19 @@ func (b *GcpAuthBackend) getSigningKey(ctx context.Context, token *jwt.JSONWebTo } } +// ParseServiceAccountFromIAMJWT parses the service account from the 'sub' claim given a serialized signed JWT. +func parseServiceAccountFromIAMJWT(signedJwt string) (string, error) { + jwtVal, err := jws.ParseJWT([]byte(signedJwt)) + if err != nil { + return "", fmt.Errorf("could not parse service account from JWT 'sub' claim: %v", err) + } + accountId, ok := jwtVal.Claims().Subject() + if !ok { + return "", errors.New("expected 'sub' claim with service account ID or name") + } + return accountId, nil +} + func (b *GcpAuthBackend) getGoogleOauthCert(ctx context.Context, keyId string, s logical.Storage) (interface{}, error) { var certsEndpoint string conf, err := b.config(ctx, s) @@ -229,7 +247,7 @@ func (b *GcpAuthBackend) getGoogleOauthCert(ctx context.Context, keyId string, s certsEndpoint = conf.GoogleCertsEndpoint } - key, err := util.OAuth2RSAPublicKey(keyId, certsEndpoint) + key, err := gcputil.OAuth2RSAPublicKey(keyId, certsEndpoint) if err != nil { return nil, err } @@ -279,7 +297,11 @@ func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request, } // Get service account and make sure it still exists. - serviceAccount, err := util.ServiceAccount(iamClient, loginInfo.ServiceAccountId, role.ProjectId) + accountId := &gcputil.ServiceAccountId{ + Project: role.ProjectId, + EmailOrId: loginInfo.ServiceAccountId, + } + serviceAccount, err := gcputil.ServiceAccount(iamClient, accountId) if err != nil { return nil, err } @@ -335,7 +357,10 @@ func (b *GcpAuthBackend) pathIamRenew(ctx context.Context, req *logical.Request, return errors.New("service account id metadata not associated with auth token, invalid") } - serviceAccount, err := util.ServiceAccount(iamClient, serviceAccountId, role.ProjectId) + serviceAccount, err := gcputil.ServiceAccount(iamClient, &gcputil.ServiceAccountId{ + Project: role.ProjectId, + EmailOrId: serviceAccountId, + }) if err != nil { return fmt.Errorf("cannot find service account %s", serviceAccountId) } @@ -411,7 +436,10 @@ func (b *GcpAuthBackend) pathGceLogin(ctx context.Context, req *logical.Request, return logical.ErrorResponse(fmt.Sprintf(clientErrorTemplate, "IAM", err)), nil } - serviceAccount, err := util.ServiceAccount(iamClient, loginInfo.ServiceAccountId, loginInfo.Role.ProjectId) + serviceAccount, err := gcputil.ServiceAccount(iamClient, &gcputil.ServiceAccountId{ + Project: loginInfo.Role.ProjectId, + EmailOrId: loginInfo.ServiceAccountId, + }) if err != nil { return logical.ErrorResponse(fmt.Sprintf( "Could not find service account '%s' used for GCE metadata token: %s", @@ -466,7 +494,7 @@ func (b *GcpAuthBackend) pathGceRenew(ctx context.Context, req *logical.Request, return fmt.Errorf(clientErrorTemplate, "GCE", err) } - meta, err := util.GetInstanceMetadataFromAuth(req.Auth.Metadata) + meta, err := getInstanceMetadataFromAuth(req.Auth.Metadata) if err != nil { return fmt.Errorf("invalid auth metadata: %v", err) } @@ -487,6 +515,53 @@ func (b *GcpAuthBackend) pathGceRenew(ctx context.Context, req *logical.Request, return nil } +func getInstanceMetadataFromAuth(authMetadata map[string]string) (*gcputil.GCEIdentityMetadata, error) { + meta := &gcputil.GCEIdentityMetadata{} + var ok bool + var err error + + meta.ProjectId, ok = authMetadata["project_id"] + if !ok { + return nil, errors.New("expected 'project_id' field") + } + + meta.Zone, ok = authMetadata["zone"] + if !ok { + return nil, errors.New("expected 'zone' field") + } + + meta.InstanceId, ok = authMetadata["instance_id"] + if !ok { + return nil, errors.New("expected 'instance_id' field") + } + + meta.InstanceName, ok = authMetadata["instance_name"] + if !ok { + return nil, errors.New("expected 'instance_name' field") + } + + // Parse numbers back into int values. + projectNumber, ok := authMetadata["project_number"] + if !ok { + return nil, errors.New("expected 'project_number' field, got %v") + } + meta.ProjectNumber, err = strconv.ParseInt(projectNumber, 10, 64) + if err != nil { + return nil, fmt.Errorf("expected 'project_number' value '%s' to be a int64", projectNumber) + } + + createdAt, ok := authMetadata["instance_creation_timestamp"] + if !ok { + return nil, errors.New("expected 'instance_creation_timestamp' field") + } + meta.CreatedAt, err = strconv.ParseInt(createdAt, 10, 64) + if err != nil { + return nil, fmt.Errorf("expected 'instance_creation_timestamp' value '%s' to be int64", createdAt) + } + + return meta, nil +} + // validateGCEInstance returns an error if the given GCE instance is not authorized for the role. func (b *GcpAuthBackend) authorizeGCEInstance(ctx context.Context, instance *compute.Instance, s logical.Storage, role *gcpRole, zone, serviceAccountId string) error { gceClient, err := b.GCE(ctx, s) @@ -504,27 +579,16 @@ func (b *GcpAuthBackend) authorizeGCEInstance(ctx context.Context, instance *com // Verify that instance is in zone or region if given. if len(role.BoundZone) > 0 { - var zone string - idx := strings.LastIndex(instance.Zone, "zones/") - if idx > 0 { - // Parse zone name from full zone self-link URL. - idx += len("zones/") - zone = instance.Zone[idx:len(instance.Zone)] - } else { - // Expect full zone name to be set as instance zone. - zone = instance.Zone - } - - if zone != role.BoundZone { - return fmt.Errorf("instance is not in role zone '%s'", role.BoundZone) + if !compareResourceNameOrSelfLink(role.BoundZone, instance.Zone, "zones") { + return fmt.Errorf("instance zone %s is not role zone '%s'", instance.Zone, role.BoundZone) } } else if len(role.BoundRegion) > 0 { zone, err := gceClient.Zones.Get(role.ProjectId, zone).Do() if err != nil { return fmt.Errorf("could not verify instance zone '%s' is available for project '%s': %v", zone.Name, role.ProjectId, err) } - if zone.Region != role.BoundRegion { - return fmt.Errorf("zone '%s' is not in region '%s'", zone.Name, zone.Region) + if !compareResourceNameOrSelfLink(role.BoundRegion, zone.Region, "regions") { + return fmt.Errorf("instance zone %s is not in role region '%s'", zone.Name, role.BoundRegion) } } @@ -569,7 +633,10 @@ func (b *GcpAuthBackend) authorizeGCEInstance(ctx context.Context, instance *com return err } - serviceAccount, err := util.ServiceAccount(iamClient, serviceAccountId, role.ProjectId) + serviceAccount, err := gcputil.ServiceAccount(iamClient, &gcputil.ServiceAccountId{ + Project: role.ProjectId, + EmailOrId: serviceAccountId, + }) if err != nil { return fmt.Errorf("could not find service account with id '%s': %v", serviceAccountId, err) } @@ -584,6 +651,17 @@ func (b *GcpAuthBackend) authorizeGCEInstance(ctx context.Context, instance *com return nil } +func compareResourceNameOrSelfLink(expected, actual, collectionId string) bool { + sep := fmt.Sprintf("%s/", collectionId) + if strings.Contains(expected, sep) { + return expected == actual + } + + actTkns := strings.SplitAfter(actual, sep) + actualName := actTkns[len(actTkns)-1] + return expected == actualName +} + const pathLoginHelpSyn = `Authenticates Google Cloud Platform entities with Vault.` const pathLoginHelpDesc = ` Authenticate Google Cloud Platform (GCP) entities. diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_role.go b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_role.go index 8accefe41a..a1c361daf4 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_role.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-gcp/plugin/path_role.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/hashicorp/vault-plugin-auth-gcp/plugin/util" + "github.com/hashicorp/go-gcp-common/gcputil" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" @@ -107,13 +107,14 @@ var gceOnlyFieldSchema map[string]*framework.FieldSchema = map[string]*framework Type: framework.TypeString, Description: ` "gce" roles only. If set, determines the zone that a GCE instance must belong to. If a group is provided, it is assumed -to be a zonal group and the group must belong to this zone.`, +to be a zonal group and the group must belong to this zone. Accepts self-link or zone name.`, }, "bound_region": { Type: framework.TypeString, Description: ` "gce" roles only. If set, determines the region that a GCE instance must belong to. If a group is provided, it is -assumed to be a regional group and the group must belong to this region. If zone is provided, region will be ignored`, +assumed to be a regional group and the group must belong to this region. If zone is provided, region will be ignored. +Either self-link or region name are accepted.`, }, "bound_instance_group": { Type: framework.TypeString, @@ -401,7 +402,7 @@ func (b *GcpAuthBackend) pathRoleEditGceLabels(ctx context.Context, req *logical return logical.ErrorResponse(fmt.Sprintf(errTemplateEditListWrongType, role.RoleType, "labels", gceRoleType)), nil } - labelsToAdd, invalidLabels := util.ParseGcpLabels(toAdd) + labelsToAdd, invalidLabels := gcputil.ParseGcpLabels(toAdd) if len(invalidLabels) > 0 { return logical.ErrorResponse(fmt.Sprintf("given invalid labels to add: %q", invalidLabels)), nil } @@ -669,23 +670,23 @@ func (role *gcpRole) updateIamFields(data *framework.FieldData, op logical.Opera func (role *gcpRole) updateGceFields(data *framework.FieldData, op logical.Operation) error { region, hasRegion := data.GetOk("bound_region") if hasRegion { - role.BoundRegion = region.(string) + role.BoundRegion = strings.TrimSpace(region.(string)) } zone, hasZone := data.GetOk("bound_zone") if hasZone { - role.BoundZone = zone.(string) + role.BoundZone = strings.TrimSpace(zone.(string)) } instanceGroup, ok := data.GetOk("bound_instance_group") if ok { - role.BoundInstanceGroup = instanceGroup.(string) + role.BoundInstanceGroup = strings.TrimSpace(instanceGroup.(string)) } labels, ok := data.GetOk("bound_labels") if ok { var invalidLabels []string - role.BoundLabels, invalidLabels = util.ParseGcpLabels(labels.([]string)) + role.BoundLabels, invalidLabels = gcputil.ParseGcpLabels(labels.([]string)) if len(invalidLabels) > 0 { return fmt.Errorf("invalid labels given: %q", invalidLabels) } diff --git a/vendor/vendor.json b/vendor/vendor.json index b50166f491..4c52586df7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1303,10 +1303,10 @@ "revisionTime": "2018-04-03T19:19:30Z" }, { - "checksumSHA1": "klHF0u2D5rTw60ZcNufHHtO8nSk=", + "checksumSHA1": "CidzvD2Z5a68j+AfUatPOUlHZT4=", "path": "github.com/hashicorp/vault-plugin-auth-gcp/plugin", - "revision": "c1f38c311636440ff37e1f655f9722d3d9c1c0cc", - "revisionTime": "2018-04-08T01:06:05Z" + "revision": "c6d50df183e9848729b204364e1436e85bf7fb26", + "revisionTime": "2018-05-29T23:55:28Z" }, { "checksumSHA1": "ffJQvzbQvmCG/PdaElGSfGnDgNM=", diff --git a/website/source/docs/auth/gcp.html.md b/website/source/docs/auth/gcp.html.md index ae3531ed46..cc91c36c5d 100644 --- a/website/source/docs/auth/gcp.html.md +++ b/website/source/docs/auth/gcp.html.md @@ -19,181 +19,143 @@ Currently supports authentication for: * GCP IAM service accounts (`iam`) * GCE IAM service accounts (`gce`) -## Precursors: +## Quick Links + * [API documentation](/api/auth/gcp/index.html) + * [Usage: Setup](#setup): How to set up the auth backend on the server + * [Usage: Authentication](#authentication): How to authenticate to Vault using this backend + * [Workflows](#authentication-workflow): Authentication and authorization workflows in more detail + +## Setup The following documentation assumes that the method has been [mounted](/docs/plugin/index.html) at `auth/gcp`. +```sh +$ vault auth enable 'gcp' +Success! Enabled gcp auth method at: gcp/ +``` + You must also [enable the following GCP APIs](https://support.google.com/cloud/answer/6158841?hl=en) for your GCP project: * IAM API for both `iam` service accounts and `gce` instances * GCE API for just `gce` instances -The next sections review how the authN/Z workflows work. If you -have already reviewed these sections, here are some quick links to: +There are generally two setup steps: - * [Usage](#usage) - * [API documentation](/api/auth/gcp/index.html) docs. +### 1. Config + +Example: + +```sh +$ vault write auth/gcp/config credentials=@/path/to/creds.json +``` + +This step allows you to explicitly set GCP credentials that Vault (this auth backend) uses. +If credentials are not configured or if the user explicitly sets the config with no credentials, +the Vault server will attempt to infer credentials. In order of preference, the auth method will use +the first credentials it finds from the following: + +* Provided JSON as `credentials` arg +* Environment variables `GOOGLE_CREDENTIALS` or `GOOGLE_CLOUD_KEYFILE_JSON` set to the credentials JSON string +* JSON file `~/.gcp/credentials` +* [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials) + +For the complete list of configuration options, see the [API documentation](/api/auth/gcp/index.html#configure). + +### 2. Roles + +Example: + +```sh +$ vault write auth/gcp/role/dev-iam-role \ + type="iam" \ + project_id="project-123456" \ + policies="dev" \ + bound_service_accounts="serviceaccount1@project1234.iam.gserviceaccount.com,uuid123,..." + +$ vault write auth/gcp/role/prod-gce-role \ + type="gce" \ + project_id="project-123456" \ + policies="prod" \ + bound_zone="us-central1-a" \ + bound_instance_group="my-gce-group" +``` + +Users will login to Vault under roles (Vault-specific, unrelated to GCP IAM roles). +Roles are associated with an authentication type using the `type` parameter. Currently +`iam` or `gce` are acceptable values. The role also manages the [policies](/docs/concepts/policies.html) +the logged-in user will have and a set of constraints on authenticated users. +These are determined by the authenticating entity type and confirmed with GCP. + +For the complete list of role options, please see the [API documentation](/api/auth/gcp/index.html#create-role). ## Authentication +These following examples assume the auth method is enabled at `/auth/gcp` +and use the CLI. For more complete documentation of parameters, see the +[HTTP API documentation](/api/auth/gcp/index.html#login). -### Via the CLI +There are two ways to login using the CLI: -The default path is `/gcp`. If this auth method was enabled at a different -path, specify `auth/my-path/login` instead. +### Client-provided JWT -```text +In this case, you already have the GCE VM identity metadata token +or have obtained a IAM service account JWT, self-signed +or Google-signed using the GCP IAM API method +[projects.serviceAccounts.signJwt](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt). + +```sh $ vault write auth/gcp/login \ role="dev-role" \ jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` -The `role` and `jwt` parameters are required. These map to the name of the role -to login against, and the signed JWT token for authenticating a role -respectively. The format of the provided JWT differs depending on the -authenticating entity. +* `role` is the role to login under +* `jwt` is the GCE metadata token or IAM service account token +See docs on the [IAM workflow](#iam) or [GCE workflow](#gce) to see how to obtain this JWT. -### Via the API - -The default endpoint is `auth/gcp/login`. If this auth method was enabled -at a different path, use that value instead of `gcp`. +### (IAM ONLY) Client-side helper for service account JWT +Because the process to sign a service account JWT can be tedious, +we implemented a helper that can be run from client-side. ```sh -$ curl \ - --request POST \ - --data '{"role": "dev-role", "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}' \ - http://127.0.0.1:8200/v1/auth/gcp/login +$ vault login -method=gcp \ + mount="gcp" + role="dev-role" \ + jwt_exp + credentials="@/path/to/creds.json" \ + project="project-for-authenticating-service-account" \ + service-account="authenticating-service-account@project.iam.gserviceaccounts.com" ``` -The response will contain the token at `auth.client_token`: +This signs a properly formatted service account JWT for you and logs into Vault +directly. For more information, run `vault auth help gcp`. -```json -{ - "auth": { - "client_token": "f33f8c72-924e-11f8-cb43-ac59d697597c", - "accessor": "0e9e354a-520f-df04-6867-ee81cae3d42d", - "policies": [ - "default", - "dev", - "prod" - ], - "metadata": { - "role": "dev-role", - "service_account_email": "dev1@project-123456.iam.gserviceaccount.com", - "service_account_id": "111111111111111111111" - }, - "lease_duration": 2764800, - "renewable": true - } -} -``` +Required Parameters: -## Configuration +- `role` `(string: )` - The role with type=`iam` to login under. -Auth methods must be configured in advance before users or machines can -authenticate. These steps are usually completed by an operator or configuration -management tool. +Optional Parameters: -### Via the CLI - -1. Enable GCP authentication in Vault: - - ```text - $ vault auth enable gcp - ``` - -1. Configure the GCP auth method: - - ```text - $ vault write auth/gcp/config credentials=@/path/to/creds.json - ``` - - Configuration includes GCP credentials Vault will use these to make calls to - GCP APIs. If credentials are not configured or if the user explicitly sets - the config with no credentials, the Vault server will attempt to use - [Application Default - Credentials](https://developers.google.com/identity/protocols/application-default-credentials) - as set on the Vault server. - - For the complete list of configuration options, please see the API - documentation. - -1. Create a role: - - ```text - $ vault write auth/gcp/role/dev-role \ - type="iam" \ - project_id="project-123456" \ - policies="prod,dev" \ - service_accounts="serviceaccount1@project1234.iam.gserviceaccount.com,uuid123,..." - ``` - - Roles are associated with an authentication type/entity and a set of Vault - policies. Roles are configured with constraints specific to the - authentication type, as well as overall constraints and configuration for - the generated auth tokens. - - For the complete list of role options, please see the API documentation. - -### Via the API - -1. Enable GCP authentication in Vault: - - ```sh - $ curl \ - --header "X-Vault-Token: ..." \ - --request POST \ - --data '{"type": "gcp"}' \ - http://127.0.0.1:8200/v1/sys/auth/gcp - ``` - -1. Configure the GCP auth method: - - ```sh - $ curl \ - --header "X-Vault-Token: ..." \ - --request POST \ - --data '{"credentials": "{...}"}' \ - http://127.0.0.1:8200/v1/auth/gcp/config - ``` - -1. Create a role: - - ```sh - $ curl \ - --header "X-Vault-Token: ..." \ - --request POST \ - --data '{"type": "iam", "project_id": "project-123456", ...}' \ - http://127.0.0.1:8200/v1/auth/gcp/role/dev-role - ``` - -### Plugin Setup - -~> The following section is only relevant if you decide to enable the gcp auth -method as an external plugin. The gcp plugin method is integrated into Vault as -a builtin method by default. - -Assuming you have saved the binary `vault-plugin-auth-gcp` to some folder and -configured the [plugin directory](/docs/internals/plugins.html#plugin-directory) -for your server at `path/to/plugins`: - - -1. Enable the plugin in the catalog: - - ```text - $ vault write sys/plugins/catalog/gcp-auth \ - command="vault-plugin-auth-gcp" \ - sha_256="..." - ``` - -1. Enable the gcp auth method as a plugin: - - ```text - $ vault auth enable -path=gcp -plugin-name=gcp-auth plugin - ``` +* `mount` (`string`: ``"gcp"`) - Name of the role. +* `jwt_exp` (`int`: `15`) - Number of minutes within which the JWT should expire. + This defaults to the default value used when creating a role, and should only be set + if set differently in the role (i.e. if the admin setting up your Vault server only accepts + JWTs that expire within < 15 min). +* `credentials`: If not provided, attempts to use Application Default Credentials. + These credentials are ONLY used to sign the service account JWT, i.e. call API + method [projects.serviceAccounts.signJwt](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt), + and thus must have IAM permission `iam.serviceAccounts.signJWT` or role `iam.serviceAccountTokenCreator` + on the `service_account`, even if they are the same service account (**NOTE** this permission is not enabled by default). + ``` +* `service_account` `(string: `""`)`: Service account to sign JWT for i.e. login to Vault as + and is listed under roles `bound_service_accounts`. If not provided, uses `client_email` + from the credentials JSON. Only provide if the credentials are for a different account, acting as a "signer". +* `project`: Project of service account. If not provided, attempts to use `project_id` from the credentials JSON. + Fails if not found and not provided by Application Default Credentials. Only provide if the credentials are for a + different account, acting as a "signer". ## Authentication Workflow @@ -263,6 +225,8 @@ for the identity metadata token. The params the user provides are: #### Generating IAM Token +If you don't want to use the CLI helper, you can generate the JWT using the following methods. + **HTTP Request Example** This uses [Google API HTTP annotation](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto).