open-vault/command/agent/auth/gcp/gcp.go
Jeff Mitchell f7147025dd
Migrate to sdk/internalshared libs in go-secure-stdlib (#12090)
* Swap sdk/helper libs to go-secure-stdlib

* Migrate to go-secure-stdlib reloadutil

* Migrate to go-secure-stdlib kv-builder

* Migrate to go-secure-stdlib gatedwriter
2021-07-15 20:17:31 -04:00

234 lines
5.9 KiB
Go

package gcp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-gcp-common/gcputil"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
"golang.org/x/oauth2"
"google.golang.org/api/iamcredentials/v1"
)
const (
typeGCE = "gce"
typeIAM = "iam"
identityEndpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/%s/identity"
defaultIamMaxJwtExpMinutes = 15
)
type gcpMethod struct {
logger hclog.Logger
authType string
mountPath string
role string
credentials string
serviceAccount string
project string
jwtExp int64
}
func NewGCPAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
var err error
g := &gcpMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
serviceAccount: "default",
}
typeRaw, ok := conf.Config["type"]
if !ok {
return nil, errors.New("missing 'type' value")
}
g.authType, ok = typeRaw.(string)
if !ok {
return nil, errors.New("could not convert 'type' config value to string")
}
roleRaw, ok := conf.Config["role"]
if !ok {
return nil, errors.New("missing 'role' value")
}
g.role, ok = roleRaw.(string)
if !ok {
return nil, errors.New("could not convert 'role' config value to string")
}
switch {
case g.role == "":
return nil, errors.New("'role' value is empty")
case g.authType == "":
return nil, errors.New("'type' value is empty")
case g.authType != typeGCE && g.authType != typeIAM:
return nil, errors.New("'type' value is invalid")
}
credentialsRaw, ok := conf.Config["credentials"]
if ok {
g.credentials, ok = credentialsRaw.(string)
if !ok {
return nil, errors.New("could not convert 'credentials' value into string")
}
}
serviceAccountRaw, ok := conf.Config["service_account"]
if ok {
g.serviceAccount, ok = serviceAccountRaw.(string)
if !ok {
return nil, errors.New("could not convert 'service_account' value into string")
}
}
projectRaw, ok := conf.Config["project"]
if ok {
g.project, ok = projectRaw.(string)
if !ok {
return nil, errors.New("could not convert 'project' value into string")
}
}
jwtExpRaw, ok := conf.Config["jwt_exp"]
if ok {
g.jwtExp, err = parseutil.ParseInt(jwtExpRaw)
if err != nil {
return nil, fmt.Errorf("error parsing 'jwt_raw' into integer: %w", err)
}
}
return g, nil
}
func (g *gcpMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, header http.Header, retData map[string]interface{}, retErr error) {
g.logger.Trace("beginning authentication")
data := make(map[string]interface{})
var jwt string
switch g.authType {
case typeGCE:
httpClient := cleanhttp.DefaultClient()
// Fetch token
{
req, err := http.NewRequest("GET", fmt.Sprintf(identityEndpoint, g.serviceAccount), nil)
if err != nil {
retErr = fmt.Errorf("error creating request: %w", err)
return
}
req = req.WithContext(ctx)
req.Header.Add("Metadata-Flavor", "Google")
q := req.URL.Query()
q.Add("audience", fmt.Sprintf("%s/vault/%s", client.Address(), g.role))
q.Add("format", "full")
req.URL.RawQuery = q.Encode()
resp, err := httpClient.Do(req)
if err != nil {
retErr = fmt.Errorf("error fetching instance token: %w", err)
return
}
if resp == nil {
retErr = errors.New("empty response fetching instance toke")
return
}
defer resp.Body.Close()
jwtBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
retErr = fmt.Errorf("error reading instance token response body: %w", err)
return
}
jwt = string(jwtBytes)
}
default:
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
credentials, tokenSource, err := gcputil.FindCredentials(g.credentials, ctx, iamcredentials.CloudPlatformScope)
if err != nil {
retErr = fmt.Errorf("could not obtain credentials: %w", err)
return
}
httpClient := oauth2.NewClient(ctx, tokenSource)
var serviceAccount string
if g.serviceAccount == "" && credentials != nil {
serviceAccount = credentials.ClientEmail
} else {
serviceAccount = g.serviceAccount
}
if serviceAccount == "" {
retErr = errors.New("could not obtain service account from credentials (possibly Application Default Credentials are being used); a service account to authenticate as must be provided")
return
}
ttlMin := int64(defaultIamMaxJwtExpMinutes)
if g.jwtExp != 0 {
ttlMin = g.jwtExp
}
ttl := time.Minute * time.Duration(ttlMin)
jwtPayload := map[string]interface{}{
"aud": fmt.Sprintf("http://vault/%s", g.role),
"sub": serviceAccount,
"exp": time.Now().Add(ttl).Unix(),
}
payloadBytes, err := json.Marshal(jwtPayload)
if err != nil {
retErr = fmt.Errorf("could not convert JWT payload to JSON string: %w", err)
return
}
jwtReq := &iamcredentials.SignJwtRequest{
Payload: string(payloadBytes),
}
iamClient, err := iamcredentials.New(httpClient)
if err != nil {
retErr = fmt.Errorf("could not create IAM client: %w", err)
return
}
resourceName := fmt.Sprintf("projects/-/serviceAccounts/%s", serviceAccount)
resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do()
if err != nil {
retErr = fmt.Errorf("unable to sign JWT for %s using given Vault credentials: %w", resourceName, err)
return
}
jwt = resp.SignedJwt
}
data["role"] = g.role
data["jwt"] = jwt
return fmt.Sprintf("%s/login", g.mountPath), nil, data, nil
}
func (g *gcpMethod) NewCreds() chan struct{} {
return nil
}
func (g *gcpMethod) CredSuccess() {
}
func (g *gcpMethod) Shutdown() {
}