Skip to content

Commit

Permalink
Vault Agent Cache (#6220)
Browse files Browse the repository at this point in the history
* vault-agent-cache: squashed 250+ commits

* Add proper token revocation validations to the tests

* Add more test cases

* Avoid leaking by not closing request/response bodies; add comments

* Fix revoke orphan use case; update tests

* Add CLI test for making request over unix socket

* agent/cache: remove namespace-related tests

* Strip-off the auto-auth token from the lookup response

* Output listener details along with configuration

* Add scheme to API address output

* leasecache: use IndexNameLease for prefix lease revocations

* Make CLI accept the fully qualified unix address

* export VAULT_AGENT_ADDR=unix://path/to/socket

* unix:/ to unix://
  • Loading branch information
vishalnayak authored Feb 15, 2019
1 parent 5dd50ef commit e39a5f2
Show file tree
Hide file tree
Showing 26 changed files with 4,283 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ Vagrantfile
# Configs
*.hcl
!command/agent/config/test-fixtures/config.hcl
!command/agent/config/test-fixtures/config-cache.hcl
!command/agent/config/test-fixtures/config-embedded-type.hcl
!command/agent/config/test-fixtures/config-cache-embedded-type.hcl

.DS_Store
.idea
Expand Down
22 changes: 21 additions & 1 deletion api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"golang.org/x/time/rate"
)

