Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: change encryption keys #199

Merged
merged 65 commits into from Apr 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
33e1aa7
Initial work using memberlist keyring
ryanuber Apr 5, 2014
5668a34
Added basic ability to use a -keyring-file
ryanuber Apr 5, 2014
9f5db93
agent: added keyring writer, might need to move it into Serf since th…
ryanuber Apr 5, 2014
b47e22b
agent: fixed call to base64 encode (doesn't return errors)
ryanuber Apr 5, 2014
1cddd50
serf: implement WriteKeyringFile in Serf so that we can persist recei…
ryanuber Apr 5, 2014
0078582
command: added use-key skeleton
ryanuber Apr 5, 2014
a870ba4
command: use-key is now a functional command, and works with -keyring…
ryanuber Apr 5, 2014
abf711b
serf: only write keyring file if it is configured
ryanuber Apr 5, 2014
9e9929f
serf: implement remove-key, consolidate like code
ryanuber Apr 5, 2014
8f4f206
Return failed nodes when performing key operations
ryanuber Apr 6, 2014
7c4b3d3
serf: log a message when receiving key modify events
ryanuber Apr 6, 2014
e834f26
command: implement keyring subcommand with multiple commands beneath it
ryanuber Apr 7, 2014
c70a6a9
command: Consolidate all key functionality into a single command with…
ryanuber Apr 7, 2014
7b74972
command: better description of keys command and ordered arguments in …
ryanuber Apr 8, 2014
9cdabc9
Return all node errors and display them during key queries
ryanuber Apr 8, 2014
a4b3a3e
command: exit before connecting to rpc in key command
ryanuber Apr 8, 2014
cd968f4
command: better output during key command failures
ryanuber Apr 8, 2014
288940a
command: fixed key command name in metadata and help
ryanuber Apr 8, 2014
35e73bb
command: add note to key command that changes are broadcasted/applied…
ryanuber Apr 8, 2014
05e78ef
command: fancy up the key command output with a prefixed ui
ryanuber Apr 8, 2014
050ee26
Moved key functions and added types in serf/key.go
ryanuber Apr 9, 2014
6e3cb2c
website: first pass at docs for keyring functionality
ryanuber Apr 9, 2014
4557521
Make rigid encryption key option handling more verbose in error scena…
ryanuber Apr 9, 2014
3419766
agent: remove duplicate condition
ryanuber Apr 9, 2014
ca95c70
serf: trim down KeyResponse object and adjust key command
ryanuber Apr 9, 2014
486ae03
serf: first pass at tests for keyring
ryanuber Apr 10, 2014
06d1df0
serf: added tests for use-key and remove-key. need negative testing.
ryanuber Apr 10, 2014
5687e35
serf: test trying to use a non-existent primary key
ryanuber Apr 10, 2014
46c8081
Removed unhelpful tests
ryanuber Apr 10, 2014
c920d5f
command: better keyring file loading routine
ryanuber Apr 10, 2014
ed5a9f4
agent: add tests for loading keyring files
ryanuber Apr 10, 2014
36c990d
serf: split out all operations for key in internal_query.go
ryanuber Apr 11, 2014
3bd6cb4
serf: remove unused resp object
ryanuber Apr 11, 2014
1771c1b
Added key -list option to ask cluster for a collective list of keys
ryanuber Apr 11, 2014
adc65ed
Added comments, fixed log message, and changed query log messages to …
ryanuber Apr 11, 2014
b0c1c02
Better documentation for the key command
ryanuber Apr 11, 2014
c37462f
command: minor doc adjustments for key command
ryanuber Apr 11, 2014
afaf2f6
serf: added test for key list api
ryanuber Apr 11, 2014
8c6d5c3
Added return's in key.go to make sure we execute just 1 operation per…
ryanuber Apr 11, 2014
5ff7886
serf: added comments on undocumented functions
ryanuber Apr 11, 2014
175533d
Move key manipulation functionality into a KeyManager object to avoid…
ryanuber Apr 11, 2014
afdf549
Relay number of nodes with a given encryption key installed
ryanuber Apr 12, 2014
6007c80
Squashed identical response types into one type, removed unneeded req…
ryanuber Apr 12, 2014
bdca549
Commented types in keymanager
ryanuber Apr 12, 2014
af82446
Added r/w mutex to keyManager
ryanuber Apr 12, 2014
311dc97
serf: store a pointer to a keyring manager so the RWLock works
ryanuber Apr 14, 2014
d892201
command: rename 'serf key' to 'serf keys' for uniformity with other c…
ryanuber Apr 14, 2014
8f59d4e
serf: writeKeyringFile is private now, and takes no arguments
ryanuber Apr 15, 2014
ce47deb
serf: implement key broadcast and response handling in reusable funct…
ryanuber Apr 15, 2014
fc45d23
serf: mark query logs as INFO rather than DEBUG
ryanuber Apr 15, 2014
aa54599
website: added RPC documentation for key operations
ryanuber Apr 15, 2014
d5c15b6
command: use newer columnize api
ryanuber Apr 16, 2014
17e8fb1
command: protect against passing ambiguous arguments to keys command
ryanuber Apr 16, 2014
db060d5
serf: return early when all nodes respond before query timeout
ryanuber Apr 16, 2014
d756172
serf: remove unused message types
ryanuber Apr 16, 2014
9b7528c
Added keyring file format documentation
ryanuber Apr 16, 2014
c1205a3
serf: use encoded struct as query payload for key operations
ryanuber Apr 17, 2014
c741506
serf: handle responding to key-related queries using a common function
ryanuber Apr 17, 2014
ad6a008
serf: use a goto inside of the channel loop to handle failure cases
ryanuber Apr 16, 2014
5a006da
Improve documentation and help output for keys command -list option
ryanuber Apr 16, 2014
0d3767b
serf: make RWMutex private in keymanager
ryanuber Apr 17, 2014
ab5d959
Always return allocated response object to avoid nil pointer dereference
ryanuber Apr 18, 2014
85a7528
agent: first pass at rpc tests
ryanuber Apr 18, 2014
f575528
command: added tests for keys command
ryanuber Apr 18, 2014
94ec6dd
serf: added keyring file writer tests
ryanuber Apr 18, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions client/const.go
Expand Up @@ -21,6 +21,10 @@ const (
stopCommand = "stop"
monitorCommand = "monitor"
leaveCommand = "leave"
installKeyCommand = "install-key"
useKeyCommand = "use-key"
removeKeyCommand = "remove-key"
listKeysCommand = "list-keys"
tagsCommand = "tags"
queryCommand = "query"
respondCommand = "respond"
Expand Down Expand Up @@ -95,6 +99,18 @@ type membersResponse struct {
Members []Member
}

type keyRequest struct {
Key string
}

type keyResponse struct {
Messages map[string]string
Keys map[string]int
NumNodes int
NumErr int
NumResp int
}

type monitorRequest struct {
LogLevel string
}
Expand Down
63 changes: 62 additions & 1 deletion client/rpc_client.go
Expand Up @@ -166,7 +166,7 @@ func ClientFromConfig(c *Config) (*RPCClient, error) {
type StreamHandle uint64

func (c *RPCClient) IsClosed() bool {
return c.shutdown;
return c.shutdown
}

// Close is used to free any resources associated with the client
Expand Down Expand Up @@ -292,6 +292,67 @@ func (c *RPCClient) Respond(id uint64, buf []byte) error {
return c.genericRPC(&header, &req, nil)
}

// IntallKey installs a new encryption key onto the keyring
func (c *RPCClient) InstallKey(key string) (map[string]string, error) {
header := requestHeader{
Command: installKeyCommand,
Seq: c.getSeq(),
}
req := keyRequest{
Key: key,
}

resp := keyResponse{}
err := c.genericRPC(&header, &req, &resp)

return resp.Messages, err
}

// UseKey changes the primary encryption key on the keyring
func (c *RPCClient) UseKey(key string) (map[string]string, error) {
header := requestHeader{
Command: useKeyCommand,
Seq: c.getSeq(),
}
req := keyRequest{
Key: key,
}

resp := keyResponse{}
err := c.genericRPC(&header, &req, &resp)

return resp.Messages, err
}

// RemoveKey changes the primary encryption key on the keyring
func (c *RPCClient) RemoveKey(key string) (map[string]string, error) {
header := requestHeader{
Command: removeKeyCommand,
Seq: c.getSeq(),
}
req := keyRequest{
Key: key,
}

resp := keyResponse{}
err := c.genericRPC(&header, &req, &resp)

return resp.Messages, err
}

// ListKeys returns all of the active keys on each member of the cluster
func (c *RPCClient) ListKeys() (map[string]int, int, error) {
header := requestHeader{
Command: listKeysCommand,
Seq: c.getSeq(),
}

resp := keyResponse{}
err := c.genericRPC(&header, nil, &resp)

return resp.Keys, resp.NumNodes, err
}

type monitorHandler struct {
client *RPCClient
closed bool
Expand Down
83 changes: 83 additions & 0 deletions command/agent/agent.go
@@ -1,8 +1,10 @@
package agent

import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/hashicorp/memberlist"
"github.com/hashicorp/serf/serf"
"io"
"io/ioutil"
Expand Down Expand Up @@ -73,6 +75,13 @@ func Create(agentConf *Config, conf *serf.Config, logOutput io.Writer) (*Agent,
}
}

// Load in a keyring file if provided
if agentConf.KeyringFile != "" {
if err := agent.loadKeyringFile(agentConf.KeyringFile); err != nil {
return nil, err
}
}

return agent, nil
}

Expand Down Expand Up @@ -237,6 +246,34 @@ func (a *Agent) eventLoop() {
}
}

// InstallKey initiates a query to install a new key on all members
func (a *Agent) InstallKey(key string) (*serf.KeyResponse, error) {
a.logger.Print("[INFO] agent: Initiating key installation")
manager := a.serf.KeyManager()
return manager.InstallKey(key)
}

// UseKey sends a query instructing all members to switch primary keys
func (a *Agent) UseKey(key string) (*serf.KeyResponse, error) {
a.logger.Print("[INFO] agent: Initiating primary key change")
manager := a.serf.KeyManager()
return manager.UseKey(key)
}

// RemoveKey sends a query to all members to remove a key from the keyring
func (a *Agent) RemoveKey(key string) (*serf.KeyResponse, error) {
a.logger.Print("[INFO] agent: Initiating key removal")
manager := a.serf.KeyManager()
return manager.RemoveKey(key)
}

// ListKeys sends a query to all members to return a list of their keys
func (a *Agent) ListKeys() (*serf.KeyResponse, error) {
a.logger.Print("[INFO] agent: Initiating key listing")
manager := a.serf.KeyManager()
return manager.ListKeys()
}

// 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 {
Expand Down Expand Up @@ -315,3 +352,49 @@ func UnmarshalTags(tags []string) (map[string]string, error) {
}
return result, nil
}

