diff --git a/api/operator.go b/api/operator.go index 48d74f3ca6ab..a8d04a38eb88 100644 --- a/api/operator.go +++ b/api/operator.go @@ -43,6 +43,26 @@ type RaftConfiguration struct { Index uint64 } +// keyringRequest is used for performing Keyring operations +type keyringRequest struct { + Key string +} + +// 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,65 @@ 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, q *WriteOptions) error { + r := op.c.newRequest("POST", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + 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(q *QueryOptions) ([]*KeyringResponse, error) { + r := op.c.newRequest("GET", "/v1/operator/keyring") + r.setQueryOptions(q) + _, 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, q *WriteOptions) error { + r := op.c.newRequest("DELETE", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + 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, q *WriteOptions) error { + r := op.c.newRequest("PUT", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + 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 f9d242b81087..e55495e2e95e 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, nil); err != nil { + t.Fatalf("err: %v", err) + } + + listResponses, err := operator.KeyringList(nil) + 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, nil); err != nil { + t.Fatalf("err: %v", err) + } + + if err := operator.KeyringRemove(oldKey, nil); err != nil { + t.Fatalf("err: %v", err) + } + + listResponses, err = operator.KeyringList(nil) + 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 082fc039b54e..0d13ee4fd233 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -291,6 +291,7 @@ 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", s.wrap(s.OperatorKeyringEndpoint)) 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 cdab48c387bc..ac5377a614dd 100644 --- a/command/agent/operator_endpoint.go +++ b/command/agent/operator_endpoint.go @@ -1,9 +1,11 @@ package agent import ( + "fmt" "net/http" "github.com/hashicorp/consul/consul/structs" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/raft" ) @@ -55,3 +57,86 @@ func (s *HTTPServer) OperatorRaftPeer(resp http.ResponseWriter, req *http.Reques } return nil, nil } + +type keyringArgs struct { + Key string + Token string +} + +// OperatorKeyringEndpoint handles keyring operations (install, list, use, remove) +func (s *HTTPServer) OperatorKeyringEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + var args keyringArgs + if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" { + 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) + + // Switch on the method + switch req.Method { + case "GET": + return s.KeyringList(resp, req, &args) + case "POST": + return s.KeyringInstall(resp, req, &args) + case "PUT": + return s.KeyringUse(resp, req, &args) + case "DELETE": + return s.KeyringRemove(resp, req, &args) + default: + resp.WriteHeader(405) + return nil, nil + } +} + +// KeyringInstall is used to install a new gossip encryption key into the cluster +func (s *HTTPServer) KeyringInstall(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) { + responses, err := s.agent.InstallKey(args.Key, args.Token) + if err != nil { + return nil, err + } + + return nil, keyringErrorsOrNil(responses.Responses) +} + +// KeyringList is used to list the keys installed in the cluster +func (s *HTTPServer) KeyringList(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) { + responses, err := s.agent.ListKeys(args.Token) + if err != nil { + return nil, err + } + + return responses.Responses, keyringErrorsOrNil(responses.Responses) +} + +// KeyringRemove is used to list the keys installed in the cluster +func (s *HTTPServer) KeyringRemove(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) { + responses, err := s.agent.RemoveKey(args.Key, args.Token) + if err != nil { + return nil, err + } + + return nil, keyringErrorsOrNil(responses.Responses) +} + +// KeyringUse is used to change the primary gossip encryption key +func (s *HTTPServer) KeyringUse(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) { + responses, err := s.agent.UseKey(args.Key, args.Token) + if err != nil { + return nil, err + } + + return nil, keyringErrorsOrNil(responses.Responses) +} + +func keyringErrorsOrNil(responses []*structs.KeyringResponse) error { + var errs error + for _, response := range responses { + if response.Error != "" { + errs = multierror.Append(errs, fmt.Errorf(response.Error)) + } + } + return errs +} diff --git a/command/agent/operator_endpoint_test.go b/command/agent/operator_endpoint_test.go index bc9b51ad4ea4..9aff08a4b2b3 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,202 @@ 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("POST", "/v1/operator/keyring", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringEndpoint(resp, req) + if err != nil { + t.Fatalf("err: %s", err) + } + + listResponse, err := srv.agent.ListKeys("") + if err != nil { + t.Fatalf("err: %s", err) + } + if len(listResponse.Responses) != 2 { + t.Fatalf("bad: %d", len(listResponse.Responses)) + } + + 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", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + r, err := srv.OperatorKeyringEndpoint(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)) + } + + // WAN + if len(responses[0].Keys) != 1 { + t.Fatalf("bad: %d", len(responses[0].Keys)) + } + if !responses[0].WAN { + t.Fatalf("bad: %v", responses[0].WAN) + } + if _, ok := responses[0].Keys[key]; !ok { + t.Fatalf("bad: %v", ok) + } + + // LAN + if len(responses[1].Keys) != 1 { + t.Fatalf("bad: %d", len(responses[1].Keys)) + } + if responses[1].WAN { + t.Fatalf("bad: %v", responses[1].WAN) + } + if _, ok := responses[1].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", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringEndpoint(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", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.OperatorKeyringEndpoint(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 0bb4c5db8309..f0f9d70c2637 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/command/operator.go b/command/operator.go index 68ae5853198b..b049e325a933 100644 --- a/command/operator.go +++ b/command/operator.go @@ -24,7 +24,7 @@ Usage: consul operator [common options] [action] [options] the Raft subsystem. NOTE: Use this command with extreme caution, as improper use could lead to a Consul outage and even loss of data. - If ACLs are enabled then a token with operator privileges may required in + If ACLs are enabled then a token with operator privileges may be required in order to use this command. Requests are forwarded internally to the leader if required, so this can be run from any Consul node in a cluster. diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 837d34a8bd4c..a51658ddcb8e 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 aad60e3866b6..831fafa9b70c 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:"-"` } diff --git a/website/source/docs/agent/http/operator.html.markdown b/website/source/docs/agent/http/operator.html.markdown index 763625c70363..cc3019158a67 100644 --- a/website/source/docs/agent/http/operator.html.markdown +++ b/website/source/docs/agent/http/operator.html.markdown @@ -27,6 +27,7 @@ The following endpoints are supported: * [`/v1/operator/raft/configuration`](#raft-configuration): Inspects the Raft configuration * [`/v1/operator/raft/peer`](#raft-peer): Operates on Raft peers +* [`/v1/operator/keyring`](#keyring): Operates on gossip keyring Not all endpoints support blocking queries and all consistency modes, see details in the sections below. @@ -130,3 +131,128 @@ If ACLs are enabled, the client will need to supply an ACL Token with The return code will indicate success or failure. +### /v1/operator/keyring + +Available in Consul 0.7.2 and later, the keyring endpoint supports the +`GET`, `POST`, `PUT` and `DELETE` methods. + +This endpoint supports the use of ACL tokens using either the `X-CONSUL-TOKEN` +header or the "?token=" query parameter. + +#### GET Method + +Using the `GET` method, this endpoint will list the gossip encryption keys +installed on both the WAN and LAN rings of every known datacenter. There is more +information on gossip encryption available +[here](/docs/agent/encryption.html#gossip-encryption). + +If ACLs are enabled, the client will need to supply an ACL Token with +[`keyring`](/docs/internals/acl.html#keyring) read privileges. + +A JSON body is returned that looks like this: + +```javascript +[ + { + "WAN": true, + "Datacenter": "dc1", + "Keys": { + "0eK8RjnsGC/+I1fJErQsBA==": 1, + "G/3/L4yOw3e5T7NTvuRi9g==": 1, + "z90lFx3sZZLtTOkutXcwYg==": 1 + }, + "NumNodes": 1 + }, + { + "WAN": false, + "Datacenter": "dc1", + "Keys": { + "0eK8RjnsGC/+I1fJErQsBA==": 1, + "G/3/L4yOw3e5T7NTvuRi9g==": 1, + "z90lFx3sZZLtTOkutXcwYg==": 1 + }, + "NumNodes": 1 + } +] +``` + +`WAN` is true if the block refers to the WAN ring of that datacenter (rather than + LAN). + +`Datacenter` is the datacenter the block refers to. + +`Keys` is a map of each gossip key to the number of nodes it's currently installed + on. + +`NumNodes` is the total number of nodes in the datacenter. + +#### POST Method + +Using the `POST` method, this endpoint will install a new gossip encryption key +into the cluster. There is more information on gossip encryption available +[here](/docs/agent/encryption.html#gossip-encryption). + +The POST method expects a JSON request body to be submitted. The request +body must look like: + +```javascript +{ + "Key": "3lg9DxVfKNzI8O+IQ5Ek+Q==" +} +``` + +The `Key` field is mandatory and provides the encryption key to install into the +cluster. + +If ACLs are enabled, the client will need to supply an ACL Token with +[`keyring`](/docs/internals/acl.html#keyring) write privileges. + +The return code will indicate success or failure. + +#### PUT Method + +Using the `PUT` method, this endpoint will change the primary gossip encryption +key. The key must already be installed before this operation can succeed. There +is more information on gossip encryption available +[here](/docs/agent/encryption.html#gossip-encryption). + +The PUT method expects a JSON request body to be submitted. The request +body must look like: + +```javascript +{ + "Key": "3lg9DxVfKNzI8O+IQ5Ek+Q==" +} +``` + +The `Key` field is mandatory and provides the primary encryption key to begin +using. + +If ACLs are enabled, the client will need to supply an ACL Token with +[`keyring`](/docs/internals/acl.html#keyring) write privileges. + +The return code will indicate success or failure. + +#### DELETE Method + +Using the `DELETE` method, this endpoint will remove a gossip encryption key from +the cluster. This operation may only be performed on keys which are not currently +the primary key. There is more information on gossip encryption available +[here](/docs/agent/encryption.html#gossip-encryption). + +The DELETE method expects a JSON request body to be submitted. The request +body must look like: + +```javascript +{ + "Key": "3lg9DxVfKNzI8O+IQ5Ek+Q==" +} +``` + +The `Key` field is mandatory and provides the encryption key to remove from the +cluster. + +If ACLs are enabled, the client will need to supply an ACL Token with +[`keyring`](/docs/internals/acl.html#keyring) write privileges. + +The return code will indicate success or failure. diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 78ba000e7c90..fa091879d2a0 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -336,6 +336,7 @@ access to each API token based on the events they should be able to fire. After Consul 0.6.3, significant changes were made to ACLs for prepared queries, including a new `query` ACL policy. See [Prepared Query ACLs](#prepared_query_acls) below for more details. + #### Blacklist Mode and Keyring Operations Consul 0.6 and later supports securing the encryption keyring operations using