9c5f018938
Remove template_retry config section. Add new vault.retry section which only has num_retries field; if num_retries is 0 or absent, default it to 12 for backwards compat with pre-1.7 template retrying. Setting num_retries=-1 disables retries. Configured retries are used for both templating and api proxy, though if template requests go through proxy (currently requires persistence enabled) we'll only configure retries for the latter to avoid duplicate retrying. Though there is some duplicate retrying already because whenever the template server does a retry when not going through the proxy, the Vault client it uses allows for 2 behind-the-scenes retries for some 400/500 http error codes.
583 lines
14 KiB
Go
583 lines
14 KiB
Go
package template
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
ctconfig "github.com/hashicorp/consul-template/config"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vault/command/agent/config"
|
|
"github.com/hashicorp/vault/internalshared/configutil"
|
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
|
"github.com/hashicorp/vault/sdk/helper/pointerutil"
|
|
)
|
|
|
|
// TestNewServer is a simple test to make sure NewServer returns a Server and
|
|
// channel
|
|
func TestNewServer(t *testing.T) {
|
|
server := NewServer(&ServerConfig{})
|
|
if server == nil {
|
|
t.Fatal("nil server returned")
|
|
}
|
|
}
|
|
|
|
func newAgentConfig(listeners []*configutil.Listener, enableCache, enablePersisentCache bool) *config.Config {
|
|
agentConfig := &config.Config{
|
|
SharedConfig: &configutil.SharedConfig{
|
|
PidFile: "./pidfile",
|
|
Listeners: listeners,
|
|
},
|
|
AutoAuth: &config.AutoAuth{
|
|
Method: &config.Method{
|
|
Type: "aws",
|
|
MountPath: "auth/aws",
|
|
Config: map[string]interface{}{
|
|
"role": "foobar",
|
|
},
|
|
},
|
|
Sinks: []*config.Sink{
|
|
{
|
|
Type: "file",
|
|
DHType: "curve25519",
|
|
DHPath: "/tmp/file-foo-dhpath",
|
|
AAD: "foobar",
|
|
Config: map[string]interface{}{
|
|
"path": "/tmp/file-foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Vault: &config.Vault{
|
|
Address: "http://127.0.0.1:1111",
|
|
CACert: "config_ca_cert",
|
|
CAPath: "config_ca_path",
|
|
TLSSkipVerifyRaw: interface{}("true"),
|
|
TLSSkipVerify: true,
|
|
ClientCert: "config_client_cert",
|
|
ClientKey: "config_client_key",
|
|
},
|
|
}
|
|
if enableCache {
|
|
agentConfig.Cache = &config.Cache{
|
|
UseAutoAuthToken: true,
|
|
}
|
|
}
|
|
|
|
if enablePersisentCache {
|
|
agentConfig.Cache.Persist = &config.Persist{Type: "kubernetes"}
|
|
}
|
|
|
|
return agentConfig
|
|
}
|
|
|
|
func TestCacheConfigUnix(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSDisable: true,
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, true)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "unix://foobar"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigHTTP(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSDisable: true,
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, true)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "http://127.0.0.1:8300"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigHTTPS(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSDisable: true,
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, true)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "https://127.0.0.1:8300"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
|
|
if *ctConfig.Vault.SSL.Verify {
|
|
t.Fatalf("expected %t, got %t", true, *ctConfig.Vault.SSL.Verify)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigNoCache(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSDisable: true,
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, false, false)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "http://127.0.0.1:1111"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigNoPersistentCache(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSDisable: true,
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, false)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "http://127.0.0.1:1111"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigNoListener(t *testing.T) {
|
|
listeners := []*configutil.Listener{}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, true)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
expected := "http://127.0.0.1:1111"
|
|
if *ctConfig.Vault.Address != expected {
|
|
t.Fatalf("expected %s, got %s", expected, *ctConfig.Vault.Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheConfigRejectMTLS(t *testing.T) {
|
|
listeners := []*configutil.Listener{
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8300",
|
|
TLSKeyFile: "/path/to/cakey.pem",
|
|
TLSCertFile: "/path/to/cacert.pem",
|
|
TLSRequireAndVerifyClientCert: true,
|
|
},
|
|
{
|
|
Type: "unix",
|
|
Address: "foobar",
|
|
TLSDisable: true,
|
|
SocketMode: "configmode",
|
|
SocketUser: "configuser",
|
|
SocketGroup: "configgroup",
|
|
},
|
|
{
|
|
Type: "tcp",
|
|
Address: "127.0.0.1:8400",
|
|
TLSDisable: true,
|
|
},
|
|
}
|
|
|
|
agentConfig := newAgentConfig(listeners, true, true)
|
|
serverConfig := ServerConfig{AgentConfig: agentConfig}
|
|
|
|
_, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{})
|
|
if err == nil {
|
|
t.Fatal("expected error, got none")
|
|
}
|
|
}
|
|
|
|
func TestServerRun(t *testing.T) {
|
|
// create http test server
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/v1/kv/myapp/config", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintln(w, jsonResponse)
|
|
})
|
|
mux.HandleFunc("/v1/kv/myapp/config-bad", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
fmt.Fprintln(w, `{"errors":[]}`)
|
|
})
|
|
mux.HandleFunc("/v1/kv/myapp/perm-denied", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(403)
|
|
fmt.Fprintln(w, `{"errors":["1 error occurred:\n\t* permission denied\n\n"]}`)
|
|
})
|
|
|
|
ts := httptest.NewServer(mux)
|
|
defer ts.Close()
|
|
tmpDir, err := ioutil.TempDir("", "agent-tests")
|
|
defer os.RemoveAll(tmpDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// secretRender is a simple struct that represents the secret we render to
|
|
// disk. It's used to unmarshal the file contents and test against
|
|
type secretRender struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
type templateTest struct {
|
|
template *ctconfig.TemplateConfig
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
templateMap map[string]*templateTest
|
|
expectError bool
|
|
}{
|
|
"simple": {
|
|
templateMap: map[string]*templateTest{
|
|
"render_01": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
"multiple": {
|
|
templateMap: map[string]*templateTest{
|
|
"render_01": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_02": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_03": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_04": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_05": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_06": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
"render_07": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContents),
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
"bad secret": {
|
|
templateMap: map[string]*templateTest{
|
|
"render_01": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContentsBad),
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
"missing key": {
|
|
templateMap: map[string]*templateTest{
|
|
"render_01": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContentsMissingKey),
|
|
ErrMissingKey: pointerutil.BoolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
"permission denied": {
|
|
templateMap: map[string]*templateTest{
|
|
"render_01": &templateTest{
|
|
template: &ctconfig.TemplateConfig{
|
|
Contents: pointerutil.StringPtr(templateContentsPermDenied),
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
templateTokenCh := make(chan string, 1)
|
|
var templatesToRender []*ctconfig.TemplateConfig
|
|
for fileName, templateTest := range tc.templateMap {
|
|
dstFile := fmt.Sprintf("%s/%s", tmpDir, fileName)
|
|
templateTest.template.Destination = pointerutil.StringPtr(dstFile)
|
|
templatesToRender = append(templatesToRender, templateTest.template)
|
|
}
|
|
|
|
ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
|
|
sc := ServerConfig{
|
|
Logger: logging.NewVaultLogger(hclog.Trace),
|
|
AgentConfig: &config.Config{
|
|
Vault: &config.Vault{
|
|
Address: ts.URL,
|
|
Retry: &config.Retry{
|
|
NumRetries: 3,
|
|
},
|
|
},
|
|
},
|
|
LogLevel: hclog.Trace,
|
|
LogWriter: hclog.DefaultOutput,
|
|
ExitAfterAuth: true,
|
|
}
|
|
|
|
var server *Server
|
|
server = NewServer(&sc)
|
|
if ts == nil {
|
|
t.Fatal("nil server returned")
|
|
}
|
|
|
|
errCh := make(chan error)
|
|
go func() {
|
|
errCh <- server.Run(ctx, templateTokenCh, templatesToRender)
|
|
}()
|
|
|
|
// send a dummy value to trigger the internal Runner to query for secret
|
|
// info
|
|
templateTokenCh <- "test"
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal("timeout reached before templates were rendered")
|
|
case err := <-errCh:
|
|
if err != nil && !tc.expectError {
|
|
t.Fatalf("did not expect error, got: %v", err)
|
|
}
|
|
if err != nil && tc.expectError {
|
|
t.Logf("received expected error: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// verify test file exists and has the content we're looking for
|
|
var fileCount int
|
|
for _, template := range templatesToRender {
|
|
if template.Destination == nil {
|
|
t.Fatal("nil template destination")
|
|
}
|
|
content, err := ioutil.ReadFile(*template.Destination)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileCount++
|
|
|
|
secret := secretRender{}
|
|
if err := json.Unmarshal(content, &secret); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if secret.Username != "appuser" || secret.Password != "password" || secret.Version != "3" {
|
|
t.Fatalf("secret didn't match: %#v", secret)
|
|
}
|
|
}
|
|
if fileCount != len(templatesToRender) {
|
|
t.Fatalf("mismatch file to template: (%d) / (%d)", fileCount, len(templatesToRender))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var jsonResponse = `
|
|
{
|
|
"request_id": "8af096e9-518c-7351-eff5-5ba20554b21f",
|
|
"lease_id": "",
|
|
"renewable": false,
|
|
"lease_duration": 0,
|
|
"data": {
|
|
"data": {
|
|
"password": "password",
|
|
"username": "appuser"
|
|
},
|
|
"metadata": {
|
|
"created_time": "2019-10-07T22:18:44.233247Z",
|
|
"deletion_time": "",
|
|
"destroyed": false,
|
|
"version": 3
|
|
}
|
|
},
|
|
"wrap_info": null,
|
|
"warnings": null,
|
|
"auth": null
|
|
}
|
|
`
|
|
|
|
var templateContents = `
|
|
{{ with secret "kv/myapp/config"}}
|
|
{
|
|
{{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }}
|
|
{{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }}
|
|
{{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }}
|
|
}
|
|
{{ end }}
|
|
`
|
|
|
|
var templateContentsMissingKey = `
|
|
{{ with secret "kv/myapp/config"}}
|
|
{
|
|
{{ if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{ end }}
|
|
}
|
|
{{ end }}
|
|
`
|
|
|
|
var templateContentsBad = `
|
|
{{ with secret "kv/myapp/config-bad"}}
|
|
{
|
|
{{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }}
|
|
{{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }}
|
|
{{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }}
|
|
}
|
|
{{ end }}
|
|
`
|
|
|
|
var templateContentsPermDenied = `
|
|
{{ with secret "kv/myapp/perm-denied"}}
|
|
{
|
|
{{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }}
|
|
{{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }}
|
|
{{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }}
|
|
}
|
|
{{ end }}
|
|
`
|