Merge pull request #336 from ryanuber/f-keyring

feature: gossip encryption key rotation
This commit is contained in:
Ryan Uber 2014-11-19 23:18:44 -08:00
commit a3cd40cf9d
24 changed files with 1538 additions and 18 deletions

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"net" "net"
"os" "os"
"path/filepath"
"strconv" "strconv"
"sync" "sync"
@ -160,11 +161,6 @@ func (a *Agent) consulConfig() *consul.Config {
if a.config.DataDir != "" { if a.config.DataDir != "" {
base.DataDir = a.config.DataDir base.DataDir = a.config.DataDir
} }
if a.config.EncryptKey != "" {
key, _ := a.config.EncryptBytes()
base.SerfLANConfig.MemberlistConfig.SecretKey = key
base.SerfWANConfig.MemberlistConfig.SecretKey = key
}
if a.config.NodeName != "" { if a.config.NodeName != "" {
base.NodeName = a.config.NodeName base.NodeName = a.config.NodeName
} }
@ -260,7 +256,13 @@ func (a *Agent) consulConfig() *consul.Config {
// setupServer is used to initialize the Consul server // setupServer is used to initialize the Consul server
func (a *Agent) setupServer() error { func (a *Agent) setupServer() error {
server, err := consul.NewServer(a.consulConfig()) config := a.consulConfig()
if err := a.setupKeyrings(config); err != nil {
return fmt.Errorf("Failed to configure keyring: %v", err)
}
server, err := consul.NewServer(config)
if err != nil { if err != nil {
return fmt.Errorf("Failed to start Consul server: %v", err) return fmt.Errorf("Failed to start Consul server: %v", err)
} }
@ -270,7 +272,13 @@ func (a *Agent) setupServer() error {
// setupClient is used to initialize the Consul client // setupClient is used to initialize the Consul client
func (a *Agent) setupClient() error { func (a *Agent) setupClient() error {
client, err := consul.NewClient(a.consulConfig()) config := a.consulConfig()
if err := a.setupKeyrings(config); err != nil {
return fmt.Errorf("Failed to configure keyring: %v", err)
}
client, err := consul.NewClient(config)
if err != nil { if err != nil {
return fmt.Errorf("Failed to start Consul client: %v", err) return fmt.Errorf("Failed to start Consul client: %v", err)
} }
@ -278,6 +286,47 @@ func (a *Agent) setupClient() error {
return nil return nil
} }
// setupKeyrings is used to initialize and load keyrings during agent startup
func (a *Agent) setupKeyrings(config *consul.Config) error {
fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring)
fileWAN := filepath.Join(a.config.DataDir, serfWANKeyring)
if a.config.EncryptKey == "" {
goto LOAD
}
if _, err := os.Stat(fileLAN); err != nil {
if err := initKeyring(fileLAN, a.config.EncryptKey); err != nil {
return err
}
}
if a.config.Server {
if _, err := os.Stat(fileWAN); err != nil {
if err := initKeyring(fileWAN, a.config.EncryptKey); err != nil {
return err
}
}
}
LOAD:
if _, err := os.Stat(fileLAN); err == nil {
config.SerfLANConfig.KeyringFile = fileLAN
}
if err := loadKeyringFile(config.SerfLANConfig); err != nil {
return err
}
if a.config.Server {
if _, err := os.Stat(fileWAN); err == nil {
config.SerfWANConfig.KeyringFile = fileWAN
}
if err := loadKeyringFile(config.SerfWANConfig); err != nil {
return err
}
}
// Success!
return nil
}
// RPC is used to make an RPC call to the Consul servers // RPC is used to make an RPC call to the Consul servers
// This allows the agent to implement the Consul.Interface // This allows the agent to implement the Consul.Interface
func (a *Agent) RPC(method string, args interface{}, reply interface{}) error { func (a *Agent) RPC(method string, args interface{}, reply interface{}) error {

View File

@ -5,6 +5,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@ -71,6 +72,31 @@ func makeAgentLog(t *testing.T, conf *Config, l io.Writer) (string, *Agent) {
return dir, agent return dir, agent
} }
func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) {
dir, err := ioutil.TempDir("", "agent")
if err != nil {
t.Fatalf("err: %v", err)
}
conf.DataDir = dir
fileLAN := filepath.Join(dir, serfLANKeyring)
if err := initKeyring(fileLAN, key); err != nil {
t.Fatalf("err: %s", err)
}
fileWAN := filepath.Join(dir, serfWANKeyring)
if err := initKeyring(fileWAN, key); err != nil {
t.Fatalf("err: %s", err)
}
agent, err := Create(conf, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
return dir, agent
}
func makeAgent(t *testing.T, conf *Config) (string, *Agent) { func makeAgent(t *testing.T, conf *Config) (string, *Agent) {
return makeAgentLog(t, conf, nil) return makeAgentLog(t, conf, nil)
} }

View File

@ -143,17 +143,27 @@ func (c *Command) readConfig() *Config {
config.NodeName = hostname config.NodeName = hostname
} }
// Ensure we have a data directory
if config.DataDir == "" {
c.Ui.Error("Must specify data directory using -data-dir")
return nil
}
if config.EncryptKey != "" { if config.EncryptKey != "" {
if _, err := config.EncryptBytes(); err != nil { if _, err := config.EncryptBytes(); err != nil {
c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err))
return nil return nil
} }
} keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring)
if _, err := os.Stat(keyfileLAN); err == nil {
// Ensure we have a data directory c.Ui.Error("WARNING: LAN keyring exists but -encrypt given, ignoring")
if config.DataDir == "" { }
c.Ui.Error("Must specify data directory using -data-dir") if config.Server {
return nil keyfileWAN := filepath.Join(config.DataDir, serfWANKeyring)
if _, err := os.Stat(keyfileWAN); err == nil {
c.Ui.Error("WARNING: WAN keyring exists but -encrypt given, ignoring")
}
}
} }
// Verify data center is valid // Verify data center is valid
@ -459,6 +469,22 @@ func (c *Command) retryJoinWan(config *Config, errCh chan<- struct{}) {
} }
} }
// gossipEncrypted determines if the consul instance is using symmetric
// encryption keys to protect gossip protocol messages.
func (c *Command) gossipEncrypted() bool {
if c.agent.config.EncryptKey != "" {
return true
}
server := c.agent.server
if server != nil {
return server.KeyManagerLAN() != nil || server.KeyManagerWAN() != nil
}
client := c.agent.client
return client != nil && client.KeyManagerLAN() != nil
}
func (c *Command) Run(args []string) int { func (c *Command) Run(args []string) int {
c.Ui = &cli.PrefixedUi{ c.Ui = &cli.PrefixedUi{
OutputPrefix: "==> ", OutputPrefix: "==> ",
@ -585,6 +611,14 @@ func (c *Command) Run(args []string) int {
}(wp) }(wp)
} }
// Figure out if gossip is encrypted
var gossipEncrypted bool
if config.Server {
gossipEncrypted = c.agent.server.Encrypted()
} else {
gossipEncrypted = c.agent.client.Encrypted()
}
// Let the agent know we've finished registration // Let the agent know we've finished registration
c.agent.StartSync() c.agent.StartSync()
@ -597,7 +631,7 @@ func (c *Command) Run(args []string) int {
c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr, c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr,
config.Ports.SerfLan, config.Ports.SerfWan)) config.Ports.SerfLan, config.Ports.SerfWan))
c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v", c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v",
config.EncryptKey != "", config.VerifyOutgoing, config.VerifyIncoming)) gossipEncrypted, config.VerifyOutgoing, config.VerifyIncoming))
// Enable log streaming // Enable log streaming
c.Ui.Info("") c.Ui.Info("")

