diff --git a/client/const.go b/client/const.go index a19b49187..617c1ea00 100644 --- a/client/const.go +++ b/client/const.go @@ -27,6 +27,7 @@ const ( useKeyCommand = "use-key" removeKeyCommand = "remove-key" listKeysCommand = "list-keys" + getPrimaryKeyCommand = "get-primary-key" tagsCommand = "tags" queryCommand = "query" respondCommand = "respond" diff --git a/client/rpc_client.go b/client/rpc_client.go index d643ea7c9..273c989de 100644 --- a/client/rpc_client.go +++ b/client/rpc_client.go @@ -370,6 +370,19 @@ func (c *RPCClient) ListKeys() (map[string]int, int, map[string]string, error) { return resp.Keys, resp.NumNodes, resp.Messages, err } +// GetPrimaryKey returns the current encyption key used in cluster +func (c *RPCClient) GetPrimaryKey() (map[string]int, int, map[string]string, error) { + header := requestHeader{ + Command: getPrimaryKeyCommand, + Seq: c.getSeq(), + } + + resp := keyResponse{} + err := c.genericRPC(&header, nil, &resp) + + return resp.Keys, resp.NumNodes, resp.Messages, err +} + // Stats is used to get debugging state information func (c *RPCClient) Stats() (map[string]map[string]string, error) { header := requestHeader{ diff --git a/cmd/serf/command/agent/agent.go b/cmd/serf/command/agent/agent.go index c35d13e36..9afaefa16 100644 --- a/cmd/serf/command/agent/agent.go +++ b/cmd/serf/command/agent/agent.go @@ -299,6 +299,14 @@ func (a *Agent) ListKeys() (*serf.KeyResponse, error) { return manager.ListKeys() } +// GetPrimaryKey sends a query to all members to return a list containing +// the primary encryption key +func (a *Agent) GetPrimaryKey() (*serf.KeyResponse, error) { + a.logger.Print("[INFO] agent: Initiating primary key retrieval") + manager := a.serf.KeyManager() + return manager.GetPrimaryKey() +} + // SetTags is used to update the tags. The agent will make sure to // persist tags if necessary before gossiping to the cluster. func (a *Agent) SetTags(tags map[string]string) error { diff --git a/cmd/serf/command/agent/ipc.go b/cmd/serf/command/agent/ipc.go index 74a8b2f53..1d59a2b2e 100644 --- a/cmd/serf/command/agent/ipc.go +++ b/cmd/serf/command/agent/ipc.go @@ -61,6 +61,7 @@ const ( useKeyCommand = "use-key" removeKeyCommand = "remove-key" listKeysCommand = "list-keys" + getPrimaryKeyCommand = "get-primary-key" tagsCommand = "tags" queryCommand = "query" respondCommand = "respond" @@ -518,6 +519,9 @@ func (i *AgentIPC) handleRequest(client *IPCClient, reqHeader *requestHeader) er case listKeysCommand: return i.handleListKeys(client, seq) + case getPrimaryKeyCommand: + return i.handleGetPrimaryKey(client, seq) + case tagsCommand: return i.handleTags(client, seq) @@ -820,6 +824,24 @@ func (i *AgentIPC) handleListKeys(client *IPCClient, seq uint64) error { return client.Send(&header, &resp) } +func (i *AgentIPC) handleGetPrimaryKey(client *IPCClient, seq uint64) error { + queryResp, err := i.agent.GetPrimaryKey() + + header := responseHeader{ + Seq: seq, + Error: errToString(err), + } + resp := keyResponse{ + Messages: queryResp.Messages, + Keys: queryResp.Keys, + NumNodes: queryResp.NumNodes, + NumErr: queryResp.NumErr, + NumResp: queryResp.NumResp, + } + + return client.Send(&header, &resp) +} + func (i *AgentIPC) handleStream(client *IPCClient, seq uint64) error { var es *eventStream var req streamRequest diff --git a/cmd/serf/command/keys.go b/cmd/serf/command/keys.go index 9d0920116..d2e3c332a 100644 --- a/cmd/serf/command/keys.go +++ b/cmd/serf/command/keys.go @@ -49,6 +49,7 @@ Options: will ask all nodes in the cluster for a list of keys and dump a summary containing each key and the number of members it is installed on to the console. + -getPrimaryKey Obtain the primary key used for encrypting messages. -rpc-addr=127.0.0.1:7373 RPC address of the Serf agent. -rpc-auth="" RPC auth token of the Serf agent. ` @@ -58,7 +59,7 @@ Options: func (c *KeysCommand) Run(args []string) int { var installKey, useKey, removeKey string var lines []string - var listKeys bool + var listKeys, getPrimaryKey bool cmdFlags := flag.NewFlagSet("key", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -66,6 +67,7 @@ func (c *KeysCommand) Run(args []string) int { cmdFlags.StringVar(&useKey, "use", "", "change primary encryption key") cmdFlags.StringVar(&removeKey, "remove", "", "remove a key") cmdFlags.BoolVar(&listKeys, "list", false, "list cluster keys") + cmdFlags.BoolVar(&getPrimaryKey, "getPrimaryKey", false, "get primary encryption key") rpcAddr := RPCAddrFlag(cmdFlags) rpcAuth := RPCAuthFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -80,10 +82,14 @@ func (c *KeysCommand) Run(args []string) int { } // Make sure that we only have one actionable argument to avoid ambiguity - found := listKeys + found := listKeys || getPrimaryKey + if listKeys && getPrimaryKey { + c.Ui.Error("Only one of -getPrimaryKey or -list allowed") + return 1 + } for _, arg := range []string{installKey, useKey, removeKey} { if found && len(arg) > 0 { - c.Ui.Error("Only one of -install, -use, -remove, or -list allowed") + c.Ui.Error("Only one of -install, -use, -remove, -getPrimaryKey, or -list allowed") return 1 } found = found || len(arg) > 0 @@ -132,6 +138,34 @@ func (c *KeysCommand) Run(args []string) int { return 0 } + if getPrimaryKey { + c.Ui.Info("Getting the primary encryption key") + keys, _, failures, err := client.GetPrimaryKey() + + if err != nil { + if len(failures) > 0 { + for node, message := range failures { + lines = append(lines, fmt.Sprintf("failed: | %s | %s", node, message)) + } + out := columnize.SimpleFormat(lines) + c.Ui.Error(out) + } + + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Failed to gather member keys: %s", err)) + return 1 + } + + c.Ui.Info("Key obtained") + c.Ui.Output("") + + for key := range keys { + c.Ui.Output(key) + } + + return 0 + } + if installKey != "" { c.Ui.Info("Installing key on all members...") if failures, err := client.InstallKey(installKey); err != nil { diff --git a/serf/internal_query.go b/serf/internal_query.go index a74ebf705..235925a49 100644 --- a/serf/internal_query.go +++ b/serf/internal_query.go @@ -30,6 +30,9 @@ const ( // listKeysQuery is used to list all known keys in the cluster listKeysQuery = "list-keys" + // getPrimaryKeyQuery is used to obtain the current primary key + getPrimaryKeyQuery = "get-primary-key" + // minEncodedKeyLength is used to compute the max number of keys in a list key // response. eg 1024/25 = 40. a message with max size of 1024 bytes cannot // contain more than 40 keys. There is a test @@ -118,6 +121,8 @@ func (s *serfQueries) handleQuery(q *Query) { s.handleRemoveKey(q) case listKeysQuery: s.handleListKeys(q) + case getPrimaryKeyQuery: + s.handleGetPrimaryKey(q) default: s.logger.Printf("[WARN] serf: Unhandled internal query '%s'", queryName) } @@ -365,3 +370,25 @@ func (s *serfQueries) handleListKeys(q *Query) { SEND: s.sendKeyResponse(q, &response) } + +// handleGetPrimaryKey is invoked when a query is received to return the current +// primary key used by the Serf instance in the same fasion as handleListKeys method +func (s *serfQueries) handleGetPrimaryKey(q *Query) { + response := nodeKeyResponse{Result: false} + keyring := s.serf.config.MemberlistConfig.Keyring + + if !s.serf.EncryptionEnabled() { + response.Message = "Keyring is empty (encryption not enabled)" + s.logger.Printf("[ERR] serf: Keyring is empty (encryption not enabled)") + goto SEND + } + + s.logger.Printf("[INFO] serf: Received get-primary-key query") + + response.Keys = append(response.Keys, + base64.StdEncoding.EncodeToString(keyring.GetPrimaryKey())) + response.Result = true + +SEND: + s.sendKeyResponse(q, &response) +} diff --git a/serf/keymanager.go b/serf/keymanager.go index 11aaff2aa..464e0f24b 100644 --- a/serf/keymanager.go +++ b/serf/keymanager.go @@ -195,3 +195,17 @@ func (k *KeyManager) ListKeysWithOptions(opts *KeyRequestOptions) (*KeyResponse, return k.handleKeyRequest("", listKeysQuery, opts) } + +// GetPrimaryKey is used to obtain the currently used primary key from members in a Serf cluster +// and returns the response formatted as usual, but containing the single key in the list +// for WAN and LAN keyrings +func (k *KeyManager) GetPrimaryKey() (*KeyResponse, error) { + return k.GetPrimaryKeyWithOptions(nil) +} + +func (k *KeyManager) GetPrimaryKeyWithOptions(opts *KeyRequestOptions) (*KeyResponse, error) { + k.l.RLock() + defer k.l.RUnlock() + + return k.handleKeyRequest("", getPrimaryKeyQuery, opts) +} diff --git a/serf/keymanager_test.go b/serf/keymanager_test.go index 478d61b5d..a69f0e7f4 100644 --- a/serf/keymanager_test.go +++ b/serf/keymanager_test.go @@ -305,3 +305,61 @@ func TestSerf_ListKeys(t *testing.T) { } } } + +func TestSerf_GetPrimaryKey(t *testing.T) { + ip1, returnFn1 := testutil.TakeIP() + defer returnFn1() + + s1, err := testKeyringSerf(t, ip1) + if err != nil { + t.Fatalf("err: %v", err) + } + defer s1.Shutdown() + + manager := s1.KeyManager() + + initialPrimaryKeyBytes := s1.config.MemberlistConfig.Keyring.GetKeys()[0] + initialPrimaryKey := base64.StdEncoding.EncodeToString(initialPrimaryKeyBytes) + + response, err := manager.GetPrimaryKey() + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(response.Keys) != 1 { + t.Fatalf("Expected single key in keyring: %s, but got %v", initialPrimaryKey, response.Keys) + } + + // should be single key there + for k := range response.Keys { + if k != string(initialPrimaryKey) { + t.Fatalf("Expected to find single primary key equals %s, but got %s", initialPrimaryKey, k) + } + } + + extraKey := "5K9OtfP7efFrNKe5WCQvXvnaXJ5cWP0SvXiwe0kkjM4=" + extraKeyBytes, err := base64.StdEncoding.DecodeString(extraKey) + if err != nil { + t.Fatalf("err: %v", err) + } + + s1.config.MemberlistConfig.Keyring.AddKey(extraKeyBytes) + s1.config.MemberlistConfig.Keyring.UseKey(extraKeyBytes) + + response, err = manager.GetPrimaryKey() + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(response.Keys) != 1 { + t.Fatalf("Expected single key in keyring even after adding the key: %s, but got %v", initialPrimaryKey, response.Keys) + } + + // should be single key there + for k := range response.Keys { + if k != extraKey { + t.Fatalf("Expected to find single primary key equals %s, but got %s", extraKey, k) + } + } + +}