// loadKeyringFile will load a keyring out of a file
func (a *Agent) loadKeyringFile(keyringFile string) error {
// Avoid passing an encryption key and a keyring file at the same time
if len(a.agentConf.EncryptKey) > 0 {
return fmt.Errorf("Encryption key not allowed while using a keyring")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not just set the encrypt key as the primary key in the key ring? I imagine that generally -encrypt is provided with the primary key, and the -keyring-file is used only as a temp store while keys are being swapped

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could definitely do this, but my thinking here was that providing a -encrypt might become brittle if the keys are updated at any point. As an example, if I have a script that starts serf with serf agent -keyring-file /tmp/ring.json -encrypt xxxxxxxxxx, then each time that Serf starts, the primary key will again be set to xxxxxxxxxx, even if a newer key had been set/broadcasted from a member at some other point and saved into the -keyring-file.

The -keyring-file in its current form is intended to work almost exactly like the -tags-file.

}

if _, err := os.Stat(keyringFile); err != nil {
return err
}

// Read in the keyring file data
keyringData, err := ioutil.ReadFile(keyringFile)
if err != nil {
return fmt.Errorf("Failed to read keyring file: %s", err)
}

// Decode keyring JSON
keys := make([]string, 0)
if err := json.Unmarshal(keyringData, &keys); err != nil {
return fmt.Errorf("Failed to decode keyring file: %s", err)
}

// Decode base64 values
keysDecoded := make([][]byte, len(keys))
for i, key := range keys {
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return fmt.Errorf("Failed to decode key from keyring: %s", err)
}
keysDecoded[i] = keyBytes
}