145
command/agent/keyring.go Normal file
View File

@ -0,0 +1,145 @@
package agent
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/memberlist"
"github.com/hashicorp/serf/serf"
)
const (
serfLANKeyring = "serf/local.keyring"
serfWANKeyring = "serf/remote.keyring"
)
// initKeyring will create a keyring file at a given path.
func initKeyring(path, key string) error {
var keys []string
if _, err := base64.StdEncoding.DecodeString(key); err != nil {
return fmt.Errorf("Invalid key: %s", err)
}
// Just exit if the file already exists.
if _, err := os.Stat(path); err == nil {
return nil
}
keys = append(keys, key)
keyringBytes, err := json.Marshal(keys)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer fh.Close()
if _, err := fh.Write(keyringBytes); err != nil {
os.Remove(path)
return err
}
return nil
}
// loadKeyringFile will load a gossip encryption keyring out of a file. The file
// must be in JSON format and contain a list of encryption key strings.
func loadKeyringFile(c *serf.Config) error {
if c.KeyringFile == "" {
return nil
}
if _, err := os.Stat(c.KeyringFile); err != nil {
return err
}
// Read in the keyring file data
keyringData, err := ioutil.ReadFile(c.KeyringFile)
if err != nil {
return err
}
// Decode keyring JSON
keys := make([]string, 0)
if err := json.Unmarshal(keyringData, &keys); err != nil {
return err
}
// Decode base64 values
keysDecoded := make([][]byte, len(keys))
for i, key := range keys {
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return err
}
keysDecoded[i] = keyBytes
}
// Guard against empty keyring
if len(keysDecoded) == 0 {
return fmt.Errorf("no keys present in keyring file: %s", c.KeyringFile)
}
// Create the keyring
keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0])
if err != nil {
return err
}
c.MemberlistConfig.Keyring = keyring
// Success!
return nil
}
// keyringProcess is used to abstract away the semantic similarities in
// performing various operations on the encryption keyring.
func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringResponses, error) {
var reply structs.KeyringResponses
if a.server == nil {
return nil, fmt.Errorf("keyring operations must run against a server node")
}
if err := a.RPC("Internal.KeyringOperation", args, &reply); err != nil {
return &reply, err
}
return &reply, nil
}
// ListKeys lists out all keys installed on the collective Consul cluster. This
// includes both servers and clients in all DC's.
func (a *Agent) ListKeys() (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Operation: structs.KeyringList}
return a.keyringProcess(&args)
}
// InstallKey installs a new gossip encryption key
func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringInstall}
return a.keyringProcess(&args)
}
// UseKey changes the primary encryption key used to encrypt messages
func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringUse}
return a.keyringProcess(&args)
}
// RemoveKey will remove a gossip encryption key from the keyring
func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringRemove}
return a.keyringProcess(&args)
}

View File

@ -0,0 +1,115 @@
package agent
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestAgent_LoadKeyrings(t *testing.T) {
key := "tbLJg26ZJyJ9pK3qhc9jig=="
// Should be no configured keyring file by default
conf1 := nextConfig()
dir1, agent1 := makeAgent(t, conf1)
defer os.RemoveAll(dir1)
defer agent1.Shutdown()
c := agent1.config.ConsulConfig
if c.SerfLANConfig.KeyringFile != "" {
t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile)
}
if c.SerfLANConfig.MemberlistConfig.Keyring != nil {
t.Fatalf("keyring should not be loaded")
}
if c.SerfWANConfig.KeyringFile != "" {
t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile)
}
if c.SerfWANConfig.MemberlistConfig.Keyring != nil {
t.Fatalf("keyring should not be loaded")
}
// Server should auto-load LAN and WAN keyring files
conf2 := nextConfig()
dir2, agent2 := makeAgentKeyring(t, conf2, key)
defer os.RemoveAll(dir2)
defer agent2.Shutdown()
c = agent2.config.ConsulConfig
if c.SerfLANConfig.KeyringFile == "" {
t.Fatalf("should have keyring file")
}
if c.SerfLANConfig.MemberlistConfig.Keyring == nil {
t.Fatalf("keyring should be loaded")
}
if c.SerfWANConfig.KeyringFile == "" {
t.Fatalf("should have keyring file")
}
if c.SerfWANConfig.MemberlistConfig.Keyring == nil {
t.Fatalf("keyring should be loaded")
}
// Client should auto-load only the LAN keyring file
conf3 := nextConfig()
conf3.Server = false
dir3, agent3 := makeAgentKeyring(t, conf3, key)
defer os.RemoveAll(dir3)
defer agent3.Shutdown()
c = agent3.config.ConsulConfig
if c.SerfLANConfig.KeyringFile == "" {
t.Fatalf("should have keyring file")
}
if c.SerfLANConfig.MemberlistConfig.Keyring == nil {
t.Fatalf("keyring should be loaded")
}
if c.SerfWANConfig.KeyringFile != "" {
t.Fatalf("bad: %#v", c.SerfWANConfig.KeyringFile)
}
if c.SerfWANConfig.MemberlistConfig.Keyring != nil {
t.Fatalf("keyring should not be loaded")
}
}
func TestAgent_InitKeyring(t *testing.T) {
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
key2 := "4leC33rgtXKIVUr9Nr0snQ=="
expected := fmt.Sprintf(`["%s"]`, key1)
dir, err := ioutil.TempDir("", "consul")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir)
file := filepath.Join(dir, "keyring")
// First initialize the keyring
if err := initKeyring(file, key1); err != nil {
t.Fatalf("err: %s", err)
}
content, err := ioutil.ReadFile(file)
if err != nil {
t.Fatalf("err: %s", err)
}
if string(content) != expected {
t.Fatalf("bad: %s", content)
}
// Try initializing again with a different key
if err := initKeyring(file, key2); err != nil {
t.Fatalf("err: %s", err)
}
// Content should still be the same
content, err = ioutil.ReadFile(file)
if err != nil {
t.Fatalf("err: %s", err)
}
if string(content) != expected {
t.Fatalf("bad: %s", content)
}
}

