diff --git a/api/operator.go b/api/operator.go index 48d74f3ca..9df13d5d4 100644 --- a/api/operator.go +++ b/api/operator.go @@ -43,6 +43,26 @@ type RaftConfiguration struct { Index uint64 } +// KeyringOpts is used for performing Keyring operations +type KeyringOpts struct { + Key string `json:",omitempty"` +} + +// KeyringResponse is returned when listing the gossip encryption keys +type KeyringResponse struct { + // Whether this response is for a WAN ring + WAN bool + + // The datacenter name this request corresponds to + Datacenter string + + // A map of the encryption keys to the number of nodes they're installed on + Keys map[string]int + + // The total number of nodes in this ring + NumNodes int +} + // RaftGetConfiguration is used to query the current Raft peer set. func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) { r := op.c.newRequest("GET", "/v1/operator/raft/configuration") @@ -79,3 +99,61 @@ func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) err resp.Body.Close() return nil } + +// KeyringInstall is used to install a new gossip encryption key into the cluster +func (op *Operator) KeyringInstall(key string) error { + r := op.c.newRequest("PUT", "/v1/operator/keyring/install") + r.obj = KeyringOpts{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// KeyringList is used to list the gossip keys installed in the cluster +func (op *Operator) KeyringList() ([]*KeyringResponse, error) { + r := op.c.newRequest("GET", "/v1/operator/keyring/list") + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out []*KeyringResponse + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + +// KeyringRemove is used to remove a gossip encryption key from the cluster +func (op *Operator) KeyringRemove(key string) error { + r := op.c.newRequest("DELETE", "/v1/operator/keyring/remove") + r.obj = KeyringOpts{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// KeyringUse is used to change the active gossip encryption key +func (op *Operator) KeyringUse(key string) error { + r := op.c.newRequest("PUT", "/v1/operator/keyring/use") + r.obj = KeyringOpts{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} diff --git a/api/operator_test.go b/api/operator_test.go index f9d242b81..9fd1b5d5c 100644 --- a/api/operator_test.go +++ b/api/operator_test.go @@ -3,6 +3,8 @@ package api import ( "strings" "testing" + + "github.com/hashicorp/consul/testutil" ) func TestOperator_RaftGetConfiguration(t *testing.T) { @@ -36,3 +38,69 @@ func TestOperator_RaftRemovePeerByAddress(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestOperator_KeyringInstallListPutRemove(t *testing.T) { + oldKey := "d8wu8CSUrqgtjVsvcBPmhQ==" + newKey := "qxycTi/SsePj/TZzCBmNXw==" + t.Parallel() + c, s := makeClientWithConfig(t, nil, func(c *testutil.TestServerConfig) { + c.Encrypt = oldKey + }) + defer s.Stop() + + operator := c.Operator() + if err := operator.KeyringInstall(newKey); err != nil { + t.Fatalf("err: %v", err) + } + + listResponses, err := operator.KeyringList() + if err != nil { + t.Fatalf("err %v", err) + } + + // Make sure the new key is installed + if len(listResponses) != 2 { + t.Fatalf("bad: %v", len(listResponses)) + } + for _, response := range listResponses { + if len(response.Keys) != 2 { + t.Fatalf("bad: %v", len(response.Keys)) + } + if _, ok := response.Keys[oldKey]; !ok { + t.Fatalf("bad: %v", ok) + } + if _, ok := response.Keys[newKey]; !ok { + t.Fatalf("bad: %v", ok) + } + } + + // Switch the primary to the new key + if err := operator.KeyringUse(newKey); err != nil { + t.Fatalf("err: %v", err) + } + + if err := operator.KeyringRemove(oldKey); err != nil { + t.Fatalf("err: %v", err) + } + + listResponses, err = operator.KeyringList() + if err != nil { + t.Fatalf("err %v", err) + } + + // Make sure the old key is removed + if len(listResponses) != 2 { + t.Fatalf("bad: %v", len(listResponses)) + } + for _, response := range listResponses { + if len(response.Keys) != 1 { + t.Fatalf("bad: %v", len(response.Keys)) + } + if _, ok := response.Keys[oldKey]; ok { + t.Fatalf("bad: %v", ok) + } + if _, ok := response.Keys[newKey]; !ok { + t.Fatalf("bad: %v", ok) + } + } +} diff --git a/command/agent/http.go b/command/agent/http.go index b9b8902b5..6de095d5f 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -291,6 +291,10 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.handleFuncMetrics("/v1/kv/", s.wrap(s.KVSEndpoint)) s.handleFuncMetrics("/v1/operator/raft/configuration", s.wrap(s.OperatorRaftConfiguration)) s.handleFuncMetrics("/v1/operator/raft/peer", s.wrap(s.OperatorRaftPeer)) + s.handleFuncMetrics("/v1/operator/keyring/install", s.wrap(s.OperatorKeyringInstall)) + s.handleFuncMetrics("/v1/operator/keyring/list", s.wrap(s.OperatorKeyringList)) + s.handleFuncMetrics("/v1/operator/keyring/remove", s.wrap(s.OperatorKeyringRemove)) + s.handleFuncMetrics("/v1/operator/keyring/use", s.wrap(s.OperatorKeyringUse)) s.handleFuncMetrics("/v1/query", s.wrap(s.PreparedQueryGeneral)) s.handleFuncMetrics("/v1/query/", s.wrap(s.PreparedQuerySpecific)) s.handleFuncMetrics("/v1/session/create", s.wrap(s.SessionCreate)) diff --git a/command/agent/operator_endpoint.go b/command/agent/operator_endpoint.go index cdab48c38..93d1d1972 100644 --- a/command/agent/operator_endpoint.go +++ b/command/agent/operator_endpoint.go @@ -1,6 +1,7 @@ package agent import ( + "fmt" "net/http" "github.com/hashicorp/consul/consul/structs" @@ -55,3 +56,110 @@ func (s *HTTPServer) OperatorRaftPeer(resp http.ResponseWriter, req *http.Reques } return nil, nil } + +// OperatorKeyringInstall is used to install a new gossip encryption key into the cluster +func (s *HTTPServer) OperatorKeyringInstall(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + resp.WriteHeader(http.StatusMethodNotAllowed) + return nil, nil + } + + var args structs.KeyringRequest + if err := decodeBody(req, &args, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + s.parseToken(req, &args.Token) + + responses, err := s.agent.InstallKey(args.Key, args.Token) + if err != nil { + return nil, err + } + for _, response := range responses.Responses { + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + } + + return nil, nil +} + +// OperatorKeyringList is used to list the keys installed in the cluster +func (s *HTTPServer) OperatorKeyringList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + resp.WriteHeader(http.StatusMethodNotAllowed) + return nil, nil + } + + var token string + s.parseToken(req, &token) + + responses, err := s.agent.ListKeys(token) + if err != nil { + return nil, err + } + for _, response := range responses.Responses { + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + } + + return responses.Responses, nil +} + +// OperatorKeyringRemove is used to list the keys installed in the cluster +func (s *HTTPServer) OperatorKeyringRemove(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "DELETE" { + resp.WriteHeader(http.StatusMethodNotAllowed) + return nil, nil + } + + var args structs.KeyringRequest + if err := decodeBody(req, &args, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + s.parseToken(req, &args.Token) + + responses, err := s.agent.RemoveKey(args.Key, args.Token) + if err != nil { + return nil, err + } + for _, response := range responses.Responses { + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + } + + return nil, nil +} + +// OperatorKeyringUse is used to change the primary gossip encryption key +func (s *HTTPServer) OperatorKeyringUse(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + resp.WriteHeader(http.StatusMethodNotAllowed) + return nil, nil + } + + var args structs.KeyringRequest + if err := decodeBody(req, &args, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + s.parseToken(req, &args.Token) + + responses, err := s.agent.UseKey(args.Key, args.Token) + if err != nil { + return nil, err + } + for _, response := range responses.Responses { + if response.Error != "" { + return nil, fmt.Errorf(response.Error) + } + } + + return nil, nil +} diff --git a/command/agent/operator_endpoint_test.go b/command/agent/operator_endpoint_test.go index bc9b51ad4..8bac5858c 100644 --- a/command/agent/operator_endpoint_test.go +++ b/command/agent/operator_endpoint_test.go @@ -2,6 +2,7 @@ package agent import ( "bytes" + "fmt" "net/http" "net/http/httptest" "strings" @@ -56,3 +57,185 @@ func TestOperator_OperatorRaftPeer(t *testing.T) { } }) } + +func TestOperator_KeyringInstall(t *testing.T) { + oldKey := "H3/9gBxcKKRf45CaI2DlRg==" + newKey := "z90lFx3sZZLtTOkutXcwYg==" + configFunc := func(c *Config) { + c.EncryptKey = oldKey + } + httpTestWithConfig(t, func(srv *HTTPServer) { + body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) + req, err := http.NewRequest("PUT", "/v1/operator/keyring/install", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringInstall(resp, req) + if err != nil { + t.Fatalf("err: %s", err) + } + + listResponse, err := srv.agent.ListKeys("") + if err != nil { + t.Fatalf("err: %s", err) + } + + for _, response := range listResponse.Responses { + count, ok := response.Keys[newKey] + if !ok { + t.Fatalf("bad: %v", response.Keys) + } + if count != response.NumNodes { + t.Fatalf("bad: %d, %d", count, response.NumNodes) + } + } + }, configFunc) +} + +func TestOperator_KeyringList(t *testing.T) { + key := "H3/9gBxcKKRf45CaI2DlRg==" + configFunc := func(c *Config) { + c.EncryptKey = key + } + httpTestWithConfig(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", "/v1/operator/keyring/list", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + r, err := srv.OperatorKeyringList(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + responses, ok := r.([]*structs.KeyringResponse) + if !ok { + t.Fatalf("err: %v", !ok) + } + + // Check that we get both a LAN and WAN response, and that they both only + // contain the original key + if len(responses) != 2 { + t.Fatalf("bad: %d", len(responses)) + } + for _, response := range responses { + if len(response.Keys) != 1 { + t.Fatalf("bad: %d", len(response.Keys)) + } + if _, ok := response.Keys[key]; !ok { + t.Fatalf("bad: %v", ok) + } + } + }, configFunc) +} + +func TestOperator_KeyringRemove(t *testing.T) { + key := "H3/9gBxcKKRf45CaI2DlRg==" + tempKey := "z90lFx3sZZLtTOkutXcwYg==" + configFunc := func(c *Config) { + c.EncryptKey = key + } + httpTestWithConfig(t, func(srv *HTTPServer) { + _, err := srv.agent.InstallKey(tempKey, "") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Make sure the temp key is installed + list, err := srv.agent.ListKeys("") + if err != nil { + t.Fatalf("err: %v", err) + } + responses := list.Responses + if len(responses) != 2 { + t.Fatalf("bad: %d", len(responses)) + } + for _, response := range responses { + if len(response.Keys) != 2 { + t.Fatalf("bad: %d", len(response.Keys)) + } + if _, ok := response.Keys[tempKey]; !ok { + t.Fatalf("bad: %v", ok) + } + } + + body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", tempKey)) + req, err := http.NewRequest("DELETE", "/v1/operator/keyring/remove", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringRemove(resp, req) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the temp key has been removed + list, err = srv.agent.ListKeys("") + if err != nil { + t.Fatalf("err: %v", err) + } + responses = list.Responses + if len(responses) != 2 { + t.Fatalf("bad: %d", len(responses)) + } + for _, response := range responses { + if len(response.Keys) != 1 { + t.Fatalf("bad: %d", len(response.Keys)) + } + if _, ok := response.Keys[tempKey]; ok { + t.Fatalf("bad: %v", ok) + } + } + }, configFunc) +} + +func TestOperator_KeyringUse(t *testing.T) { + oldKey := "H3/9gBxcKKRf45CaI2DlRg==" + newKey := "z90lFx3sZZLtTOkutXcwYg==" + configFunc := func(c *Config) { + c.EncryptKey = oldKey + } + httpTestWithConfig(t, func(srv *HTTPServer) { + if _, err := srv.agent.InstallKey(newKey, ""); err != nil { + t.Fatalf("err: %v", err) + } + + body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) + req, err := http.NewRequest("PUT", "/v1/operator/keyring/use", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringUse(resp, req) + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := srv.agent.RemoveKey(oldKey, ""); err != nil { + t.Fatalf("err: %v", err) + } + + // Make sure only the new key remains + list, err := srv.agent.ListKeys("") + if err != nil { + t.Fatalf("err: %v", err) + } + responses := list.Responses + if len(responses) != 2 { + t.Fatalf("bad: %d", len(responses)) + } + for _, response := range responses { + if len(response.Keys) != 1 { + t.Fatalf("bad: %d", len(response.Keys)) + } + if _, ok := response.Keys[newKey]; !ok { + t.Fatalf("bad: %v", ok) + } + } + }, configFunc) +} diff --git a/command/keygen.go b/command/keygen.go index 0bb4c5db8..f0f9d70c2 100644 --- a/command/keygen.go +++ b/command/keygen.go @@ -4,8 +4,9 @@ import ( "crypto/rand" "encoding/base64" "fmt" - "github.com/mitchellh/cli" "strings" + + "github.com/mitchellh/cli" ) // KeygenCommand is a Command implementation that generates an encryption diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 837d34a8b..a51658ddc 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -906,10 +906,10 @@ func (r *KeyringRequest) RequestDatacenter() string { type KeyringResponse struct { WAN bool Datacenter string - Messages map[string]string + Messages map[string]string `json:",omitempty"` Keys map[string]int NumNodes int - Error string + Error string `json:",omitempty"` } // KeyringResponses holds multiple responses to keyring queries. Each diff --git a/testutil/server.go b/testutil/server.go index aad60e386..831fafa9b 100644 --- a/testutil/server.go +++ b/testutil/server.go @@ -70,6 +70,7 @@ type TestServerConfig struct { ACLMasterToken string `json:"acl_master_token,omitempty"` ACLDatacenter string `json:"acl_datacenter,omitempty"` ACLDefaultPolicy string `json:"acl_default_policy,omitempty"` + Encrypt string `json:"encrypt,omitempty"` Stdout, Stderr io.Writer `json:"-"` }