Quit agent endpoint with config (#14223)
* Add agent/v1/quit endpoint * Closes https://github.com/hashicorp/vault/issues/11089 * Agent quit API behind config setting * Normalise test config whitespace * Document config option Co-authored-by: Rémi Lapeyre <remi.lapeyre@lenstra.fr> Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com>
This commit is contained in:
parent
c2d7386be4
commit
3668275903
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
agent: The `agent/v1/quit` endpoint can now be used to stop the Vault Agent remotely
|
||||
```
|
|
@ -715,7 +715,10 @@ func (c *AgentCommand) Run(args []string) int {
|
|||
|
||||
// Create a muxer and add paths relevant for the lease cache layer
|
||||
mux := http.NewServeMux()
|
||||
quitEnabled := lnConfig.AgentAPI != nil && lnConfig.AgentAPI.EnableQuit
|
||||
|
||||
mux.Handle(consts.AgentPathCacheClear, leaseCache.HandleCacheClear(ctx))
|
||||
mux.Handle(consts.AgentPathQuit, c.handleQuit(quitEnabled))
|
||||
mux.Handle(consts.AgentPathMetrics, c.handleMetrics())
|
||||
mux.Handle("/", muxHandler)
|
||||
|
||||
|
@ -1047,3 +1050,22 @@ func (c *AgentCommand) handleMetrics() http.Handler {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *AgentCommand) handleQuit(enabled bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !enabled {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Debug("received quit request")
|
||||
close(c.ShutdownCh)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -510,21 +511,31 @@ cache {
|
|||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:8101"
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:8102"
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
require_request_header = false
|
||||
}
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:8103"
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
require_request_header = true
|
||||
}
|
||||
`
|
||||
config = fmt.Sprintf(config, roleIDPath, secretIDPath)
|
||||
listenAddr1 := generateListenerAddress(t)
|
||||
listenAddr2 := generateListenerAddress(t)
|
||||
listenAddr3 := generateListenerAddress(t)
|
||||
config = fmt.Sprintf(
|
||||
config,
|
||||
roleIDPath,
|
||||
secretIDPath,
|
||||
listenAddr1,
|
||||
listenAddr2,
|
||||
listenAddr3,
|
||||
)
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
|
@ -563,19 +574,19 @@ listener "tcp" {
|
|||
|
||||
// Test against a listener configuration that omits
|
||||
// 'require_request_header', with the header missing from the request.
|
||||
agentClient := newApiClient("http://127.0.0.1:8101", false)
|
||||
agentClient := newApiClient("http://"+listenAddr1, false)
|
||||
req = agentClient.NewRequest("GET", "/v1/sys/health")
|
||||
request(t, agentClient, req, 200)
|
||||
|
||||
// Test against a listener configuration that sets 'require_request_header'
|
||||
// to 'false', with the header missing from the request.
|
||||
agentClient = newApiClient("http://127.0.0.1:8102", false)
|
||||
agentClient = newApiClient("http://"+listenAddr2, false)
|
||||
req = agentClient.NewRequest("GET", "/v1/sys/health")
|
||||
request(t, agentClient, req, 200)
|
||||
|
||||
// Test against a listener configuration that sets 'require_request_header'
|
||||
// to 'true', with the header missing from the request.
|
||||
agentClient = newApiClient("http://127.0.0.1:8103", false)
|
||||
agentClient = newApiClient("http://"+listenAddr3, false)
|
||||
req = agentClient.NewRequest("GET", "/v1/sys/health")
|
||||
resp, err := agentClient.RawRequest(req)
|
||||
if err == nil {
|
||||
|
@ -587,7 +598,7 @@ listener "tcp" {
|
|||
|
||||
// Test against a listener configuration that sets 'require_request_header'
|
||||
// to 'true', with an invalid header present in the request.
|
||||
agentClient = newApiClient("http://127.0.0.1:8103", false)
|
||||
agentClient = newApiClient("http://"+listenAddr3, false)
|
||||
h := agentClient.Headers()
|
||||
h[consts.RequestHeaderName] = []string{"bogus"}
|
||||
agentClient.SetHeaders(h)
|
||||
|
@ -602,7 +613,7 @@ listener "tcp" {
|
|||
|
||||
// Test against a listener configuration that sets 'require_request_header'
|
||||
// to 'true', with the proper header present in the request.
|
||||
agentClient = newApiClient("http://127.0.0.1:8103", true)
|
||||
agentClient = newApiClient("http://"+listenAddr3, true)
|
||||
req = agentClient.NewRequest("GET", "/v1/sys/health")
|
||||
request(t, agentClient, req, 200)
|
||||
}
|
||||
|
@ -613,16 +624,17 @@ listener "tcp" {
|
|||
func TestAgent_RequireAutoAuthWithForce(t *testing.T) {
|
||||
logger := logging.NewVaultLogger(hclog.Trace)
|
||||
// Create a config file
|
||||
config := `
|
||||
config := fmt.Sprintf(`
|
||||
cache {
|
||||
use_auto_auth_token = "force"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:8101"
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
`
|
||||
`, generateListenerAddress(t))
|
||||
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
|
@ -1623,7 +1635,7 @@ func TestAgent_Cache_Retry(t *testing.T) {
|
|||
cache {
|
||||
}
|
||||
`
|
||||
listenAddr := "127.0.0.1:18123"
|
||||
listenAddr := generateListenerAddress(t)
|
||||
listenConfig := fmt.Sprintf(`
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
|
@ -1861,7 +1873,7 @@ func TestAgent_TemplateConfig_ExitOnRetryFailure(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listenAddr := "127.0.0.1:18123"
|
||||
listenAddr := generateListenerAddress(t)
|
||||
listenConfig := fmt.Sprintf(`
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
|
@ -2021,14 +2033,15 @@ func TestAgent_Metrics(t *testing.T) {
|
|||
serverClient := cluster.Cores[0].Client
|
||||
|
||||
// Create a config file
|
||||
config := `
|
||||
listenAddr := generateListenerAddress(t)
|
||||
config := fmt.Sprintf(`
|
||||
cache {}
|
||||
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:8101"
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
`
|
||||
`, listenAddr)
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
|
@ -2062,7 +2075,7 @@ listener "tcp" {
|
|||
}()
|
||||
|
||||
conf := api.DefaultConfig()
|
||||
conf.Address = "http://127.0.0.1:8101"
|
||||
conf.Address = "http://" + listenAddr
|
||||
agentClient, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
@ -2082,3 +2095,133 @@ listener "tcp" {
|
|||
"Points",
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgent_Quit(t *testing.T) {
|
||||
//----------------------------------------------------
|
||||
// Start the server and agent
|
||||
//----------------------------------------------------
|
||||
logger := logging.NewVaultLogger(hclog.Error)
|
||||
cluster := vault.NewTestCluster(t,
|
||||
&vault.CoreConfig{
|
||||
Logger: logger,
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"approle": credAppRole.Factory,
|
||||
},
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"kv": logicalKv.Factory,
|
||||
},
|
||||
},
|
||||
&vault.TestClusterOptions{
|
||||
NumCores: 1,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||
serverClient := cluster.Cores[0].Client
|
||||
|
||||
// Unset the environment variable so that agent picks up the right test
|
||||
// cluster address
|
||||
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
|
||||
err := os.Unsetenv(api.EnvVaultAddress)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
listenAddr := generateListenerAddress(t)
|
||||
listenAddr2 := generateListenerAddress(t)
|
||||
config := fmt.Sprintf(`
|
||||
vault {
|
||||
address = "%s"
|
||||
tls_skip_verify = true
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "%s"
|
||||
tls_disable = true
|
||||
agent_api {
|
||||
enable_quit = true
|
||||
}
|
||||
}
|
||||
|
||||
cache {}
|
||||
`, serverClient.Address(), listenAddr, listenAddr2)
|
||||
|
||||
configPath := makeTempFile(t, "config.hcl", config)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
// Start the agent
|
||||
_, cmd := testAgentCommand(t, logger)
|
||||
cmd.startedCh = make(chan struct{})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
cmd.Run([]string{"-config", configPath})
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-cmd.startedCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("timeout")
|
||||
}
|
||||
client, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client.SetToken(serverClient.Token())
|
||||
client.SetMaxRetries(0)
|
||||
err = client.SetAddress("http://" + listenAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First try on listener 1 where the API should be disabled.
|
||||
resp, err := client.RawRequest(client.NewRequest(http.MethodPost, "/agent/v1/quit"))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if resp != nil && resp.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("expected %d but got: %d", http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Now try on listener 2 where the quit API should be enabled.
|
||||
err = client.SetAddress("http://" + listenAddr2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.RawRequest(client.NewRequest(http.MethodPost, "/agent/v1/quit"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-cmd.ShutdownCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("timeout")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Get a randomly assigned port and then free it again before returning it.
|
||||
// There is still a race when trying to use it, but should work better
|
||||
// than a static port.
|
||||
func generateListenerAddress(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
ln1, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listenAddr := ln1.Addr().String()
|
||||
ln1.Close()
|
||||
return listenAddr
|
||||
}
|
||||
|
|
|
@ -780,22 +780,25 @@ func testConfig_Sanitized(t *testing.T) {
|
|||
func testParseListeners(t *testing.T) {
|
||||
obj, _ := hcl.Parse(strings.TrimSpace(`
|
||||
listener "tcp" {
|
||||
address = "127.0.0.1:443"
|
||||
cluster_address = "127.0.0.1:8201"
|
||||
tls_disable = false
|
||||
tls_cert_file = "./certs/server.crt"
|
||||
tls_key_file = "./certs/server.key"
|
||||
tls_client_ca_file = "./certs/rootca.crt"
|
||||
tls_min_version = "tls12"
|
||||
tls_max_version = "tls13"
|
||||
tls_require_and_verify_client_cert = true
|
||||
tls_disable_client_certs = true
|
||||
telemetry {
|
||||
unauthenticated_metrics_access = true
|
||||
}
|
||||
profiling {
|
||||
unauthenticated_pprof_access = true
|
||||
}
|
||||
address = "127.0.0.1:443"
|
||||
cluster_address = "127.0.0.1:8201"
|
||||
tls_disable = false
|
||||
tls_cert_file = "./certs/server.crt"
|
||||
tls_key_file = "./certs/server.key"
|
||||
tls_client_ca_file = "./certs/rootca.crt"
|
||||
tls_min_version = "tls12"
|
||||
tls_max_version = "tls13"
|
||||
tls_require_and_verify_client_cert = true
|
||||
tls_disable_client_certs = true
|
||||
telemetry {
|
||||
unauthenticated_metrics_access = true
|
||||
}
|
||||
profiling {
|
||||
unauthenticated_pprof_access = true
|
||||
}
|
||||
agent_api {
|
||||
enable_quit = true
|
||||
}
|
||||
}`))
|
||||
|
||||
config := Config{
|
||||
|
@ -833,6 +836,9 @@ listener "tcp" {
|
|||
Profiling: configutil.ListenerProfiling{
|
||||
UnauthenticatedPProfAccess: true,
|
||||
},
|
||||
AgentAPI: &configutil.AgentAPI{
|
||||
EnableQuit: true,
|
||||
},
|
||||
CustomResponseHeaders: DefaultCustomHeaders,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -93,6 +93,8 @@ type Listener struct {
|
|||
SocketUser string `hcl:"socket_user"`
|
||||
SocketGroup string `hcl:"socket_group"`
|
||||
|
||||
AgentAPI *AgentAPI `hcl:"agent_api"`
|
||||
|
||||
Telemetry ListenerTelemetry `hcl:"telemetry"`
|
||||
Profiling ListenerProfiling `hcl:"profiling"`
|
||||
InFlightRequestLogging ListenerInFlightRequestLogging `hcl:"inflight_requests_logging"`
|
||||
|
@ -111,6 +113,11 @@ type Listener struct {
|
|||
CustomResponseHeadersRaw interface{} `hcl:"custom_response_headers"`
|
||||
}
|
||||
|
||||
// AgentAPI allows users to select which parts of the Agent API they want enabled.
|
||||
type AgentAPI struct {
|
||||
EnableQuit bool `hcl:"enable_quit"`
|
||||
}
|
||||
|
||||
func (l *Listener) GoString() string {
|
||||
return fmt.Sprintf("*%#v", *l)
|
||||
}
|
||||
|
|
|
@ -7,3 +7,6 @@ const AgentPathCacheClear = "/agent/v1/cache-clear"
|
|||
// AgentPathMetrics is the path the the agent will use to expose its internal
|
||||
// metrics.
|
||||
const AgentPathMetrics = "/agent/v1/metrics"
|
||||
|
||||
// AgentPathQuit is the path that the agent will use to trigger stopping it.
|
||||
const AgentPathQuit = "/agent/v1/quit"
|
||||
|
|
|
@ -109,6 +109,22 @@ Vault Agent allows client-side caching of responses containing newly created tok
|
|||
and responses containing leased secrets generated off of these newly created tokens.
|
||||
Please see the [Caching docs][caching] for information.
|
||||
|
||||
## API
|
||||
|
||||
### Quit
|
||||
|
||||
This endpoints triggers shutdown of the agent. By default, it is disabled, and can
|
||||
be enabled per listener using the [`agent_api`][agent-api] stanza. It is recommended
|
||||
to only enable this on trusted interfaces, as it does not require any authorization to use.
|
||||
|
||||
| Method | Path |
|
||||
| :----- | :--------------- |
|
||||
| `POST` | `/agent/v1/quit` |
|
||||
|
||||
### Cache
|
||||
|
||||
See the [caching](/docs/agent/caching#api) page for details on the cache API.
|
||||
|
||||
## Configuration
|
||||
|
||||
These are the currently-available general configuration option:
|
||||
|
@ -214,7 +230,7 @@ to address in the future.
|
|||
|
||||
Agent supports one or more [listener][listener_main] stanzas. In addition to
|
||||
the standard listener configuration, an Agent's listener configuration also
|
||||
supports an additional optional entry:
|
||||
supports the following:
|
||||
|
||||
- `require_request_header` `(bool: false)` - Require that all incoming HTTP
|
||||
requests on this listener must have an `X-Vault-Request: true` header entry.
|
||||
|
@ -222,6 +238,12 @@ supports an additional optional entry:
|
|||
Request Forgery attacks. Requests on the listener that do not have the proper
|
||||
`X-Vault-Request` header will fail, with a HTTP response status code of `412: Precondition Failed`.
|
||||
|
||||
- `agent_api` <code>([agent_api][agent-api]: <optional\>)</code> - Manages optional Agent API endpoints.
|
||||
|
||||
#### agent_api Stanza
|
||||
|
||||
- `enable_quit` `(bool: false)` - If set to `true`, the agent will enable the [quit](/docs/agent#quit) API.
|
||||
|
||||
### telemetry Stanza
|
||||
|
||||
Vault Agent supports the [telemetry][telemetry] stanza and collects various
|
||||
|
@ -334,6 +356,7 @@ template {
|
|||
[persistent-cache]: /docs/agent/caching/persistent-caches
|
||||
[template]: /docs/agent/template
|
||||
[template-config]: /docs/agent/template-config
|
||||
[agent-api]: /docs/agent/#agent_api-stanza
|
||||
[listener]: /docs/agent#listener-stanza
|
||||
[listener_main]: /docs/configuration/listener/tcp
|
||||
[winsvc]: /docs/agent/winsvc
|
||||
|
|
Loading…
Reference in New Issue