View File

@ -24,15 +24,17 @@ package agent
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/logutils"
"github.com/hashicorp/serf/serf"
"io" "io"
"log" "log"
"net" "net"
"os" "os"
"strings" "strings"
"sync" "sync"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/logutils"
"github.com/hashicorp/serf/serf"
) )
const ( const (
@ -51,6 +53,10 @@ const (
leaveCommand = "leave" leaveCommand = "leave"
statsCommand = "stats" statsCommand = "stats"
reloadCommand = "reload" reloadCommand = "reload"
installKeyCommand = "install-key"
useKeyCommand = "use-key"
removeKeyCommand = "remove-key"
listKeysCommand = "list-keys"
) )
const ( const (
@ -103,6 +109,37 @@ type joinResponse struct {
Num int32 Num int32
} }
type keyringRequest struct {
Key string
}
type KeyringEntry struct {
Datacenter string
Pool string
Key string
Count int
}
type KeyringMessage struct {
Datacenter string
Pool string
Node string
Message string
}
type KeyringInfo struct {
Datacenter string
Pool string
NumNodes int
Error string
}
type keyringResponse struct {
Keys []KeyringEntry
Messages []KeyringMessage
Info []KeyringInfo
}
type membersResponse struct { type membersResponse struct {
Members []Member Members []Member
} }
@ -373,6 +410,9 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er
case reloadCommand: case reloadCommand:
return i.handleReload(client, seq) return i.handleReload(client, seq)
case installKeyCommand, useKeyCommand, removeKeyCommand, listKeysCommand:
return i.handleKeyring(client, seq, command)
default: default:
respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} respHeader := responseHeader{Seq: seq, Error: unsupportedCommand}
client.Send(&respHeader, nil) client.Send(&respHeader, nil)
@ -583,6 +623,80 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error {
return client.Send(&resp, nil) return client.Send(&resp, nil)
} }
func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error {
var req keyringRequest
var queryResp *structs.KeyringResponses
var r keyringResponse
var err error
if cmd != listKeysCommand {
if err = client.dec.Decode(&req); err != nil {
return fmt.Errorf("decode failed: %v", err)
}
}
switch cmd {
case listKeysCommand:
queryResp, err = i.agent.ListKeys()
case installKeyCommand:
queryResp, err = i.agent.InstallKey(req.Key)
case useKeyCommand:
queryResp, err = i.agent.UseKey(req.Key)
case removeKeyCommand:
queryResp, err = i.agent.RemoveKey(req.Key)
default:
respHeader := responseHeader{Seq: seq, Error: unsupportedCommand}
client.Send(&respHeader, nil)
return fmt.Errorf("command '%s' not recognized", cmd)
}
header := responseHeader{
Seq: seq,
Error: errToString(err),
}
if queryResp == nil {
goto SEND
}
for _, kr := range queryResp.Responses {
var pool string
if kr.WAN {
pool = "WAN"
} else {
pool = "LAN"
}
for node, message := range kr.Messages {
msg := KeyringMessage{
Datacenter: kr.Datacenter,
Pool: pool,
Node: node,
Message: message,
}
r.Messages = append(r.Messages, msg)
}
for key, qty := range kr.Keys {
k := KeyringEntry{
Datacenter: kr.Datacenter,
Pool: pool,
Key: key,
Count: qty,
}
r.Keys = append(r.Keys, k)
}
info := KeyringInfo{
Datacenter: kr.Datacenter,
Pool: pool,
NumNodes: kr.NumNodes,
Error: kr.Error,
}
r.Info = append(r.Info, info)
}
SEND:
return client.Send(&header, r)
}
// Used to convert an error to a string representation // Used to convert an error to a string representation
func errToString(err error) string { func errToString(err error) string {
if err == nil { if err == nil {

View File

@ -176,6 +176,49 @@ func (c *RPCClient) WANMembers() ([]Member, error) {
return resp.Members, err return resp.Members, err
} }
func (c *RPCClient) ListKeys() (keyringResponse, error) {
header := requestHeader{
Command: listKeysCommand,
Seq: c.getSeq(),
}
var resp keyringResponse
err := c.genericRPC(&header, nil, &resp)
return resp, err
}
func (c *RPCClient) InstallKey(key string) (keyringResponse, error) {
header := requestHeader{
Command: installKeyCommand,
Seq: c.getSeq(),
}
req := keyringRequest{key}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err
}
func (c *RPCClient) UseKey(key string) (keyringResponse, error) {
header := requestHeader{
Command: useKeyCommand,
Seq: c.getSeq(),
}
req := keyringRequest{key}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err
}
func (c *RPCClient) RemoveKey(key string) (keyringResponse, error) {
header := requestHeader{
Command: removeKeyCommand,
Seq: c.getSeq(),
}
req := keyringRequest{key}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err
}
// Leave is used to trigger a graceful leave and shutdown // Leave is used to trigger a graceful leave and shutdown
func (c *RPCClient) Leave() error { func (c *RPCClient) Leave() error {
header := requestHeader{ header := requestHeader{

View File

@ -30,6 +30,10 @@ func (r *rpcParts) Close() {
// testRPCClient returns an RPCClient connected to an RPC server that // testRPCClient returns an RPCClient connected to an RPC server that
// serves only this connection. // serves only this connection.
func testRPCClient(t *testing.T) *rpcParts { func testRPCClient(t *testing.T) *rpcParts {
return testRPCClientWithConfig(t, func(c *Config) {})
}
func testRPCClientWithConfig(t *testing.T, cb func(c *Config)) *rpcParts {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -39,6 +43,8 @@ func testRPCClient(t *testing.T) *rpcParts {
mult := io.MultiWriter(os.Stderr, lw) mult := io.MultiWriter(os.Stderr, lw)
conf := nextConfig() conf := nextConfig()
cb(conf)
dir, agent := makeAgentLog(t, conf, mult) dir, agent := makeAgentLog(t, conf, mult)
rpc := NewAgentRPC(agent, l, mult, lw) rpc := NewAgentRPC(agent, l, mult, lw)
@ -273,3 +279,159 @@ OUTER2:
t.Fatalf("should log joining") t.Fatalf("should log joining")
} }
} }
func TestRPCClientListKeys(t *testing.T) {
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
c.Datacenter = "dc1"
})
defer p1.Close()
// Key is initially installed to both wan/lan
keys := listKeys(t, p1.client)
if _, ok := keys["dc1"][key1]; !ok {
t.Fatalf("bad: %#v", keys)
}
if _, ok := keys["WAN"][key1]; !ok {
t.Fatalf("bad: %#v", keys)
}
}
func TestRPCClientInstallKey(t *testing.T) {
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
key2 := "xAEZ3uVHRMZD9GcYMZaRQw=="
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
})
defer p1.Close()
// key2 is not installed yet
testutil.WaitForResult(func() (bool, error) {
keys := listKeys(t, p1.client)
if num, ok := keys["dc1"][key2]; ok || num != 0 {
return false, fmt.Errorf("bad: %#v", keys)
}
if num, ok := keys["WAN"][key2]; ok || num != 0 {
return false, fmt.Errorf("bad: %#v", keys)
}
return true, nil
}, func(err error) {
t.Fatal(err.Error())
})
// install key2
r, err := p1.client.InstallKey(key2)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
// key2 should now be installed
testutil.WaitForResult(func() (bool, error) {
keys := listKeys(t, p1.client)
if num, ok := keys["dc1"][key2]; !ok || num != 1 {
return false, fmt.Errorf("bad: %#v", keys)
}
if num, ok := keys["WAN"][key2]; !ok || num != 1 {
return false, fmt.Errorf("bad: %#v", keys)
}
return true, nil
}, func(err error) {
t.Fatal(err.Error())
})
}
func TestRPCClientUseKey(t *testing.T) {
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
key2 := "xAEZ3uVHRMZD9GcYMZaRQw=="
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
})
defer p1.Close()
// add a second key to the ring
r, err := p1.client.InstallKey(key2)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
// key2 is installed
testutil.WaitForResult(func() (bool, error) {
keys := listKeys(t, p1.client)
if num, ok := keys["dc1"][key2]; !ok || num != 1 {
return false, fmt.Errorf("bad: %#v", keys)
}
if num, ok := keys["WAN"][key2]; !ok || num != 1 {
return false, fmt.Errorf("bad: %#v", keys)
}
return true, nil
}, func(err error) {
t.Fatal(err.Error())
})
// can't remove key1 yet
r, err = p1.client.RemoveKey(key1)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringError(t, r)
// change primary key
r, err = p1.client.UseKey(key2)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
// can remove key1 now
r, err = p1.client.RemoveKey(key1)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
}
func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) {
p1 := testRPCClient(t)
defer p1.Close()
r, err := p1.client.ListKeys()
if err != nil {
t.Fatalf("err: %s", err)
}
keyringError(t, r)
}
func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int {
resp, err := c.ListKeys()
if err != nil {
t.Fatalf("err: %s", err)
}
out := make(map[string]map[string]int)
for _, k := range resp.Keys {
respID := k.Datacenter
if k.Pool == "WAN" {
respID = k.Pool
}
out[respID] = map[string]int{k.Key: k.Count}
}
return out
}
func keyringError(t *testing.T, r keyringResponse) {
for _, i := range r.Info {
if i.Error == "" {
t.Fatalf("no error reported from %s (%s)", i.Datacenter, i.Pool)
}
}
}
func keyringSuccess(t *testing.T, r keyringResponse) {
for _, i := range r.Info {
if i.Error != "" {
t.Fatalf("error from %s (%s): %s", i.Datacenter, i.Pool, i.Error)
}
}
}

