From 24942f507f40292097f26e74f8f7bb107d021bb5 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Mon, 14 Nov 2016 21:54:37 -0500 Subject: [PATCH 1/5] Add keyring http endpoints --- api/operator.go | 78 ++++++++++ api/operator_test.go | 68 +++++++++ command/agent/http.go | 4 + command/agent/operator_endpoint.go | 108 ++++++++++++++ command/agent/operator_endpoint_test.go | 183 ++++++++++++++++++++++++ command/keygen.go | 3 +- command/operator.go | 2 +- consul/structs/structs.go | 4 +- testutil/server.go | 1 + 9 files changed, 447 insertions(+), 4 deletions(-) diff --git a/api/operator.go b/api/operator.go index 48d74f3ca6ab..9df13d5d4352 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 f9d242b81087..9fd1b5d5c901 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 082fc039b54e..fb8d280a7549 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 cdab48c387bc..93d1d197288d 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 bc9b51ad4ea4..8bac5858c8ad 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 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:"-"` } From babe41cc9c74cd2c39f9d62d06762b7576ca65b7 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 15 Nov 2016 15:33:57 -0500 Subject: [PATCH 2/5] Add keyring api website doc sections --- .../docs/agent/http/operator.html.markdown | 137 ++++++++++++++++++ .../source/docs/internals/acl.html.markdown | 1 + 2 files changed, 138 insertions(+) diff --git a/website/source/docs/agent/http/operator.html.markdown b/website/source/docs/agent/http/operator.html.markdown index 763625c70363..4facee96942c 100644 --- a/website/source/docs/agent/http/operator.html.markdown +++ b/website/source/docs/agent/http/operator.html.markdown @@ -27,6 +27,10 @@ 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/install`](#keyring-install): Installs a new key into the keyring +* [`/v1/operator/keyring/list`](#keyring-list): Lists the installed gossip encryption keys +* [`/v1/operator/keyring/remove`](#keyring-remove): Removes a gossip key from the cluster +* [`/v1/operator/keyring/use`](#keyring-use): Changes the active encryption key Not all endpoints support blocking queries and all consistency modes, see details in the sections below. @@ -130,3 +134,136 @@ 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/install + +The keyring install endpoint supports the `PUT` method. + +#### PUT Method + +Using the `PUT` 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 register endpoint expects a JSON request body to be PUT. 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. + +### /v1/operator/keyring/list + +The keyring install endpoint supports the `GET` method. + +#### 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. + +### /v1/operator/keyring/remove + +The keyring remove endpoint supports the `PUT` method. + +#### PUT Method + +Using the `PUT` 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 register endpoint expects a JSON request body to be PUT. 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. + +### /v1/operator/keyring/use + +The keyring use endpoint supports the `PUT` method. + +#### 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 register endpoint expects a JSON request body to be PUT. 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. 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 From 3b8b6ae1a9ef1808cf225ea0f0a3c12404d915bc Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Wed, 16 Nov 2016 18:08:54 -0500 Subject: [PATCH 3/5] Add version specific note to keyring http docs --- .../source/docs/agent/http/operator.html.markdown | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/http/operator.html.markdown b/website/source/docs/agent/http/operator.html.markdown index 4facee96942c..b80a1005aacb 100644 --- a/website/source/docs/agent/http/operator.html.markdown +++ b/website/source/docs/agent/http/operator.html.markdown @@ -136,7 +136,8 @@ The return code will indicate success or failure. ### /v1/operator/keyring/install -The keyring install endpoint supports the `PUT` method. +Available in Consul 0.7.2 and later, the keyring install endpoint supports the +`PUT` method. #### PUT Method @@ -163,7 +164,8 @@ The return code will indicate success or failure. ### /v1/operator/keyring/list -The keyring install endpoint supports the `GET` method. +Available in Consul 0.7.2 and later, the keyring install endpoint supports the +`GET` method. #### GET Method @@ -214,7 +216,8 @@ A JSON body is returned that looks like this: ### /v1/operator/keyring/remove -The keyring remove endpoint supports the `PUT` method. +Available in Consul 0.7.2 and later, the keyring remove endpoint supports the +`PUT` method. #### PUT Method @@ -242,7 +245,8 @@ The return code will indicate success or failure. ### /v1/operator/keyring/use -The keyring use endpoint supports the `PUT` method. +Available in Consul 0.7.2 and later, the keyring use endpoint supports the `PUT` +method. #### PUT Method From 727dc873a71338f49ef18d3fd7d50dd5a294d626 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 22 Nov 2016 18:24:40 -0500 Subject: [PATCH 4/5] Combine keyring endpoints into one --- api/operator.go | 30 +++-- api/operator_test.go | 10 +- command/agent/http.go | 5 +- command/agent/operator_endpoint.go | 123 +++++++----------- command/agent/operator_endpoint_test.go | 47 ++++--- .../docs/agent/http/operator.html.markdown | 85 +++++------- 6 files changed, 140 insertions(+), 160 deletions(-) diff --git a/api/operator.go b/api/operator.go index 9df13d5d4352..502f3704e15c 100644 --- a/api/operator.go +++ b/api/operator.go @@ -43,8 +43,8 @@ type RaftConfiguration struct { Index uint64 } -// KeyringOpts is used for performing Keyring operations -type KeyringOpts struct { +// keyringRequest is used for performing Keyring operations +type keyringRequest struct { Key string `json:",omitempty"` } @@ -101,9 +101,10 @@ func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) err } // 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{ +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)) @@ -115,8 +116,9 @@ func (op *Operator) KeyringInstall(key string) error { } // 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") +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 @@ -131,9 +133,10 @@ func (op *Operator) KeyringList() ([]*KeyringResponse, error) { } // 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{ +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)) @@ -145,9 +148,10 @@ func (op *Operator) KeyringRemove(key string) error { } // 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{ +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)) diff --git a/api/operator_test.go b/api/operator_test.go index 9fd1b5d5c901..e55495e2e95e 100644 --- a/api/operator_test.go +++ b/api/operator_test.go @@ -49,11 +49,11 @@ func TestOperator_KeyringInstallListPutRemove(t *testing.T) { defer s.Stop() operator := c.Operator() - if err := operator.KeyringInstall(newKey); err != nil { + if err := operator.KeyringInstall(newKey, nil); err != nil { t.Fatalf("err: %v", err) } - listResponses, err := operator.KeyringList() + listResponses, err := operator.KeyringList(nil) if err != nil { t.Fatalf("err %v", err) } @@ -75,15 +75,15 @@ func TestOperator_KeyringInstallListPutRemove(t *testing.T) { } // Switch the primary to the new key - if err := operator.KeyringUse(newKey); err != nil { + if err := operator.KeyringUse(newKey, nil); err != nil { t.Fatalf("err: %v", err) } - if err := operator.KeyringRemove(oldKey); err != nil { + if err := operator.KeyringRemove(oldKey, nil); err != nil { t.Fatalf("err: %v", err) } - listResponses, err = operator.KeyringList() + listResponses, err = operator.KeyringList(nil) if err != nil { t.Fatalf("err %v", err) } diff --git a/command/agent/http.go b/command/agent/http.go index fb8d280a7549..0d13ee4fd233 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -291,10 +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/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/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 93d1d197288d..ac5377a614dd 100644 --- a/command/agent/operator_endpoint.go +++ b/command/agent/operator_endpoint.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/hashicorp/consul/consul/structs" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/raft" ) @@ -57,109 +58,85 @@ 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 +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) - 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))) + // 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 } - s.parseToken(req, &args.Token) +} +// 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 } - for _, response := range responses.Responses { - if response.Error != "" { - return nil, fmt.Errorf(response.Error) - } - } - return nil, nil + return nil, keyringErrorsOrNil(responses.Responses) } -// 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) +// 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 } - for _, response := range responses.Responses { - if response.Error != "" { - return nil, fmt.Errorf(response.Error) - } - } - return responses.Responses, nil + return responses.Responses, keyringErrorsOrNil(responses.Responses) } -// 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) - +// 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 } - for _, response := range responses.Responses { - if response.Error != "" { - return nil, fmt.Errorf(response.Error) - } - } - return nil, nil + return nil, keyringErrorsOrNil(responses.Responses) } -// 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) - +// 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 } - for _, response := range responses.Responses { + + return nil, keyringErrorsOrNil(responses.Responses) +} + +func keyringErrorsOrNil(responses []*structs.KeyringResponse) error { + var errs error + for _, response := range responses { if response.Error != "" { - return nil, fmt.Errorf(response.Error) + errs = multierror.Append(errs, fmt.Errorf(response.Error)) } } - - return nil, nil + return errs } diff --git a/command/agent/operator_endpoint_test.go b/command/agent/operator_endpoint_test.go index 8bac5858c8ad..9aff08a4b2b3 100644 --- a/command/agent/operator_endpoint_test.go +++ b/command/agent/operator_endpoint_test.go @@ -66,13 +66,13 @@ func TestOperator_KeyringInstall(t *testing.T) { } httpTestWithConfig(t, func(srv *HTTPServer) { body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) - req, err := http.NewRequest("PUT", "/v1/operator/keyring/install", body) + req, err := http.NewRequest("POST", "/v1/operator/keyring", body) if err != nil { t.Fatalf("err: %v", err) } resp := httptest.NewRecorder() - _, err = srv.OperatorKeyringInstall(resp, req) + _, err = srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %s", err) } @@ -81,6 +81,9 @@ func TestOperator_KeyringInstall(t *testing.T) { 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] @@ -100,13 +103,13 @@ func TestOperator_KeyringList(t *testing.T) { c.EncryptKey = key } httpTestWithConfig(t, func(srv *HTTPServer) { - req, err := http.NewRequest("GET", "/v1/operator/keyring/list", nil) + req, err := http.NewRequest("GET", "/v1/operator/keyring", nil) if err != nil { t.Fatalf("err: %v", err) } resp := httptest.NewRecorder() - r, err := srv.OperatorKeyringList(resp, req) + r, err := srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -120,13 +123,27 @@ func TestOperator_KeyringList(t *testing.T) { 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) - } + + // 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) } @@ -162,13 +179,13 @@ func TestOperator_KeyringRemove(t *testing.T) { } body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", tempKey)) - req, err := http.NewRequest("DELETE", "/v1/operator/keyring/remove", body) + req, err := http.NewRequest("DELETE", "/v1/operator/keyring", body) if err != nil { t.Fatalf("err: %v", err) } resp := httptest.NewRecorder() - _, err = srv.OperatorKeyringRemove(resp, req) + _, err = srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %s", err) } @@ -205,13 +222,13 @@ func TestOperator_KeyringUse(t *testing.T) { } body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey)) - req, err := http.NewRequest("PUT", "/v1/operator/keyring/use", body) + req, err := http.NewRequest("PUT", "/v1/operator/keyring", body) if err != nil { t.Fatalf("err: %v", err) } resp := httptest.NewRecorder() - _, err = srv.OperatorKeyringUse(resp, req) + _, err = srv.OperatorKeyringEndpoint(resp, req) if err != nil { t.Fatalf("err: %s", err) } diff --git a/website/source/docs/agent/http/operator.html.markdown b/website/source/docs/agent/http/operator.html.markdown index b80a1005aacb..4173a87601e2 100644 --- a/website/source/docs/agent/http/operator.html.markdown +++ b/website/source/docs/agent/http/operator.html.markdown @@ -27,10 +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/install`](#keyring-install): Installs a new key into the keyring -* [`/v1/operator/keyring/list`](#keyring-list): Lists the installed gossip encryption keys -* [`/v1/operator/keyring/remove`](#keyring-remove): Removes a gossip key from the cluster -* [`/v1/operator/keyring/use`](#keyring-use): Changes the active encryption key +* [`/v1/operator/keyring`](#keyring): Operates on gossip keyring Not all endpoints support blocking queries and all consistency modes, see details in the sections below. @@ -134,38 +131,13 @@ 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/install +### /v1/operator/keyring -Available in Consul 0.7.2 and later, the keyring install endpoint supports the -`PUT` method. +Available in Consul 0.7.2 and later, the keyring endpoint supports the +`GET`, `POST`, `PUT` and `DELETE` methods. -#### PUT Method - -Using the `PUT` 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 register endpoint expects a JSON request body to be PUT. 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. - -### /v1/operator/keyring/list - -Available in Consul 0.7.2 and later, the keyring install endpoint supports the -`GET` method. +This endpoint supports the use of ACL tokens using either the `X-CONSUL-TOKEN` +header or the "?token=" query parameter. #### GET Method @@ -214,16 +186,10 @@ A JSON body is returned that looks like this: `NumNodes` is the total number of nodes in the datacenter. -### /v1/operator/keyring/remove - -Available in Consul 0.7.2 and later, the keyring remove endpoint supports the -`PUT` method. - -#### PUT Method +#### POST Method -Using the `PUT` 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 +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 register endpoint expects a JSON request body to be PUT. The request @@ -231,11 +197,11 @@ body must look like: ```javascript { - "Key": "3lg9DxVfKNzI8O+IQ5Ek+Q==" + "Key": "3lg9DxVfKNzI8O+IQ5Ek+Q==" } ``` -The `Key` field is mandatory and provides the encryption key to remove from the +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 @@ -243,11 +209,6 @@ 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/use - -Available in Consul 0.7.2 and later, the keyring use endpoint supports the `PUT` -method. - #### PUT Method Using the `PUT` method, this endpoint will change the primary gossip encryption @@ -271,3 +232,27 @@ 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 register endpoint expects a JSON request body to be PUT. 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. From 567f1883a8ad9d8c4c6379a400eca5cc3a4329c5 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 22 Nov 2016 19:03:27 -0500 Subject: [PATCH 5/5] Fix keyring doc method wording --- api/operator.go | 2 +- website/source/docs/agent/http/operator.html.markdown | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/operator.go b/api/operator.go index 502f3704e15c..a8d04a38eb88 100644 --- a/api/operator.go +++ b/api/operator.go @@ -45,7 +45,7 @@ type RaftConfiguration struct { // keyringRequest is used for performing Keyring operations type keyringRequest struct { - Key string `json:",omitempty"` + Key string } // KeyringResponse is returned when listing the gossip encryption keys diff --git a/website/source/docs/agent/http/operator.html.markdown b/website/source/docs/agent/http/operator.html.markdown index 4173a87601e2..cc3019158a67 100644 --- a/website/source/docs/agent/http/operator.html.markdown +++ b/website/source/docs/agent/http/operator.html.markdown @@ -192,7 +192,7 @@ 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 register endpoint expects a JSON request body to be PUT. The request +The POST method expects a JSON request body to be submitted. The request body must look like: ```javascript @@ -216,7 +216,7 @@ 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 register endpoint expects a JSON request body to be PUT. The request +The PUT method expects a JSON request body to be submitted. The request body must look like: ```javascript @@ -240,7 +240,7 @@ the cluster. This operation may only be performed on keys which are not currentl the primary key. There is more information on gossip encryption available [here](/docs/agent/encryption.html#gossip-encryption). -The register endpoint expects a JSON request body to be PUT. The request +The DELETE method expects a JSON request body to be submitted. The request body must look like: ```javascript