1package gcp 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "time" 11 12 "github.com/hashicorp/errwrap" 13 cleanhttp "github.com/hashicorp/go-cleanhttp" 14 "github.com/hashicorp/go-gcp-common/gcputil" 15 hclog "github.com/hashicorp/go-hclog" 16 "github.com/hashicorp/vault/api" 17 "github.com/hashicorp/vault/command/agent/auth" 18 "github.com/hashicorp/vault/sdk/helper/parseutil" 19 "golang.org/x/oauth2" 20 iam "google.golang.org/api/iam/v1" 21) 22 23const ( 24 typeGCE = "gce" 25 typeIAM = "iam" 26 identityEndpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/%s/identity" 27 defaultIamMaxJwtExpMinutes = 15 28) 29 30type gcpMethod struct { 31 logger hclog.Logger 32 authType string 33 mountPath string 34 role string 35 credentials string 36 serviceAccount string 37 project string 38 jwtExp int64 39} 40 41func NewGCPAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) { 42 if conf == nil { 43 return nil, errors.New("empty config") 44 } 45 if conf.Config == nil { 46 return nil, errors.New("empty config data") 47 } 48 49 var err error 50 51 g := &gcpMethod{ 52 logger: conf.Logger, 53 mountPath: conf.MountPath, 54 serviceAccount: "default", 55 } 56 57 typeRaw, ok := conf.Config["type"] 58 if !ok { 59 return nil, errors.New("missing 'type' value") 60 } 61 g.authType, ok = typeRaw.(string) 62 if !ok { 63 return nil, errors.New("could not convert 'type' config value to string") 64 } 65 66 roleRaw, ok := conf.Config["role"] 67 if !ok { 68 return nil, errors.New("missing 'role' value") 69 } 70 g.role, ok = roleRaw.(string) 71 if !ok { 72 return nil, errors.New("could not convert 'role' config value to string") 73 } 74 75 switch { 76 case g.role == "": 77 return nil, errors.New("'role' value is empty") 78 case g.authType == "": 79 return nil, errors.New("'type' value is empty") 80 case g.authType != typeGCE && g.authType != typeIAM: 81 return nil, errors.New("'type' value is invalid") 82 } 83 84 credentialsRaw, ok := conf.Config["credentials"] 85 if ok { 86 g.credentials, ok = credentialsRaw.(string) 87 if !ok { 88 return nil, errors.New("could not convert 'credentials' value into string") 89 } 90 } 91 92 serviceAccountRaw, ok := conf.Config["service_account"] 93 if ok { 94 g.serviceAccount, ok = serviceAccountRaw.(string) 95 if !ok { 96 return nil, errors.New("could not convert 'service_account' value into string") 97 } 98 } 99 100 projectRaw, ok := conf.Config["project"] 101 if ok { 102 g.project, ok = projectRaw.(string) 103 if !ok { 104 return nil, errors.New("could not convert 'project' value into string") 105 } 106 } 107 108 jwtExpRaw, ok := conf.Config["jwt_exp"] 109 if ok { 110 g.jwtExp, err = parseutil.ParseInt(jwtExpRaw) 111 if err != nil { 112 return nil, errwrap.Wrapf("error parsing 'jwt_raw' into integer: {{err}}", err) 113 } 114 } 115 116 return g, nil 117} 118 119func (g *gcpMethod) Authenticate(ctx context.Context, client *api.Client) (retPath string, retData map[string]interface{}, retErr error) { 120 g.logger.Trace("beginning authentication") 121 122 data := make(map[string]interface{}) 123 var jwt string 124 125 switch g.authType { 126 case typeGCE: 127 httpClient := cleanhttp.DefaultClient() 128 129 // Fetch token 130 { 131 req, err := http.NewRequest("GET", fmt.Sprintf(identityEndpoint, g.serviceAccount), nil) 132 if err != nil { 133 retErr = errwrap.Wrapf("error creating request: {{err}}", err) 134 return 135 } 136 req = req.WithContext(ctx) 137 req.Header.Add("Metadata-Flavor", "Google") 138 q := req.URL.Query() 139 q.Add("audience", fmt.Sprintf("%s/vault/%s", client.Address(), g.role)) 140 q.Add("format", "full") 141 req.URL.RawQuery = q.Encode() 142 resp, err := httpClient.Do(req) 143 if err != nil { 144 retErr = errwrap.Wrapf("error fetching instance token: {{err}}", err) 145 return 146 } 147 if resp == nil { 148 retErr = errors.New("empty response fetching instance toke") 149 return 150 } 151 defer resp.Body.Close() 152 jwtBytes, err := ioutil.ReadAll(resp.Body) 153 if err != nil { 154 retErr = errwrap.Wrapf("error reading instance token response body: {{err}}", err) 155 return 156 } 157 158 jwt = string(jwtBytes) 159 } 160 161 default: 162 ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient()) 163 164 credentials, tokenSource, err := gcputil.FindCredentials(g.credentials, ctx, iam.CloudPlatformScope) 165 if err != nil { 166 retErr = errwrap.Wrapf("could not obtain credentials: {{err}}", err) 167 return 168 } 169 170 httpClient := oauth2.NewClient(ctx, tokenSource) 171 172 var serviceAccount string 173 if g.serviceAccount == "" && credentials != nil { 174 serviceAccount = credentials.ClientEmail 175 } else { 176 serviceAccount = g.serviceAccount 177 } 178 if serviceAccount == "" { 179 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") 180 return 181 } 182 183 project := "-" 184 if g.project != "" { 185 project = g.project 186 } else if credentials != nil { 187 project = credentials.ProjectId 188 } 189 190 ttlMin := int64(defaultIamMaxJwtExpMinutes) 191 if g.jwtExp != 0 { 192 ttlMin = g.jwtExp 193 } 194 ttl := time.Minute * time.Duration(ttlMin) 195 196 jwtPayload := map[string]interface{}{ 197 "aud": fmt.Sprintf("http://vault/%s", g.role), 198 "sub": serviceAccount, 199 "exp": time.Now().Add(ttl).Unix(), 200 } 201 payloadBytes, err := json.Marshal(jwtPayload) 202 if err != nil { 203 retErr = errwrap.Wrapf("could not convert JWT payload to JSON string: {{err}}", err) 204 return 205 } 206 207 jwtReq := &iam.SignJwtRequest{ 208 Payload: string(payloadBytes), 209 } 210 211 iamClient, err := iam.New(httpClient) 212 if err != nil { 213 retErr = errwrap.Wrapf("could not create IAM client: {{err}}", err) 214 return 215 } 216 217 resourceName := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount) 218 resp, err := iamClient.Projects.ServiceAccounts.SignJwt(resourceName, jwtReq).Do() 219 if err != nil { 220 retErr = errwrap.Wrapf(fmt.Sprintf("unable to sign JWT for %s using given Vault credentials: {{err}}", resourceName), err) 221 return 222 } 223 224 jwt = resp.SignedJwt 225 } 226 227 data["role"] = g.role 228 data["jwt"] = jwt 229 230 return fmt.Sprintf("%s/login", g.mountPath), data, nil 231} 232 233func (g *gcpMethod) NewCreds() chan struct{} { 234 return nil 235} 236 237func (g *gcpMethod) CredSuccess() { 238} 239 240func (g *gcpMethod) Shutdown() { 241} 242