mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 17:35:43 +03:00
Enable serf encryption (#1791)
* Added the keygen command * Added support for gossip encryption * Changed the URL for keyring management * Fixed the cli * Added some tests * Added tests for keyring operations * Added a test for removal of keys * Added some docs * Fixed some docs * Added general options
This commit is contained in:
committed by
GitHub
parent
b9ff39d1c2
commit
f0806dceff
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -371,6 +372,11 @@ func (a *Agent) setupServer() error {
|
||||
return fmt.Errorf("server config setup failed: %s", err)
|
||||
}
|
||||
|
||||
// Sets up the keyring for gossip encryption
|
||||
if err := a.setupKeyrings(conf); err != nil {
|
||||
return fmt.Errorf("failed to configure keyring: %v", err)
|
||||
}
|
||||
|
||||
// Create the server
|
||||
server, err := nomad.NewServer(conf, a.consulSyncer, a.logger)
|
||||
if err != nil {
|
||||
@@ -431,6 +437,30 @@ func (a *Agent) setupServer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupKeyrings is used to initialize and load keyrings during agent startup
|
||||
func (a *Agent) setupKeyrings(config *nomad.Config) error {
|
||||
file := filepath.Join(a.config.DataDir, serfKeyring)
|
||||
|
||||
if a.config.Server.EncryptKey == "" {
|
||||
goto LOAD
|
||||
}
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
if err := initKeyring(file, a.config.Server.EncryptKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
LOAD:
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
config.SerfConfig.KeyringFile = file
|
||||
}
|
||||
if err := loadKeyringFile(config.SerfConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
// Success!
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupClient is used to setup the client if enabled
|
||||
func (a *Agent) setupClient() error {
|
||||
if !a.config.Client.Enabled {
|
||||
|
||||
@@ -3,7 +3,9 @@ package agent
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
@@ -165,6 +167,56 @@ func (s *HTTPServer) updateServers(resp http.ResponseWriter, req *http.Request)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// KeyringOperationRequest allows an operator to install/delete/use keys
|
||||
func (s *HTTPServer) KeyringOperationRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
srv := s.agent.Server()
|
||||
if srv == nil {
|
||||
return nil, CodedError(501, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
kmgr := srv.KeyManager()
|
||||
var sresp *serf.KeyResponse
|
||||
var err error
|
||||
|
||||
// Get the key from the req body
|
||||
var args structs.KeyringRequest
|
||||
|
||||
//Get the op
|
||||
op := strings.TrimPrefix(req.URL.Path, "/v1/agent/keyring/")
|
||||
|
||||
switch op {
|
||||
case "list":
|
||||
sresp, err = kmgr.ListKeys()
|
||||
case "install":
|
||||
if err := decodeBody(req, &args); err != nil {
|
||||
return nil, CodedError(500, err.Error())
|
||||
}
|
||||
sresp, err = kmgr.InstallKey(args.Key)
|
||||
case "use":
|
||||
if err := decodeBody(req, &args); err != nil {
|
||||
return nil, CodedError(500, err.Error())
|
||||
}
|
||||
sresp, err = kmgr.UseKey(args.Key)
|
||||
case "remove":
|
||||
if err := decodeBody(req, &args); err != nil {
|
||||
return nil, CodedError(500, err.Error())
|
||||
}
|
||||
sresp, err = kmgr.RemoveKey(args.Key)
|
||||
default:
|
||||
return nil, CodedError(404, "resource not found")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kresp := structs.KeyringResponse{
|
||||
Messages: sresp.Messages,
|
||||
Keys: sresp.Keys,
|
||||
NumNodes: sresp.NumNodes,
|
||||
}
|
||||
return kresp, nil
|
||||
}
|
||||
|
||||
type agentSelf struct {
|
||||
Config *Config `json:"config"`
|
||||
Member Member `json:"member,omitempty"`
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
func TestHTTP_AgentSelf(t *testing.T) {
|
||||
@@ -177,3 +181,111 @@ func TestHTTP_AgentSetServers(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_AgentListKeys(t *testing.T) {
|
||||
key1 := "HS5lJ+XuTlYKWaeGYyG+/A=="
|
||||
|
||||
httpTest(t, func(c *Config) {
|
||||
c.Server.EncryptKey = key1
|
||||
}, func(s *TestServer) {
|
||||
req, err := http.NewRequest("GET", "/v1/agent/keyring/list", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
out, err := s.Server.KeyringOperationRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
kresp := out.(structs.KeyringResponse)
|
||||
if len(kresp.Keys) != 1 {
|
||||
t.Fatalf("bad: %v", kresp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_AgentInstallKey(t *testing.T) {
|
||||
key1 := "HS5lJ+XuTlYKWaeGYyG+/A=="
|
||||
key2 := "wH1Bn9hlJ0emgWB1JttVRA=="
|
||||
|
||||
httpTest(t, func(c *Config) {
|
||||
c.Server.EncryptKey = key1
|
||||
}, func(s *TestServer) {
|
||||
b, err := json.Marshal(&structs.KeyringRequest{Key: key2})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("GET", "/v1/agent/keyring/install", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
_, err = s.Server.KeyringOperationRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
req, err = http.NewRequest("GET", "/v1/agent/keyring/list", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
out, err := s.Server.KeyringOperationRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
kresp := out.(structs.KeyringResponse)
|
||||
if len(kresp.Keys) != 2 {
|
||||
t.Fatalf("bad: %v", kresp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_AgentRemoveKey(t *testing.T) {
|
||||
key1 := "HS5lJ+XuTlYKWaeGYyG+/A=="
|
||||
key2 := "wH1Bn9hlJ0emgWB1JttVRA=="
|
||||
|
||||
httpTest(t, func(c *Config) {
|
||||
c.Server.EncryptKey = key1
|
||||
}, func(s *TestServer) {
|
||||
b, err := json.Marshal(&structs.KeyringRequest{Key: key2})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/v1/agent/keyring/install", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW := httptest.NewRecorder()
|
||||
_, err = s.Server.KeyringOperationRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "/v1/agent/keyring/remove", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW = httptest.NewRecorder()
|
||||
if _, err = s.Server.KeyringOperationRequest(respW, req); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "/v1/agent/keyring/list", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
respW = httptest.NewRecorder()
|
||||
out, err := s.Server.KeyringOperationRequest(respW, req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
kresp := out.(structs.KeyringResponse)
|
||||
if len(kresp.Keys) != 1 {
|
||||
t.Fatalf("bad: %v", kresp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ func (c *Command) readConfig() *Config {
|
||||
flags.Var((*flaghelper.StringFlag)(&cmdConfig.Server.RetryJoin), "retry-join", "")
|
||||
flags.IntVar(&cmdConfig.Server.RetryMaxAttempts, "retry-max", 0, "")
|
||||
flags.StringVar(&cmdConfig.Server.RetryInterval, "retry-interval", "", "")
|
||||
flags.StringVar(&cmdConfig.Server.EncryptKey, "encrypt", "", "gossip encryption key")
|
||||
|
||||
// Client-only options
|
||||
flags.StringVar(&cmdConfig.Client.StateDir, "state-dir", "", "")
|
||||
@@ -195,6 +196,17 @@ func (c *Command) readConfig() *Config {
|
||||
return config
|
||||
}
|
||||
|
||||
if config.Server.EncryptKey != "" {
|
||||
if _, err := config.Server.EncryptBytes(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err))
|
||||
return nil
|
||||
}
|
||||
keyfile := filepath.Join(config.DataDir, serfKeyring)
|
||||
if _, err := os.Stat(keyfile); err == nil {
|
||||
c.Ui.Error("WARNING: keyring exists but -encrypt given, using keyring")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the RetryInterval.
|
||||
dur, err := time.ParseDuration(config.Server.RetryInterval)
|
||||
if err != nil {
|
||||
@@ -818,6 +830,9 @@ Server Options:
|
||||
bootstrapping the cluster. Once <num> servers have joined eachother,
|
||||
Nomad initiates the bootstrap process.
|
||||
|
||||
-encrypt=<key>
|
||||
Provides the gossip encryption key
|
||||
|
||||
-join=<address>
|
||||
Address of an agent to join at start time. Can be specified
|
||||
multiple times.
|
||||
|
||||
@@ -68,6 +68,7 @@ server {
|
||||
retry_max = 3
|
||||
retry_interval = "15s"
|
||||
rejoin_after_leave = true
|
||||
encrypt = "abc"
|
||||
}
|
||||
telemetry {
|
||||
statsite_address = "127.0.0.1:1234"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -244,6 +245,14 @@ type ServerConfig struct {
|
||||
// the cluster until an explicit join is received. If this is set to
|
||||
// true, we ignore the leave, and rejoin the cluster on start.
|
||||
RejoinAfterLeave bool `mapstructure:"rejoin_after_leave"`
|
||||
|
||||
// Encryption key to use for the Serf communication
|
||||
EncryptKey string `mapstructure:"encrypt" json:"-"`
|
||||
}
|
||||
|
||||
// EncryptBytes returns the encryption key configured.
|
||||
func (s *ServerConfig) EncryptBytes() ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s.EncryptKey)
|
||||
}
|
||||
|
||||
// Telemetry is the telemetry configuration for the server
|
||||
@@ -669,6 +678,9 @@ func (a *ServerConfig) Merge(b *ServerConfig) *ServerConfig {
|
||||
if b.RejoinAfterLeave {
|
||||
result.RejoinAfterLeave = true
|
||||
}
|
||||
if b.EncryptKey != "" {
|
||||
result.EncryptKey = b.EncryptKey
|
||||
}
|
||||
|
||||
// Add the schedulers
|
||||
result.EnabledSchedulers = append(result.EnabledSchedulers, b.EnabledSchedulers...)
|
||||
|
||||
@@ -484,6 +484,7 @@ func parseServer(result **ServerConfig, list *ast.ObjectList) error {
|
||||
"retry_max",
|
||||
"retry_interval",
|
||||
"rejoin_after_leave",
|
||||
"encrypt",
|
||||
}
|
||||
if err := checkHCLKeys(listVal, valid); err != nil {
|
||||
return err
|
||||
|
||||
@@ -85,6 +85,7 @@ func TestConfig_Parse(t *testing.T) {
|
||||
RetryInterval: "15s",
|
||||
RejoinAfterLeave: true,
|
||||
RetryMaxAttempts: 3,
|
||||
EncryptKey: "abc",
|
||||
},
|
||||
Telemetry: &Telemetry{
|
||||
StatsiteAddr: "127.0.0.1:1234",
|
||||
|
||||
@@ -122,6 +122,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
||||
s.mux.HandleFunc("/v1/agent/members", s.wrap(s.AgentMembersRequest))
|
||||
s.mux.HandleFunc("/v1/agent/force-leave", s.wrap(s.AgentForceLeaveRequest))
|
||||
s.mux.HandleFunc("/v1/agent/servers", s.wrap(s.AgentServersRequest))
|
||||
s.mux.HandleFunc("/v1/agent/keyring/", s.wrap(s.KeyringOperationRequest))
|
||||
|
||||
s.mux.HandleFunc("/v1/regions", s.wrap(s.RegionListRequest))
|
||||
|
||||
|
||||
106
command/agent/keyring.go
Normal file
106
command/agent/keyring.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/memberlist"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
const (
|
||||
serfKeyring = "server/serf.keyring"
|
||||
)
|
||||
|
||||
// initKeyring will create a keyring file at a given path.
|
||||
func initKeyring(path, key string) error {
|
||||
var keys []string
|
||||
|
||||
if keyBytes, err := base64.StdEncoding.DecodeString(key); err != nil {
|
||||
return fmt.Errorf("Invalid key: %s", err)
|
||||
} else if err := memberlist.ValidateKey(keyBytes); 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
|
||||
}
|
||||
85
command/agent/keyring_test.go
Normal file
85
command/agent/keyring_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
dir1, agent1 := makeAgent(t, nil)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer agent1.Shutdown()
|
||||
|
||||
c := agent1.server.GetConfig()
|
||||
if c.SerfConfig.KeyringFile != "" {
|
||||
t.Fatalf("bad: %#v", c.SerfConfig.KeyringFile)
|
||||
}
|
||||
if c.SerfConfig.MemberlistConfig.Keyring != nil {
|
||||
t.Fatalf("keyring should not be loaded")
|
||||
}
|
||||
|
||||
// Server should auto-load LAN and WAN keyring files
|
||||
dir2, agent2 := makeAgent(t, func(c *Config) {
|
||||
file := filepath.Join(c.DataDir, serfKeyring)
|
||||
if err := initKeyring(file, key); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer agent2.Shutdown()
|
||||
|
||||
c = agent2.server.GetConfig()
|
||||
if c.SerfConfig.KeyringFile == "" {
|
||||
t.Fatalf("should have keyring file")
|
||||
}
|
||||
if c.SerfConfig.MemberlistConfig.Keyring == nil {
|
||||
t.Fatalf("keyring should be loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_InitKeyring(t *testing.T) {
|
||||
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
|
||||
key2 := "4leC33rgtXKIVUr9Nr0snQ=="
|
||||
expected := fmt.Sprintf(`["%s"]`, key1)
|
||||
|
||||
dir, err := ioutil.TempDir("", "nomad")
|
||||
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)
|
||||
}
|
||||
}
|
||||
45
command/keygen.go
Normal file
45
command/keygen.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KeygenCommand is a Command implementation that generates an encryption
|
||||
// key for use in `nomad agent`.
|
||||
type KeygenCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *KeygenCommand) Run(_ []string) int {
|
||||
key := make([]byte, 16)
|
||||
n, err := rand.Reader.Read(key)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error reading random data: %s", err))
|
||||
return 1
|
||||
}
|
||||
if n != 16 {
|
||||
c.Ui.Error(fmt.Sprintf("Couldn't read enough entropy. Generate more entropy!"))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(base64.StdEncoding.EncodeToString(key))
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *KeygenCommand) Synopsis() string {
|
||||
return "Generates a new encryption key"
|
||||
}
|
||||
|
||||
func (c *KeygenCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad keygen
|
||||
|
||||
Generates a new encryption key that can be used to configure the
|
||||
agent to encrypt traffic. The output of this command is already
|
||||
in the proper format that the agent expects.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
27
command/keygen_test.go
Normal file
27
command/keygen_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestKeygenCommand(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
c := &KeygenCommand{Meta: Meta{Ui: ui}}
|
||||
code := c.Run(nil)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d", code)
|
||||
}
|
||||
|
||||
output := ui.OutputWriter.String()
|
||||
result, err := base64.StdEncoding.DecodeString(output)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(result) != 16 {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
}
|
||||
157
command/keyring.go
Normal file
157
command/keyring.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// KeyringCommand is a Command implementation that handles querying, installing,
|
||||
// and removing gossip encryption keys from a keyring.
|
||||
type KeyringCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *KeyringCommand) Run(args []string) int {
|
||||
var installKey, useKey, removeKey, token string
|
||||
var listKeys bool
|
||||
|
||||
flags := c.Meta.FlagSet("keys", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
|
||||
flags.StringVar(&installKey, "install", "", "install key")
|
||||
flags.StringVar(&useKey, "use", "", "use key")
|
||||
flags.StringVar(&removeKey, "remove", "", "remove key")
|
||||
flags.BoolVar(&listKeys, "list", false, "list keys")
|
||||
flags.StringVar(&token, "token", "", "acl token")
|
||||
|
||||
if err := flags.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 := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error creating nomad cli client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if listKeys {
|
||||
c.Ui.Info("Gathering installed encryption keys...")
|
||||
r, err := client.Agent().ListKeys()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("error: %s", err))
|
||||
return 1
|
||||
}
|
||||
c.handleKeyResponse(r)
|
||||
return 0
|
||||
}
|
||||
|
||||
if installKey != "" {
|
||||
c.Ui.Info("Installing new gossip encryption key...")
|
||||
_, err := client.Agent().InstallKey(installKey)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("error: %s", err))
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if useKey != "" {
|
||||
c.Ui.Info("Changing primary gossip encryption key...")
|
||||
_, err := client.Agent().UseKey(useKey)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("error: %s", err))
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if removeKey != "" {
|
||||
c.Ui.Info("Removing gossip encryption key...")
|
||||
_, err := client.Agent().RemoveKey(removeKey)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("error: %s", err))
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Should never make it here
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *KeyringCommand) handleKeyResponse(resp *api.KeyringResponse) {
|
||||
out := make([]string, len(resp.Keys)+1)
|
||||
out[0] = "Key"
|
||||
i := 1
|
||||
for k := range resp.Keys {
|
||||
out[i] = fmt.Sprintf("%s", k)
|
||||
i = i + 1
|
||||
}
|
||||
c.Ui.Output(formatList(out))
|
||||
}
|
||||
|
||||
func (c *KeyringCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad keyring [options]
|
||||
|
||||
Manages encryption keys used for gossip messages between Nomad servers. 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.
|
||||
|
||||
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.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage() + `
|
||||
|
||||
Keyring Options:
|
||||
|
||||
-install=<key> Install a new encryption key. This will broadcast
|
||||
the new key to all members in the cluster.
|
||||
-list List all keys currently in use within the cluster.
|
||||
-remove=<key> Remove the given key from the cluster. This
|
||||
operation may only be performed on keys which are
|
||||
not currently the primary key.
|
||||
-use=<key> Change the primary encryption key, which is used to
|
||||
encrypt messages. The key must already be installed
|
||||
before this operation can succeed.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *KeyringCommand) Synopsis() string {
|
||||
return "Manages gossip layer encryption keys"
|
||||
}
|
||||
Reference in New Issue
Block a user