// Create the keyring
keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0])
if err != nil {
return fmt.Errorf("Failed to restore keyring: %s", err)
}
a.conf.MemberlistConfig.Keyring = keyring
a.logger.Printf("[INFO] agent: Restored keyring with %d keys from %s",
len(keys), keyringFile)

// Success!
return nil
}
55 changes: 55 additions & 0 deletions command/agent/agent_test.go
@@ -1,6 +1,7 @@
package agent

import (
"encoding/json"
"github.com/hashicorp/serf/serf"
"github.com/hashicorp/serf/testutil"
"io/ioutil"
Expand Down Expand Up @@ -221,3 +222,57 @@ func TestAgent_UnmarshalTagsError(t *testing.T) {
}
}
}

func TestAgentKeyringFile(t *testing.T) {
keys := []string{
"enjTwAFRe4IE71bOFhirzQ==",
"csT9mxI7aTf9ap3HLBbdmA==",
"noha2tVc0OyD/2LtCBoAOQ==",
}

td, err := ioutil.TempDir("", "serf")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)

keyringFile := filepath.Join(td, "keyring.json")

serfConfig := serf.DefaultConfig()
agentConfig := DefaultConfig()
agentConfig.KeyringFile = keyringFile

encodedKeys, err := json.Marshal(keys)
if err != nil {
t.Fatalf("err: %s", err)
}

