0af0543bbe
Vault agent redacts the token and accessor for `/auth/token/lookup-self` (and `lookup`) if the token is the auto auth token to prevent it from leaking. Similarly, we need to redact the token and accessor from `renew-self` and `renew`, which also leak the token and accessor. I tested this locally by starting up a Vault agent and querying the agent endpoints, and ensuring that the accessor and token were set to the empty string in the response.
230 lines
6.2 KiB
Go
230 lines
6.2 KiB
Go
package cache
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/command/agent/sink"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func Handler(ctx context.Context, logger hclog.Logger, proxier Proxier, inmemSink sink.Sink, proxyVaultToken bool) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger.Info("received request", "method", r.Method, "path", r.URL.Path)
|
|
|
|
if !proxyVaultToken {
|
|
r.Header.Del(consts.AuthHeaderName)
|
|
}
|
|
|
|
token := r.Header.Get(consts.AuthHeaderName)
|
|
|
|
if token == "" && inmemSink != nil {
|
|
logger.Debug("using auto auth token", "method", r.Method, "path", r.URL.Path)
|
|
token = inmemSink.(sink.SinkReader).Token()
|
|
}
|
|
|
|
// Parse and reset body.
|
|
reqBody, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
logger.Error("failed to read request body")
|
|
logical.RespondError(w, http.StatusInternalServerError, errors.New("failed to read request body"))
|
|
return
|
|
}
|
|
if r.Body != nil {
|
|
r.Body.Close()
|
|
}
|
|
r.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
|
req := &SendRequest{
|
|
Token: token,
|
|
Request: r,
|
|
RequestBody: reqBody,
|
|
}
|
|
|
|
resp, err := proxier.Send(ctx, req)
|
|
if err != nil {
|
|
// If this is a api.Response error, don't wrap the response.
|
|
if resp != nil && resp.Response.Error() != nil {
|
|
copyHeader(w.Header(), resp.Response.Header)
|
|
w.WriteHeader(resp.Response.StatusCode)
|
|
io.Copy(w, resp.Response.Body)
|
|
metrics.IncrCounter([]string{"agent", "proxy", "client_error"}, 1)
|
|
} else {
|
|
metrics.IncrCounter([]string{"agent", "proxy", "error"}, 1)
|
|
logical.RespondError(w, http.StatusInternalServerError, fmt.Errorf("failed to get the response: %w", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
err = sanitizeAutoAuthTokenResponse(ctx, logger, inmemSink, req, resp)
|
|
if err != nil {
|
|
logical.RespondError(w, http.StatusInternalServerError, fmt.Errorf("failed to process token lookup response: %w", err))
|
|
return
|
|
}
|
|
|
|
defer resp.Response.Body.Close()
|
|
|
|
metrics.IncrCounter([]string{"agent", "proxy", "success"}, 1)
|
|
if resp.CacheMeta != nil {
|
|
if resp.CacheMeta.Hit {
|
|
metrics.IncrCounter([]string{"agent", "cache", "hit"}, 1)
|
|
} else {
|
|
metrics.IncrCounter([]string{"agent", "cache", "miss"}, 1)
|
|
}
|
|
}
|
|
|
|
// Set headers
|
|
setHeaders(w, resp)
|
|
|
|
// Set response body
|
|
io.Copy(w, resp.Response.Body)
|
|
return
|
|
})
|
|
}
|
|
|
|
// setHeaders is a helper that sets the header values based on SendResponse. It
|
|
// copies over the headers from the original response and also includes any
|
|
// cache-related headers.
|
|
func setHeaders(w http.ResponseWriter, resp *SendResponse) {
|
|
// Set header values
|
|
copyHeader(w.Header(), resp.Response.Header)
|
|
if resp.CacheMeta != nil {
|
|
xCacheVal := "MISS"
|
|
|
|
if resp.CacheMeta.Hit {
|
|
xCacheVal = "HIT"
|
|
|
|
// If this is a cache hit, we also set the Age header
|
|
age := fmt.Sprintf("%.0f", resp.CacheMeta.Age.Seconds())
|
|
w.Header().Set("Age", age)
|
|
|
|
// Update the date value
|
|
w.Header().Set("Date", time.Now().Format(http.TimeFormat))
|
|
}
|
|
|
|
w.Header().Set("X-Cache", xCacheVal)
|
|
}
|
|
|
|
// Set status code
|
|
w.WriteHeader(resp.Response.StatusCode)
|
|
}
|
|
|
|
// sanitizeAutoAuthTokenResponse checks if the request was a lookup or renew
|
|
// and if the auto-auth token was used to perform lookup-self, the identifier
|
|
// of the token and its accessor same will be stripped off of the response.
|
|
func sanitizeAutoAuthTokenResponse(ctx context.Context, logger hclog.Logger, inmemSink sink.Sink, req *SendRequest, resp *SendResponse) error {
|
|
// If auto-auth token is not being used, there is nothing to do.
|
|
if inmemSink == nil {
|
|
return nil
|
|
}
|
|
autoAuthToken := inmemSink.(sink.SinkReader).Token()
|
|
|
|
// If lookup responded with non 200 status, there is nothing to do.
|
|
if resp.Response.StatusCode != http.StatusOK {
|
|
return nil
|
|
}
|
|
|
|
_, path := deriveNamespaceAndRevocationPath(req)
|
|
switch path {
|
|
case vaultPathTokenLookupSelf, vaultPathTokenRenewSelf:
|
|
if req.Token != autoAuthToken {
|
|
return nil
|
|
}
|
|
case vaultPathTokenLookup, vaultPathTokenRenew:
|
|
jsonBody := map[string]interface{}{}
|
|
if err := json.Unmarshal(req.RequestBody, &jsonBody); err != nil {
|
|
return err
|
|
}
|
|
tokenRaw, ok := jsonBody["token"]
|
|
if !ok {
|
|
// Input error will be caught by the API
|
|
return nil
|
|
}
|
|
token, ok := tokenRaw.(string)
|
|
if !ok {
|
|
// Input error will be caught by the API
|
|
return nil
|
|
}
|
|
if token != "" && token != autoAuthToken {
|
|
// Lookup is performed on the non-auto-auth token
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
logger.Info("stripping auto-auth token from the response", "method", req.Request.Method, "path", req.Request.URL.Path)
|
|
secret, err := api.ParseSecret(bytes.NewReader(resp.ResponseBody))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse token lookup response: %v", err)
|
|
}
|
|
if secret == nil {
|
|
return nil
|
|
} else if secret.Data != nil {
|
|
// lookup endpoints
|
|
if secret.Data["id"] == nil && secret.Data["accessor"] == nil {
|
|
return nil
|
|
}
|
|
delete(secret.Data, "id")
|
|
delete(secret.Data, "accessor")
|
|
} else if secret.Auth != nil {
|
|
// renew endpoints
|
|
if secret.Auth.Accessor == "" && secret.Auth.ClientToken == "" {
|
|
return nil
|
|
}
|
|
secret.Auth.Accessor = ""
|
|
secret.Auth.ClientToken = ""
|
|
} else {
|
|
// nothing to redact
|
|
return nil
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Response.Body != nil {
|
|
resp.Response.Body.Close()
|
|
}
|
|
resp.Response.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
|
resp.Response.ContentLength = int64(len(bodyBytes))
|
|
|
|
// Serialize and re-read the response
|
|
var respBytes bytes.Buffer
|
|
err = resp.Response.Write(&respBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize the updated response: %v", err)
|
|
}
|
|
|
|
updatedResponse, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(respBytes.Bytes())), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deserialize the updated response: %v", err)
|
|
}
|
|
|
|
resp.Response = &api.Response{
|
|
Response: updatedResponse,
|
|
}
|
|
resp.ResponseBody = bodyBytes
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyHeader(dst, src http.Header) {
|
|
for k, vv := range src {
|
|
for _, v := range vv {
|
|
dst.Add(k, v)
|
|
}
|
|
}
|
|
}
|