diff --git a/cli/commands.go b/cli/commands.go index f79f68fa31..ae1b10dd9e 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -2,8 +2,6 @@ package cli import ( "os" - "os/signal" - "syscall" auditFile "github.com/hashicorp/vault/builtin/audit/file" auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog" @@ -79,8 +77,8 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { "mysql": mysql.Factory, "ssh": ssh.Factory, }, - ShutdownCh: makeShutdownCh(), - SighupCh: makeSighupCh(), + ShutdownCh: command.MakeShutdownCh(), + SighupCh: command.MakeSighupCh(), ReloadFuncs: map[string][]server.ReloadFunc{}, }, nil }, @@ -311,37 +309,3 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { }, } } - -// makeShutdownCh returns a channel that can be used for shutdown -// notifications for commands. This channel will send a message for every -// interrupt or SIGTERM received. -func makeShutdownCh() <-chan struct{} { - resultCh := make(chan struct{}) - - signalCh := make(chan os.Signal, 4) - signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) - go func() { - for { - <-signalCh - resultCh <- struct{}{} - } - }() - return resultCh -} - -// makeSighupCh returns a channel that can be used for SIGHUP -// reloading. This channel will send a message for every -// SIGHUP received. -func makeSighupCh() <-chan struct{} { - resultCh := make(chan struct{}) - - signalCh := make(chan os.Signal, 4) - signal.Notify(signalCh, os.Interrupt, syscall.SIGHUP) - go func() { - for { - <-signalCh - resultCh <- struct{}{} - } - }() - return resultCh -} diff --git a/command/server.go b/command/server.go index 02ba519d52..4b96f0621f 100644 --- a/command/server.go +++ b/command/server.go @@ -8,10 +8,12 @@ import ( "net/http" "net/url" "os" + "os/signal" "runtime" "sort" "strconv" "strings" + "syscall" "time" "github.com/armon/go-metrics" @@ -35,8 +37,8 @@ type ServerCommand struct { CredentialBackends map[string]logical.Factory LogicalBackends map[string]logical.Factory - ShutdownCh <-chan struct{} - SighupCh <-chan struct{} + ShutdownCh chan struct{} + SighupCh chan struct{} Meta @@ -637,3 +639,37 @@ General Options: ` return strings.TrimSpace(helpText) } + +// MakeShutdownCh returns a channel that can be used for shutdown +// notifications for commands. This channel will send a message for every +// interrupt or SIGTERM received. +func MakeShutdownCh() chan struct{} { + resultCh := make(chan struct{}) + + signalCh := make(chan os.Signal, 4) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + go func() { + for { + <-signalCh + resultCh <- struct{}{} + } + }() + return resultCh +} + +// MakeSighupCh returns a channel that can be used for SIGHUP +// reloading. This channel will send a message for every +// SIGHUP received. +func MakeSighupCh() chan struct{} { + resultCh := make(chan struct{}) + + signalCh := make(chan os.Signal, 4) + signal.Notify(signalCh, os.Interrupt, syscall.SIGHUP) + go func() { + for { + <-signalCh + resultCh <- struct{}{} + } + }() + return resultCh +} diff --git a/command/server/listener_tcp_test.go b/command/server/listener_tcp_test.go index fa3c168863..74b65ec693 100644 --- a/command/server/listener_tcp_test.go +++ b/command/server/listener_tcp_test.go @@ -41,11 +41,7 @@ func TestTCPListener_tls(t *testing.T) { defer os.RemoveAll(td) // Setup initial certs - inBytes, _ := ioutil.ReadFile(wd + "reload_foo.pem") - ioutil.WriteFile(td+"reload_curr.pem", inBytes, 0777) - inBytes, _ = ioutil.ReadFile(wd + "reload_foo.key") - ioutil.WriteFile(td+"reload_curr.key", inBytes, 0777) - inBytes, _ = ioutil.ReadFile(wd + "reload_ca.pem") + inBytes, _ := ioutil.ReadFile(wd + "reload_ca.pem") certPool := x509.NewCertPool() ok := certPool.AppendCertsFromPEM(inBytes) if !ok { @@ -54,8 +50,8 @@ func TestTCPListener_tls(t *testing.T) { ln, _, _, err := tcpListenerFactory(map[string]string{ "address": "127.0.0.1:0", - "tls_cert_file": td + "reload_curr.pem", - "tls_key_file": td + "reload_curr.key", + "tls_cert_file": wd + "reload_foo.pem", + "tls_key_file": wd + "reload_foo.key", }) if err != nil { t.Fatalf("err: %s", err) @@ -75,22 +71,4 @@ func TestTCPListener_tls(t *testing.T) { } testListenerImpl(t, ln, connFn, "foo.example.com") - /* - inBytes, _ = ioutil.ReadFile(wd + "reload_bar.pem") - ioutil.WriteFile(td+"reload_curr.pem", inBytes, 0777) - inBytes, _ = ioutil.ReadFile(wd + "reload_bar.key") - ioutil.WriteFile(td+"reload_curr.key", inBytes, 0777) - - req := logical.TestRequest(t, logical.UpdateOperation, "sys/reload") - req.ClientToken = root - resp, err := core.HandleRequest(req) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - - testListenerImpl(t, ln, connFn, "bar.example.com") - */ } diff --git a/command/server_test.go b/command/server_test.go index 69c73d343d..2e5c299490 100644 --- a/command/server_test.go +++ b/command/server_test.go @@ -3,11 +3,18 @@ package command import ( + "crypto/tls" + "crypto/x509" + "fmt" "io/ioutil" + "math/rand" "os" "strings" + "sync" "testing" + "time" + "github.com/hashicorp/vault/command/server" "github.com/mitchellh/cli" ) @@ -38,6 +45,20 @@ ha_backend "consul" { ha_backend "file" { path = "/dev/null" } +` + + reloadhcl = ` +backend "file" { + path = "/dev/null" +} + +disable_mlock = true + +listener "tcp" { + address = "127.0.0.1:8203" + tls_cert_file = "TMPDIR/reload_FILE.pem" + tls_key_file = "TMPDIR/reload_FILE.key" +} ` ) @@ -121,3 +142,111 @@ func TestServer_BadSeparateHA(t *testing.T) { t.Fatalf("bad: should have gotten an error on a bad HA config") } } + +func TestServer_ReloadListener(t *testing.T) { + wd, _ := os.Getwd() + wd += "/server/test-fixtures/reload/" + + td, err := ioutil.TempDir("", fmt.Sprintf("vault-test-%d", rand.New(rand.NewSource(time.Now().Unix())).Int63)) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(td) + + wg := &sync.WaitGroup{} + + // Setup initial certs + inBytes, _ := ioutil.ReadFile(wd + "reload_foo.pem") + ioutil.WriteFile(td+"/reload_foo.pem", inBytes, 0777) + inBytes, _ = ioutil.ReadFile(wd + "reload_foo.key") + ioutil.WriteFile(td+"/reload_foo.key", inBytes, 0777) + inBytes, _ = ioutil.ReadFile(wd + "reload_bar.pem") + ioutil.WriteFile(td+"/reload_bar.pem", inBytes, 0777) + inBytes, _ = ioutil.ReadFile(wd + "reload_bar.key") + ioutil.WriteFile(td+"/reload_bar.key", inBytes, 0777) + + relhcl := strings.Replace(strings.Replace(reloadhcl, "TMPDIR", td, -1), "FILE", "foo", -1) + ioutil.WriteFile(td+"/reload.hcl", []byte(relhcl), 0777) + + inBytes, _ = ioutil.ReadFile(wd + "reload_ca.pem") + certPool := x509.NewCertPool() + ok := certPool.AppendCertsFromPEM(inBytes) + if !ok { + t.Fatal("not ok when appending CA cert") + } + + ui := new(cli.MockUi) + c := &ServerCommand{ + Meta: Meta{ + Ui: ui, + }, + ShutdownCh: MakeShutdownCh(), + SighupCh: MakeSighupCh(), + ReloadFuncs: map[string][]server.ReloadFunc{}, + } + + finished := false + finishedMutex := sync.Mutex{} + + wg.Add(1) + args := []string{"-config", td + "/reload.hcl"} + go func() { + if code := c.Run(args); code != 0 { + t.Error("got a non-zero exit status") + } + finishedMutex.Lock() + finished = true + finishedMutex.Unlock() + wg.Done() + }() + + checkFinished := func() { + finishedMutex.Lock() + if finished { + t.Fatal(fmt.Sprintf("finished early; relhcl was\n%s\nstdout was\n%s\nstderr was\n%s\n", relhcl, ui.OutputWriter.String(), ui.ErrorWriter.String())) + } + finishedMutex.Unlock() + } + + testCertificateName := func(cn string) error { + conn, err := tls.Dial("tcp", "127.0.0.1:8203", &tls.Config{ + RootCAs: certPool, + }) + if err != nil { + return err + } + defer conn.Close() + if err = conn.Handshake(); err != nil { + return err + } + servName := conn.ConnectionState().PeerCertificates[0].Subject.CommonName + if servName != cn { + return fmt.Errorf("expected %s, got %s", cn, servName) + } + return nil + } + + checkFinished() + time.Sleep(2 * time.Second) + checkFinished() + + if err := testCertificateName("foo.example.com"); err != nil { + t.Fatalf("certificate name didn't check out: %s", err) + } + + relhcl = strings.Replace(strings.Replace(reloadhcl, "TMPDIR", td, -1), "FILE", "bar", -1) + ioutil.WriteFile(td+"/reload.hcl", []byte(relhcl), 0777) + + c.SighupCh <- struct{}{} + checkFinished() + time.Sleep(2 * time.Second) + checkFinished() + + if err := testCertificateName("bar.example.com"); err != nil { + t.Fatalf("certificate name didn't check out: %s", err) + } + + c.ShutdownCh <- struct{}{} + + wg.Wait() +} diff --git a/website/source/docs/config/index.html.md b/website/source/docs/config/index.html.md index 47c8fd4fb6..0f4bf81b9c 100644 --- a/website/source/docs/config/index.html.md +++ b/website/source/docs/config/index.html.md @@ -32,6 +32,9 @@ telemetry { After the configuration is written, use the `-config` flag with `vault server` to specify where the configuration is. +Starting with 0.5.2, limited configuration options can be changed on-the-fly by +sending a SIGHUP to the server process. These are denoted below. + ## Reference * `backend` (required) - Configures the storage backend where Vault data @@ -93,10 +96,10 @@ The supported options are: by default that TLS will be used. * `tls_cert_file` (required unless disabled) - The path to the certificate - for TLS. + for TLS. This is reloaded via SIGHUP. * `tls_key_file` (required unless disabled) - The path to the private key - for the certificate. + for the certificate. This is reloaded via SIGHUP. * `tls_min_version` (optional) - **(Vault > 0.2)** If provided, specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11"