if err := ioutil.WriteFile(keyringFile, encodedKeys, 0600); err != nil {
t.Fatalf("err: %s", err)
}

a1 := testAgentWithConfig(agentConfig, serfConfig, nil)

if err := a1.Start(); err != nil {
t.Fatalf("err: %s", err)
}
defer a1.Shutdown()

testutil.Yield()

totalLoadedKeys := len(serfConfig.MemberlistConfig.Keyring.GetKeys())
if totalLoadedKeys != 3 {
t.Fatalf("Expected to load 3 keys but got %d", totalLoadedKeys)
}
}

func TestAgentKeyringFile_BadOptions(t *testing.T) {
agentConfig := DefaultConfig()
agentConfig.KeyringFile = "/some/path"
agentConfig.EncryptKey = "pL4owv4IE1x+ZXCyd5vLLg=="

_, err := Create(agentConfig, serf.DefaultConfig(), nil)
if err == nil || !strings.Contains(err.Error(), "not allowed") {
t.Fatalf("err: %s", err)
}
}
10 changes: 9 additions & 1 deletion command/agent/command.go
Expand Up @@ -48,6 +48,7 @@ func (c *Command) readConfig() *Config {
cmdFlags.Var((*AppendSliceValue)(&configFiles), "config-dir",
"directory of json files to read")
cmdFlags.StringVar(&cmdConfig.EncryptKey, "encrypt", "", "encryption key")
cmdFlags.StringVar(&cmdConfig.KeyringFile, "keyring-file", "", "path to the keyring file")
cmdFlags.Var((*AppendSliceValue)(&cmdConfig.EventHandlers), "event-handler",
"command to execute when events occur")
cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartJoin), "join",
Expand Down Expand Up @@ -249,6 +250,9 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer) *Agent {
serfConfig.TombstoneTimeout = config.TombstoneTimeout
}
serfConfig.EnableNameConflictResolution = !config.DisableNameResolution
if config.KeyringFile != "" {
serfConfig.KeyringFile = config.KeyringFile
}

// Start Serf
c.Ui.Output("Starting Serf agent...")
Expand Down Expand Up @@ -345,7 +349,7 @@ func (c *Command) startAgent(config *Config, agent *Agent,
}

c.Ui.Info(fmt.Sprintf(" RPC addr: '%s'", config.RPCAddr))
c.Ui.Info(fmt.Sprintf(" Encrypted: %#v", config.EncryptKey != ""))
c.Ui.Info(fmt.Sprintf(" Encrypted: %#v", agent.serf.EncryptionEnabled()))
c.Ui.Info(fmt.Sprintf(" Snapshot: %v", config.SnapshotPath != ""))
c.Ui.Info(fmt.Sprintf(" Profile: %s", config.Profile))

Expand Down Expand Up @@ -551,6 +555,10 @@ Options:
peers join each other without an explicit join.
-encrypt=foo Key for encrypting network traffic within Serf.
Must be a base64-encoded 16-byte key.
-keyring-file The keyring file is used to store encryption keys used
by Serf. As encryption keys are changed, the content of
this file is updated so that the same keys may be used
during later agent starts.
-event-handler=foo Script to execute when events occur. This can
be specified multiple times. See the event scripts
section below for more info.
Expand Down
11 changes: 9 additions & 2 deletions command/agent/config.go
Expand Up @@ -73,6 +73,10 @@ type Config struct {
// traffic will not be encrypted.
EncryptKey string `mapstructure:"encrypt_key"`

// KeyringFile is the path to a file containing a serialized keyring.
// The keyring is used to facilitate encryption.
KeyringFile string `mapstructure:"keyring_file"`

// LogLevel is the level of the logs to output.
// This can be updated during a reload.
LogLevel string `mapstructure:"log_level"`
Expand Down Expand Up @@ -216,8 +220,8 @@ func DecodeConfig(r io.Reader) (*Config, error) {
var md mapstructure.Metadata
var result Config
msdec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: &md,
Result: &result,
Metadata: &md,
Result: &result,
ErrorUnused: true,
})
if err != nil {
Expand Down Expand Up @@ -344,6 +348,9 @@ func MergeConfig(a, b *Config) *Config {
if b.TagsFile != "" {
result.TagsFile = b.TagsFile
}
if b.KeyringFile != "" {
result.KeyringFile = b.KeyringFile
}

// Copy the event handlers
result.EventHandlers = make([]string, 0, len(a.EventHandlers)+len(b.EventHandlers))
Expand Down