216
command/keyring.go Normal file
View File

@ -0,0 +1,216 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/command/agent"
"github.com/mitchellh/cli"
)
// KeyringCommand is a Command implementation that handles querying, installing,
// and removing gossip encryption keys from a keyring.
type KeyringCommand struct {
Ui cli.Ui
}
func (c *KeyringCommand) Run(args []string) int {
var installKey, useKey, removeKey string
var listKeys bool
cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.StringVar(&installKey, "install", "", "install key")
cmdFlags.StringVar(&useKey, "use", "", "use key")
cmdFlags.StringVar(&removeKey, "remove", "", "remove key")
cmdFlags.BoolVar(&listKeys, "list", false, "list keys")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
c.Ui = &cli.PrefixedUi{
OutputPrefix: "",
InfoPrefix: "==> ",
ErrorPrefix: "",
Ui: c.Ui,
}
// Only accept a single argument
found := listKeys
for _, arg := range []string{installKey, useKey, removeKey} {
if found && len(arg) > 0 {
c.Ui.Error("Only a single action is allowed")
return 1
}
found = found || len(arg) > 0
}
// Fail fast if no actionable args were passed
if !found {
c.Ui.Error(c.Help())
return 1
}
// All other operations will require a client connection
client, err := RPCClient(*rpcAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
if listKeys {
c.Ui.Info("Gathering installed encryption keys...")
r, err := client.ListKeys()
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
if rval := c.handleResponse(r.Info, r.Messages); rval != 0 {
return rval
}
c.handleList(r.Info, r.Keys)
return 0
}
if installKey != "" {
c.Ui.Info("Installing new gossip encryption key...")
r, err := client.InstallKey(installKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
}
if useKey != "" {
c.Ui.Info("Changing primary gossip encryption key...")
r, err := client.UseKey(useKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
}
if removeKey != "" {
c.Ui.Info("Removing gossip encryption key...")
r, err := client.RemoveKey(removeKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
}
// Should never make it here
return 0
}
func (c *KeyringCommand) handleResponse(
info []agent.KeyringInfo,
messages []agent.KeyringMessage) int {
var rval int
for _, i := range info {
if i.Error != "" {
pool := i.Pool
if pool != "WAN" {
pool = i.Datacenter + " (LAN)"
}
c.Ui.Error("")
c.Ui.Error(fmt.Sprintf("%s error: %s", pool, i.Error))
for _, msg := range messages {
if msg.Datacenter != i.Datacenter || msg.Pool != i.Pool {
continue
}
c.Ui.Error(fmt.Sprintf(" %s: %s", msg.Node, msg.Message))
}
rval = 1
}
}
if rval == 0 {
c.Ui.Info("Done!")
}
return rval
}
func (c *KeyringCommand) handleList(
info []agent.KeyringInfo,
keys []agent.KeyringEntry) {
installed := make(map[string]map[string][]int)
for _, key := range keys {
var nodes int
for _, i := range info {
if i.Datacenter == key.Datacenter && i.Pool == key.Pool {
nodes = i.NumNodes
}
}
pool := key.Pool
if pool != "WAN" {
pool = key.Datacenter + " (LAN)"
}
if _, ok := installed[pool]; !ok {
installed[pool] = map[string][]int{key.Key: []int{key.Count, nodes}}
} else {
installed[pool][key.Key] = []int{key.Count, nodes}
}
}
for pool, keys := range installed {
c.Ui.Output("")
c.Ui.Output(pool + ":")
for key, num := range keys {
c.Ui.Output(fmt.Sprintf(" %s [%d/%d]", key, num[0], num[1]))
}
}
}
func (c *KeyringCommand) Help() string {
helpText := `
Usage: consul keyring [options]
Manages encryption keys used for gossip messages. Gossip encryption is
optional. When enabled, this command may be used to examine active encryption
keys in the cluster, add new keys, and remove old ones. When combined, this
functionality provides the ability to perform key rotation cluster-wide,
without disrupting the cluster.
All operations performed by this command can only be run against server nodes,
and affect both the LAN and WAN keyrings in lock-step.
All variations of the keyring command return 0 if all nodes reply and there
are no errors. If any node fails to reply or reports failure, the exit code
will be 1.
Options:
-install=<key> Install a new encryption key. This will broadcast
the new key to all members in the cluster.
-use=<key> Change the primary encryption key, which is used to
encrypt messages. The key must already be installed
before this operation can succeed.
-remove=<key> Remove the given key from the cluster. This
operation may only be performed on keys which are
not currently the primary key.
-list List all keys currently in use within the cluster.
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}
func (c *KeyringCommand) Synopsis() string {
return "Manages gossip layer encryption keys"
}

136
command/keyring_test.go Normal file
View File

@ -0,0 +1,136 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/consul/command/agent"
"github.com/mitchellh/cli"
)
func TestKeyringCommand_implements(t *testing.T) {
var _ cli.Command = &KeyringCommand{}
}
func TestKeyringCommandRun(t *testing.T) {
key1 := "HS5lJ+XuTlYKWaeGYyG+/A=="
key2 := "kZyFABeAmc64UMTrm9XuKA=="
// Begin with a single key
a1 := testAgentWithConfig(t, func(c *agent.Config) {
c.EncryptKey = key1
})
defer a1.Shutdown()
// The LAN and WAN keyrings were initialized with key1
out := listKeys(t, a1.addr)
if !strings.Contains(out, "dc1 (LAN):\n "+key1) {
t.Fatalf("bad: %#v", out)
}
if !strings.Contains(out, "WAN:\n "+key1) {
t.Fatalf("bad: %#v", out)
}
if strings.Contains(out, key2) {
t.Fatalf("bad: %#v", out)
}
// Install the second key onto the keyring
installKey(t, a1.addr, key2)
// Both keys should be present
out = listKeys(t, a1.addr)
for _, key := range []string{key1, key2} {
if !strings.Contains(out, key) {
t.Fatalf("bad: %#v", out)
}
}
// Rotate to key2, remove key1
useKey(t, a1.addr, key2)
removeKey(t, a1.addr, key1)
// Only key2 is present now
out = listKeys(t, a1.addr)
if !strings.Contains(out, "dc1 (LAN):\n "+key2) {
t.Fatalf("bad: %#v", out)
}
if !strings.Contains(out, "WAN:\n "+key2) {
t.Fatalf("bad: %#v", out)
}
if strings.Contains(out, key1) {
t.Fatalf("bad: %#v", out)
}
}
func TestKeyringCommandRun_help(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
code := c.Run(nil)
if code != 1 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
// Test that we didn't actually try to dial the RPC server.
if !strings.Contains(ui.ErrorWriter.String(), "Usage:") {
t.Fatalf("bad: %#v", ui.ErrorWriter.String())
}
}
func TestKeyringCommandRun_failedConnection(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-list", "-rpc-addr=127.0.0.1:0"}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d, %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.ErrorWriter.String(), "dial") {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func listKeys(t *testing.T, addr string) string {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-list", "-rpc-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
return ui.OutputWriter.String()
}
func installKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-install=" + key, "-rpc-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}
func useKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-use=" + key, "-rpc-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}
func removeKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-remove=" + key, "-rpc-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}

View File

@ -39,6 +39,10 @@ func (a *agentWrapper) Shutdown() {
} }
func testAgent(t *testing.T) *agentWrapper { func testAgent(t *testing.T) *agentWrapper {
return testAgentWithConfig(t, func(c *agent.Config) {})
}
func testAgentWithConfig(t *testing.T, cb func(c *agent.Config)) *agentWrapper {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -48,6 +52,7 @@ func testAgent(t *testing.T) *agentWrapper {
mult := io.MultiWriter(os.Stderr, lw) mult := io.MultiWriter(os.Stderr, lw)
conf := nextConfig() conf := nextConfig()
cb(conf)
dir, err := ioutil.TempDir("", "agent") dir, err := ioutil.TempDir("", "agent")
if err != nil { if err != nil {

View File

@ -56,6 +56,12 @@ func init() {
}, nil }, nil
}, },
"keyring": func() (cli.Command, error) {
return &command.KeyringCommand{
Ui: ui,
}, nil
},
"leave": func() (cli.Command, error) { "leave": func() (cli.Command, error) {
return &command.LeaveCommand{ return &command.LeaveCommand{
Ui: ui, Ui: ui,

View File

@ -206,6 +206,16 @@ func (c *Client) UserEvent(name string, payload []byte) error {
return c.serf.UserEvent(userEventName(name), payload, false) return c.serf.UserEvent(userEventName(name), payload, false)
} }
// KeyManagerLAN returns the LAN Serf keyring manager
func (c *Client) KeyManagerLAN() *serf.KeyManager {
return c.serf.KeyManager()
}
// Encrypted determines if gossip is encrypted
func (c *Client) Encrypted() bool {
return c.serf.EncryptionEnabled()
}
// lanEventHandler is used to handle events from the lan Serf cluster // lanEventHandler is used to handle events from the lan Serf cluster
func (c *Client) lanEventHandler() { func (c *Client) lanEventHandler() {
for { for {

View File

@ -269,3 +269,23 @@ func TestClientServer_UserEvent(t *testing.T) {
t.Fatalf("missing events") t.Fatalf("missing events")
} }
} }
func TestClient_Encrypted(t *testing.T) {
dir1, c1 := testClient(t)
defer os.RemoveAll(dir1)
defer c1.Shutdown()
key := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
dir2, c2 := testClientWithConfig(t, func(c *Config) {
c.SerfLANConfig.MemberlistConfig.SecretKey = key
})
defer os.RemoveAll(dir2)
defer c2.Shutdown()
if c1.Encrypted() {
t.Fatalf("should not be encrypted")
}
if !c2.Encrypted() {
t.Fatalf("should be encrypted")
}
}

View File

@ -2,6 +2,7 @@ package consul
import ( import (
"github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/serf/serf"
) )
// Internal endpoint is used to query the miscellaneous info that // Internal endpoint is used to query the miscellaneous info that
@ -62,3 +63,64 @@ func (m *Internal) EventFire(args *structs.EventFireRequest,
// Fire the event // Fire the event
return m.srv.UserEvent(args.Name, args.Payload) return m.srv.UserEvent(args.Name, args.Payload)
} }
// KeyringOperation will query the WAN and LAN gossip keyrings of all nodes.
func (m *Internal) KeyringOperation(
args *structs.KeyringRequest,
reply *structs.KeyringResponses) error {
// Only perform WAN keyring querying and RPC forwarding once
if !args.Forwarded {
args.Forwarded = true
m.executeKeyringOp(args, reply, true)
return m.srv.globalRPC("Internal.KeyringOperation", args, reply)
}
// Query the LAN keyring of this node's DC
m.executeKeyringOp(args, reply, false)
return nil
}
// executeKeyringOp executes the appropriate keyring-related function based on
// the type of keyring operation in the request. It takes the KeyManager as an
// argument, so it can handle any operation for either LAN or WAN pools.
func (m *Internal) executeKeyringOp(
args *structs.KeyringRequest,
reply *structs.KeyringResponses,
wan bool) {
var serfResp *serf.KeyResponse
var err error
var mgr *serf.KeyManager
if wan {
mgr = m.srv.KeyManagerWAN()
} else {
mgr = m.srv.KeyManagerLAN()
}
switch args.Operation {
case structs.KeyringList:
serfResp, err = mgr.ListKeys()
case structs.KeyringInstall:
serfResp, err = mgr.InstallKey(args.Key)
case structs.KeyringUse:
serfResp, err = mgr.UseKey(args.Key)
case structs.KeyringRemove:
serfResp, err = mgr.RemoveKey(args.Key)
}
errStr := ""
if err != nil {
errStr = err.Error()
}
reply.Responses = append(reply.Responses, &structs.KeyringResponse{
WAN: wan,
Datacenter: m.srv.config.Datacenter,
Messages: serfResp.Messages,
Keys: serfResp.Keys,
NumNodes: serfResp.NumNodes,
Error: errStr,
})
}

View File

@ -1,6 +1,8 @@
package consul package consul
import ( import (
"encoding/base64"
"fmt"
"github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil" "github.com/hashicorp/consul/testutil"
"os" "os"
@ -150,3 +152,89 @@ func TestInternal_NodeDump(t *testing.T) {
t.Fatalf("missing foo or bar") t.Fatalf("missing foo or bar")
} }
} }
func TestInternal_KeyringOperation(t *testing.T) {
key1 := "H1dfkSZOVnP/JUnaBfTzXg=="
keyBytes1, err := base64.StdEncoding.DecodeString(key1)
if err != nil {
t.Fatalf("err: %s", err)
}
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.SerfLANConfig.MemberlistConfig.SecretKey = keyBytes1
c.SerfWANConfig.MemberlistConfig.SecretKey = keyBytes1
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
var out structs.KeyringResponses
req := structs.KeyringRequest{
Operation: structs.KeyringList,
Datacenter: "dc1",
}
if err := client.Call("Internal.KeyringOperation", &req, &out); err != nil {
t.Fatalf("err: %v", err)
}
// Two responses (local lan/wan pools) from single-node cluster
if len(out.Responses) != 2 {
t.Fatalf("bad: %#v", out)
}
if _, ok := out.Responses[0].Keys[key1]; !ok {
t.Fatalf("bad: %#v", out)
}
wanResp, lanResp := 0, 0
for _, resp := range out.Responses {
if resp.WAN {
wanResp++
} else {
lanResp++
}
}
if lanResp != 1 || wanResp != 1 {
t.Fatalf("should have one lan and one wan response")
}
// Start a second agent to test cross-dc queries
dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.SerfLANConfig.MemberlistConfig.SecretKey = keyBytes1
c.SerfWANConfig.MemberlistConfig.SecretKey = keyBytes1
c.Datacenter = "dc2"
})
defer os.RemoveAll(dir2)
defer s2.Shutdown()
// Try to join
addr := fmt.Sprintf("127.0.0.1:%d",
s1.config.SerfWANConfig.MemberlistConfig.BindPort)
if _, err := s2.JoinWAN([]string{addr}); err != nil {
t.Fatalf("err: %v", err)
}
var out2 structs.KeyringResponses
req2 := structs.KeyringRequest{
Operation: structs.KeyringList,
}
if err := client.Call("Internal.KeyringOperation", &req2, &out2); err != nil {
t.Fatalf("err: %v", err)
}
// 3 responses (one from each DC LAN, one from WAN) in two-node cluster
if len(out2.Responses) != 3 {
t.Fatalf("bad: %#v", out)
}
wanResp, lanResp = 0, 0
for _, resp := range out2.Responses {
if resp.WAN {
wanResp++
} else {
lanResp++
}
}
if lanResp != 2 || wanResp != 1 {
t.Fatalf("should have two lan and one wan response")
}
}

View File

@ -223,6 +223,40 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{
return s.connPool.RPC(server.Addr, server.Version, method, args, reply) return s.connPool.RPC(server.Addr, server.Version, method, args, reply)
} }
// globalRPC is used to forward an RPC request to one server in each datacenter.
// This will only error for RPC-related errors. Otherwise, application-level
// errors can be sent in the response objects.
func (s *Server) globalRPC(method string, args interface{},
reply structs.CompoundResponse) error {
errorCh := make(chan error)
respCh := make(chan interface{})
// Make a new request into each datacenter
for dc, _ := range s.remoteConsuls {
go func(dc string) {
rr := reply.New()
if err := s.forwardDC(method, dc, args, &rr); err != nil {
errorCh <- err
return
}
respCh <- rr
}(dc)
}
replies, total := 0, len(s.remoteConsuls)
for replies < total {
select {
case err := <-errorCh:
return err
case rr := <-respCh:
reply.Add(rr)
replies++
}
}
return nil
}
// raftApply is used to encode a message, run it through raft, and return // raftApply is used to encode a message, run it through raft, and return
// the FSM response along with any errors // the FSM response along with any errors
func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, error) { func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, error) {

View File

@ -551,6 +551,21 @@ func (s *Server) IsLeader() bool {
return s.raft.State() == raft.Leader return s.raft.State() == raft.Leader
} }
// KeyManagerLAN returns the LAN Serf keyring manager
func (s *Server) KeyManagerLAN() *serf.KeyManager {
return s.serfLAN.KeyManager()
}
// KeyManagerWAN returns the WAN Serf keyring manager
func (s *Server) KeyManagerWAN() *serf.KeyManager {
return s.serfWAN.KeyManager()
}
// Encrypted determines if gossip is encrypted
func (s *Server) Encrypted() bool {
return s.serfLAN.EncryptionEnabled() && s.serfWAN.EncryptionEnabled()
}
// inmemCodec is used to do an RPC call without going over a network // inmemCodec is used to do an RPC call without going over a network
type inmemCodec struct { type inmemCodec struct {
method string method string

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
@ -471,5 +472,50 @@ func TestServer_BadExpect(t *testing.T) {
}, func(err error) { }, func(err error) {
t.Fatalf("should have 0 peers: %v", err) t.Fatalf("should have 0 peers: %v", err)
}) })
}
type fakeGlobalResp struct{}
func (r *fakeGlobalResp) Add(interface{}) {
return
}
func (r *fakeGlobalResp) New() interface{} {
return struct{}{}
}
func TestServer_globalRPCErrors(t *testing.T) {
dir1, s1 := testServerDC(t, "dc1")
defer os.RemoveAll(dir1)
defer s1.Shutdown()
// Check that an error from a remote DC is returned
err := s1.globalRPC("Bad.Method", nil, &fakeGlobalResp{})
if err == nil {
t.Fatalf("should have errored")
}
if !strings.Contains(err.Error(), "Bad.Method") {
t.Fatalf("unexpcted error: %s", err)
}
}
func TestServer_Encrypted(t *testing.T) {
dir1, s1 := testServer(t)
defer os.RemoveAll(dir1)
defer s1.Shutdown()
key := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.SerfLANConfig.MemberlistConfig.SecretKey = key
c.SerfWANConfig.MemberlistConfig.SecretKey = key
})
defer os.RemoveAll(dir2)
defer s2.Shutdown()
if s1.Encrypted() {
t.Fatalf("should not be encrypted")
}
if !s2.Encrypted() {
t.Fatalf("should be encrypted")
}
} }

View File

@ -531,3 +531,66 @@ func Encode(t MessageType, msg interface{}) ([]byte, error) {
err := codec.NewEncoder(&buf, msgpackHandle).Encode(msg) err := codec.NewEncoder(&buf, msgpackHandle).Encode(msg)
return buf.Bytes(), err return buf.Bytes(), err
} }
// CompoundResponse is an interface for gathering multiple responses. It is
// used in cross-datacenter RPC calls where more than 1 datacenter is
// expected to reply.
type CompoundResponse interface {
// Add adds a new response to the compound response
Add(interface{})
// New returns an empty response object which can be passed around by
// reference, and then passed to Add() later on.
New() interface{}
}
type KeyringOp string
const (
KeyringList KeyringOp = "list"
KeyringInstall = "install"
KeyringUse = "use"
KeyringRemove = "remove"
)
// KeyringRequest encapsulates a request to modify an encryption keyring.
// It can be used for install, remove, or use key type operations.
type KeyringRequest struct {
Operation KeyringOp
Key string
Datacenter string
Forwarded bool
QueryOptions
}
func (r *KeyringRequest) RequestDatacenter() string {
return r.Datacenter
}
// KeyringResponse is a unified key response and can be used for install,
// remove, use, as well as listing key queries.
type KeyringResponse struct {
WAN bool
Datacenter string
Messages map[string]string
Keys map[string]int
NumNodes int
Error string
}
// KeyringResponses holds multiple responses to keyring queries. Each
// datacenter replies independently, and KeyringResponses is used as a
// container for the set of all responses.
type KeyringResponses struct {
Responses []*KeyringResponse
QueryMeta
}
func (r *KeyringResponses) Add(v interface{}) {
val := v.(*KeyringResponses)
r.Responses = append(r.Responses, val.Responses...)
}
func (r *KeyringResponses) New() interface{} {
return new(KeyringResponses)
}

View File

@ -32,3 +32,23 @@ func TestEncodeDecode(t *testing.T) {
t.Fatalf("bad: %#v %#v", arg, out) t.Fatalf("bad: %#v %#v", arg, out)
} }
} }
func TestStructs_Implements(t *testing.T) {
var (
_ RPCInfo = &RegisterRequest{}
_ RPCInfo = &DeregisterRequest{}
_ RPCInfo = &DCSpecificRequest{}
_ RPCInfo = &ServiceSpecificRequest{}
_ RPCInfo = &NodeSpecificRequest{}
_ RPCInfo = &ChecksInStateRequest{}
_ RPCInfo = &KVSRequest{}
_ RPCInfo = &KeyRequest{}
_ RPCInfo = &KeyListRequest{}
_ RPCInfo = &SessionRequest{}
_ RPCInfo = &SessionSpecificRequest{}
_ RPCInfo = &EventFireRequest{}
_ RPCInfo = &ACLPolicyRequest{}
_ RPCInfo = &KeyringRequest{}
_ CompoundResponse = &KeyringResponses{}
)
}

View File

@ -89,6 +89,12 @@ The options below are all specified on the command-line.
network traffic. This key must be 16-bytes that are base64 encoded. The network traffic. This key must be 16-bytes that are base64 encoded. The
easiest way to create an encryption key is to use `consul keygen`. All easiest way to create an encryption key is to use `consul keygen`. All
nodes within a cluster must share the same encryption key to communicate. nodes within a cluster must share the same encryption key to communicate.
The provided key is automatically persisted to the data directory, and loaded
automatically whenever the agent is restarted. This means that to encrypt
Consul's gossip protocol, this option only needs to be provided once on each
agent's initial startup sequence. If it is provided after Consul has been
initialized with an encryption key, then the provided key is ignored and
a warning will be displayed.
* `-join` - Address of another agent to join upon starting up. This can be * `-join` - Address of another agent to join upon starting up. This can be
specified multiple times to specify multiple agents to join. If Consul is specified multiple times to specify multiple agents to join. If Consul is

View File

@ -0,0 +1,101 @@
---
layout: "docs"
page_title: "Commands: Keyring"
sidebar_current: "docs-commands-keyring"
---
# Consul Keyring
Command: `consul keyring`
The `keyring` command is used to examine and modify the encryption keys used in
Consul's [Gossip Pools](/docs/internals/gossip.html). It is capable of
distributing new encryption keys to the cluster, retiring old encryption keys,
and changing the keys used by the cluster to encrypt messages.
Consul allows multiple encryption keys to be in use simultaneously. This is
intended to provide a transition state while the cluster converges. It is the
responsibility of the operator to ensure that only the required encryption keys
are installed on the cluster. You can review the installed keys using the
`-list` argument, and remove unneeded keys with `-remove`.
All operations performed by this command can only be run against server nodes,
and affect both the LAN and WAN keyrings in lock-step.
All variations of the `keyring` command return 0 if all nodes reply and there
are no errors. If any node fails to reply or reports failure, the exit code
will be 1.
## Usage
Usage: `consul keyring [options]`
Only one actionable argument may be specified per run, including `-list`,
`-install`, `-remove`, and `-use`.
The list of available flags are:
* `-list` - List all keys currently in use within the cluster.
* `-install` - Install a new encryption key. This will broadcast the new key to
all members in the cluster.
* `-use` - Change the primary encryption key, which is used to encrypt messages.
The key must already be installed before this operation can succeed.
* `-remove` - Remove the given key from the cluster. This operation may only be
performed on keys which are not currently the primary key.
* `-rpc-addr` - RPC address of the Consul agent.
## Output
The output of the `consul keyring -list` command consolidates information from
all nodes and all datacenters to provide a simple and easy to understand view of
the cluster. The following is some example output from a cluster with two
datacenters, each which consist of one server and one client:
```
==> Gathering installed encryption keys...
==> Done!
WAN:
a1i101sMY8rxB+0eAKD/gw== [2/2]
dc2 (LAN):
a1i101sMY8rxB+0eAKD/gw== [2/2]
dc1 (LAN):
a1i101sMY8rxB+0eAKD/gw== [2/2]
```
As you can see, the output above is divided first by gossip pool, and then by
encryption key. The indicator to the right of each key displays the number of
nodes the key is installed on over the total number of nodes in the pool.
## Errors
If any errors are encountered while performing a keyring operation, no key
information is displayed, but instead only error information. The error
information is arranged in a similar fashion, organized first by datacenter,
followed by a simple list of nodes which had errors, and the actual text of the
error. Below is sample output from the same cluster as above, if we try to do
something that causes an error; in this case, trying to remove the primary key:
```
==> Removing gossip encryption key...
dc1 (LAN) error: 2/2 nodes reported failure
server1: Removing the primary key is not allowed
client1: Removing the primary key is not allowed
WAN error: 2/2 nodes reported failure
server1.dc1: Removing the primary key is not allowed
server2.dc2: Removing the primary key is not allowed
dc2 (LAN) error: 2/2 nodes reported failure
server2: Removing the primary key is not allowed
client2: Removing the primary key is not allowed
```
As you can see, each node with a failure reported what went wrong.

View File

@ -79,6 +79,10 @@
<a href="/docs/commands/keygen.html">keygen</a> <a href="/docs/commands/keygen.html">keygen</a>
</li> </li>
<li<%= sidebar_current("docs-commands-keyring") %>>
<a href="/docs/commands/keyring.html">keyring</a>
</li>
<li<%= sidebar_current("docs-commands-leave") %>> <li<%= sidebar_current("docs-commands-leave") %>>
<a href="/docs/commands/leave.html">leave</a> <a href="/docs/commands/leave.html">leave</a>
</li> </li>