Add GCP auth helper (#4654)

* update auth plugin vendoring

* add GCP auth helper and docs
This commit is contained in:
emily
2018-05-29 19:36:24 -05:00
committed by Jeff Mitchell
parent 648ea3345f
commit 8568e791dd
9 changed files with 433 additions and 193 deletions

View File

@@ -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{},

View File

@@ -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
}

View File

@@ -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=<string>
Explicitly specified GCP credentials in JSON string format (not recommended)
jwt_exp=<minutes>
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=<string>
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=<string>
Project the service account belongs to. Defaults to credentials
"project_id" if "credentials" specified and this value is not.
`
return strings.TrimSpace(help)
}

View File

@@ -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()
}

View File

@@ -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,7 +100,7 @@ iam AUTH:
// gcpConfig contains all config required for the GCP backend.
type gcpConfig struct {
Credentials *util.GcpCredentials `json:"credentials" structs:"credentials" mapstructure:"credentials"`
Credentials *gcputil.GcpCredentials `json:"credentials" structs:"credentials" mapstructure:"credentials"`
GoogleCertsEndpoint string `json:"google_certs_endpoint" structs:"google_certs_endpoint" mapstructure:"google_certs_endpoint"`
}
@@ -108,7 +108,7 @@ type gcpConfig struct {
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)
}

View File

@@ -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.

View File

@@ -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)
}

6
vendor/vendor.json vendored
View File

@@ -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=",

View File

@@ -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: <required>)` - 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).