const EnvVaultAgentAddress = "VAULT_AGENT_ADDR"
const EnvVaultAddress = "VAULT_ADDR"
const EnvVaultCACert = "VAULT_CACERT"
const EnvVaultCAPath = "VAULT_CAPATH"
Expand Down Expand Up @@ -237,6 +238,10 @@ func (c *Config) ReadEnvironment() error {
if v := os.Getenv(EnvVaultAddress); v != "" {
envAddress = v
}
// Agent's address will take precedence over Vault's address
if v := os.Getenv(EnvVaultAgentAddress); v != "" {
envAddress = v
}
if v := os.Getenv(EnvVaultMaxRetries); v != "" {
maxRetries, err := strconv.ParseUint(v, 10, 32)
if err != nil {
Expand Down Expand Up @@ -366,6 +371,21 @@ func NewClient(c *Config) (*Client, error) {
c.modifyLock.Lock()
defer c.modifyLock.Unlock()

// If address begins with a `unix://`, treat it as a socket file path and set
// the HttpClient's transport to the corresponding socket dialer.
if strings.HasPrefix(c.Address, "unix://") {
socketFilePath := strings.TrimPrefix(c.Address, "unix://")
c.HttpClient = &http.Client{
Transport: &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) {
return net.Dial("unix", socketFilePath)
},
},
}
// Set the unix address for URL parsing below
c.Address = "http://unix"
}

u, err := url.Parse(c.Address)
if err != nil {
return nil, err
Expand Down Expand Up @@ -707,7 +727,7 @@ func (c *Client) RawRequestWithContext(ctx context.Context, r *Request) (*Respon

redirectCount := 0
START:
req, err := r.toRetryableHTTP()
req, err := r.ToRetryableHTTP()
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (r *Request) ResetJSONBody() error {
// DEPRECATED: ToHTTP turns this request into a valid *http.Request for use
// with the net/http package.
func (r *Request) ToHTTP() (*http.Request, error) {
req, err := r.toRetryableHTTP()
req, err := r.ToRetryableHTTP()
if err != nil {
return nil, err
}
Expand All @@ -85,7 +85,7 @@ func (r *Request) ToHTTP() (*http.Request, error) {
return req.Request, nil
}

func (r *Request) toRetryableHTTP() (*retryablehttp.Request, error) {
func (r *Request) ToRetryableHTTP() (*retryablehttp.Request, error) {
// Encode the query parameters
r.URL.RawQuery = r.Params.Encode()

Expand Down
1 change: 1 addition & 0 deletions api/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ type SecretAuth struct {
TokenPolicies []string `json:"token_policies"`
IdentityPolicies []string `json:"identity_policies"`
Metadata map[string]string `json:"metadata"`
Orphan bool `json:"orphan"`

LeaseDuration int `json:"lease_duration"`
Renewable bool `json:"renewable"`
Expand Down
102 changes: 88 additions & 14 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"time"

"os"
"sort"
"strings"
Expand All @@ -23,6 +27,7 @@ import (
"github.com/hashicorp/vault/command/agent/auth/gcp"
"github.com/hashicorp/vault/command/agent/auth/jwt"
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
"github.com/hashicorp/vault/command/agent/cache"
"github.com/hashicorp/vault/command/agent/config"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
Expand Down Expand Up @@ -218,19 +223,6 @@ func (c *AgentCommand) Run(args []string) int {
info["cgo"] = "enabled"
}

// Server configuration output
padding := 24
sort.Strings(infoKeys)
c.UI.Output("==> Vault agent configuration:\n")
for _, k := range infoKeys {
c.UI.Output(fmt.Sprintf(
"%s%s: %s",
strings.Repeat(" ", padding-len(k)),
strings.Title(k),
info[k]))
}
c.UI.Output("")

// Tests might not want to start a vault server and just want to verify
// the configuration.
if c.flagTestVerifyOnly {
Expand Down Expand Up @@ -332,10 +324,92 @@ func (c *AgentCommand) Run(args []string) int {
EnableReauthOnNewCredentials: config.AutoAuth.EnableReauthOnNewCredentials,
})

// Start things running
// Start auto-auth and sink servers
go ah.Run(ctx, method)
go ss.Run(ctx, ah.OutputCh, sinks)

// Parse agent listener configurations
if config.Cache != nil && len(config.Cache.Listeners) != 0 {
cacheLogger := c.logger.Named("cache")

// Create the API proxier
apiProxy := cache.NewAPIProxy(&cache.APIProxyConfig{
Logger: cacheLogger.Named("apiproxy"),
})

// Create the lease cache proxier and set its underlying proxier to
// the API proxier.
leaseCache, err := cache.NewLeaseCache(&cache.LeaseCacheConfig{
BaseContext: ctx,
Proxier: apiProxy,
Logger: cacheLogger.Named("leasecache"),
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating lease cache: %v", err))
return 1
}

// Create a muxer and add paths relevant for the lease cache layer
mux := http.NewServeMux()
mux.Handle("/v1/agent/cache-clear", leaseCache.HandleCacheClear(ctx))

mux.Handle("/", cache.Handler(ctx, cacheLogger, leaseCache, config.Cache.UseAutoAuthToken, c.client))

var listeners []net.Listener
for i, lnConfig := range config.Cache.Listeners {
listener, props, _, err := cache.ServerListener(lnConfig, c.logWriter, c.UI)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing listener configuration: %v", err))
return 1
}

listeners = append(listeners, listener)

scheme := "https://"
if props["tls"] == "disabled" {
scheme = "http://"
}
if lnConfig.Type == "unix" {
scheme = "unix://"
}

infoKey := fmt.Sprintf("api address %d", i+1)
info[infoKey] = scheme + listener.Addr().String()
infoKeys = append(infoKeys, infoKey)

cacheLogger.Info("starting listener", "addr", listener.Addr().String())
server := &http.Server{
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 5 * time.Minute,
ErrorLog: cacheLogger.StandardLogger(nil),
}
go server.Serve(listener)
}

// Ensure that listeners are closed at all the exits
listenerCloseFunc := func() {
for _, ln := range listeners {
ln.Close()
}
}
defer c.cleanupGuard.Do(listenerCloseFunc)
}

// Server configuration output
padding := 24
sort.Strings(infoKeys)
c.UI.Output("==> Vault agent configuration:\n")
for _, k := range infoKeys {
c.UI.Output(fmt.Sprintf(
"%s%s: %s",
strings.Repeat(" ", padding-len(k)),
strings.Title(k),
info[k]))
}
c.UI.Output("")

// Release the log gate.
c.logGate.Flush()

Expand Down
61 changes: 61 additions & 0 deletions command/agent/cache/api_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cache

import (
"bytes"
"context"
"io/ioutil"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
)

// APIProxy is an implementation of the proxier interface that is used to
// forward the request to Vault and get the response.
type APIProxy struct {
logger hclog.Logger
}

type APIProxyConfig struct {
Logger hclog.Logger
}

func NewAPIProxy(config *APIProxyConfig) Proxier {
return &APIProxy{
logger: config.Logger,
}
}

func (ap *APIProxy) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) {
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
return nil, err
}
client.SetToken(req.Token)
client.SetHeaders(req.Request.Header)

fwReq := client.NewRequest(req.Request.Method, req.Request.URL.Path)
fwReq.BodyBytes = req.RequestBody

// Make the request to Vault and get the response
ap.logger.Info("forwarding request", "path", req.Request.URL.Path, "method", req.Request.Method)
resp, err := client.RawRequestWithContext(ctx, fwReq)
if err != nil {
return nil, err
}

// Parse and reset response body
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
ap.logger.Error("failed to read request body", "error", err)
return nil, err
}
if resp.Body != nil {
resp.Body.Close()
}
resp.Body = ioutil.NopCloser(bytes.NewBuffer(respBody))

return &SendResponse{
Response: resp,
ResponseBody: respBody,
}, nil
}
43 changes: 43 additions & 0 deletions command/agent/cache/api_proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cache

import (
"testing"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/logging"
"github.com/hashicorp/vault/helper/namespace"
)

func TestCache_APIProxy(t *testing.T) {
cleanup, client, _, _ := setupClusterAndAgent(namespace.RootContext(nil), t, nil)
defer cleanup()

proxier := NewAPIProxy(&APIProxyConfig{
Logger: logging.NewVaultLogger(hclog.Trace),
})

r := client.NewRequest("GET", "/v1/sys/health")
req, err := r.ToRetryableHTTP()
if err != nil {
t.Fatal(err)
}

resp, err := proxier.Send(namespace.RootContext(nil), &SendRequest{
Request: req.Request,
})
if err != nil {
t.Fatal(err)
}

var result api.HealthResponse
err = jsonutil.DecodeJSONFromReader(resp.Response.Body, &result)
if err != nil {
t.Fatal(err)
}

if !result.Initialized || result.Sealed || result.Standby {
t.Fatalf("bad sys/health response")
}
}
Loading

0 comments on commit e39a5f2

Please sign in to comment.