Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIF support for GCP auth engine #204

Merged
merged 6 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Next

IMPROVEMENTS:
* Added support for Workload Identity Federation [GH-204](https://github.com/hashicorp/vault-plugin-auth-gcp/pull/204)

## v0.16.3

IMPROVEMENTS:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/go-jose/go-jose/v4 v4.0.1
github.com/golang/mock v1.6.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-gcp-common v0.8.0
github.com/hashicorp/go-gcp-common v0.9.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-gcp-common v0.8.0 h1:/2vGAbCU1v+BZ3YHXTCzTvxqma9WOJHYtADTfhZixLo=
github.com/hashicorp/go-gcp-common v0.8.0/go.mod h1:Q7zYRy9ue9SuaEN2s9YLIQs4SoKHdoRmKRcImY3SLgs=
github.com/hashicorp/go-gcp-common v0.9.0 h1:dabqPrA+vlNWcyQV/3yOI6WCmQGFJgwyztDEsqDp+Q0=
github.com/hashicorp/go-gcp-common v0.9.0/go.mod h1:aZnN6BVMqryPo4vIy97ZAYSoREnJWilLMmaOmi5P7vY=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
Expand Down
55 changes: 54 additions & 1 deletion plugin/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import (
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault-plugin-auth-gcp/plugin/cache"
"github.com/hashicorp/go-gcp-common/gcputil"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/helper/useragent"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/externalaccount"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/compute/v1"
"google.golang.org/api/iam/v1"
"google.golang.org/api/option"

"github.com/hashicorp/vault-plugin-auth-gcp/plugin/cache"
)

const (
Expand Down Expand Up @@ -238,6 +243,15 @@ func (b *GcpAuthBackend) credentials(ctx context.Context, s logical.Storage) (*g
if err != nil {
return nil, fmt.Errorf("failed to parse credentials: %w", err)
}
} else if config.IdentityTokenAudience != "" {
ts := &PluginIdentityTokenSupplier{
sys: b.System(),
logger: b.Logger(),
audience: config.IdentityTokenAudience,
ttl: config.IdentityTokenTTL,
}

creds, err = b.GetExternalAccountConfig(config, ts).GetExternalAccountCredentials(ctx)
} else {
creds, err = google.FindDefaultCredentials(ctx, iam.CloudPlatformScope)
if err != nil {
Expand All @@ -253,6 +267,45 @@ func (b *GcpAuthBackend) credentials(ctx context.Context, s logical.Storage) (*g
return creds.(*google.Credentials), nil
}

func (b *GcpAuthBackend) GetExternalAccountConfig(c *gcpConfig, ts *PluginIdentityTokenSupplier) *gcputil.ExternalAccountConfig {
b.Logger().Debug("adding web identity token fetcher")
cfg := &gcputil.ExternalAccountConfig{
ServiceAccountEmail: c.ServiceAccountEmail,
Audience: c.IdentityTokenAudience,
TTL: c.IdentityTokenTTL,
TokenSupplier: ts,
}

return cfg
}

type PluginIdentityTokenSupplier struct {
sys logical.SystemView
logger hclog.Logger
audience string
ttl time.Duration
}

var _ externalaccount.SubjectTokenSupplier = (*PluginIdentityTokenSupplier)(nil)

func (p *PluginIdentityTokenSupplier) SubjectToken(ctx context.Context, opts externalaccount.SupplierOptions) (string, error) {
p.logger.Info("fetching new plugin identity token")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would make this debug level or maybe even trace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opted to keep this particular line at the Info level to be consistent with the AWS and Azure implementations

resp, err := p.sys.GenerateIdentityToken(ctx, &pluginutil.IdentityTokenRequest{
Audience: p.audience,
TTL: p.ttl,
})
if err != nil {
return "", fmt.Errorf("failed to generate plugin identity token: %w", err)
}

if resp.TTL < p.ttl {
p.logger.Debug("generated plugin identity token has shorter TTL than requested",
"requested", p.ttl.Seconds(), "actual", resp.TTL)
}

return resp.Token.Token(), nil
}

// ClearCaches deletes all cached clients and credentials.
func (b *GcpAuthBackend) ClearCaches() {
b.cache.Clear()
Expand Down
12 changes: 9 additions & 3 deletions plugin/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,15 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
mount = "gcp"
}

loginToken, err := getSignedJwt(role, m)
if err != nil {
return nil, err
var loginToken string
var err error
if v, ok := m["jwt"]; ok {
loginToken = v
} else {
loginToken, err = getSignedJwt(role, m)
if err != nil {
return nil, err
}
}

path := fmt.Sprintf("auth/%s/login", mount)
Expand Down
23 changes: 23 additions & 0 deletions plugin/gcp_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/go-gcp-common/gcputil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/authmetadata"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"google.golang.org/api/compute/v1"
"google.golang.org/api/iam/v1"
)
Expand All @@ -32,6 +33,9 @@ type gcpConfig struct {
CRMCustomEndpoint string `json:"crm_custom_endpoint"`
// ComputeCustomEndpoint overrides the service endpoint for compute.googleapis.com
ComputeCustomEndpoint string `json:"compute_custom_endpoint"`

pluginidentityutil.PluginIdentityTokenParams
ServiceAccountEmail string `json:"service_account_email"`
}

// standardizedCreds wraps gcputil.GcpCredentials with a type to allow
Expand Down Expand Up @@ -120,6 +124,25 @@ func (c *gcpConfig) Update(d *framework.FieldData) error {
}
}

// set plugin identity token fields
if err := c.ParsePluginIdentityTokenFields(d); err != nil {
return err
}

// set Service Account email
saEmail, ok := d.GetOk("service_account_email")
if ok {
c.ServiceAccountEmail = saEmail.(string)
}

if c.IdentityTokenAudience != "" && c.Credentials != nil {
return fmt.Errorf("only one of 'credentials' or 'identity_token_audience' can be set")
}

if c.IdentityTokenAudience != "" && c.ServiceAccountEmail == "" {
return fmt.Errorf("missing required 'service_account_email' when 'identity_token_audience' is set")
}

return nil
}

Expand Down
32 changes: 31 additions & 1 deletion plugin/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ package gcpauth

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/authmetadata"
"github.com/hashicorp/vault/sdk/helper/pluginidentityutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
)

Expand Down Expand Up @@ -49,7 +52,7 @@ var (
)

func pathConfig(b *GcpAuthBackend) *framework.Path {
return &framework.Path{
p := &framework.Path{
Pattern: "config",

DisplayAttrs: &framework.DisplayAttributes{
Expand Down Expand Up @@ -89,6 +92,10 @@ If not specified, will use application default credentials`,
Deprecated. This field does nothing and be removed in a future release`,
Deprecated: true,
},
"service_account_email": {
Type: framework.TypeString,
Description: `Email ID for the Service Account to impersonate for Workload Identity Federation.`,
},
},

Operations: map[logical.Operation]framework.OperationHandler{
Expand Down Expand Up @@ -118,6 +125,10 @@ iam AUTH:
* iam.serviceAccountKeys.get
`,
}

pluginidentityutil.AddPluginIdentityTokenFields(p.Fields)

return p
}

func (b *GcpAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand All @@ -134,6 +145,19 @@ func (b *GcpAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
return nil, logical.CodedError(http.StatusBadRequest, err.Error())
}

// generate token to check if WIF is enabled on this edition of Vault
if c.IdentityTokenAudience != "" {
_, err := b.System().GenerateIdentityToken(ctx, &pluginutil.IdentityTokenRequest{
Audience: c.IdentityTokenAudience,
})
if err != nil {
if errors.Is(err, pluginidentityutil.ErrPluginWorkloadIdentityUnsupported) {
return logical.ErrorResponse(err.Error()), nil
}
return nil, err
}
}

// Create/update the storage entry
entry, err := logical.StorageEntryJSON("config", c)
if err != nil {
Expand Down Expand Up @@ -205,6 +229,12 @@ func (b *GcpAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
resp["custom_endpoint"] = endpoints
}

if v := config.ServiceAccountEmail; v != "" {
resp["service_account_email"] = v
}

config.PopulatePluginIdentityTokenData(resp)

return &logical.Response{
Data: resp,
}, nil
Expand Down
Loading
Loading