add primary keys to list keyring (#8522)

During gossip encryption key rotation it would be nice to be able to see if all nodes are using the same key. This PR adds another field to the json response from `GET v1/operator/keyring` which lists the primary keys in use per dc. That way an operator can tell when a key was successfully setup as primary key.

Based on https://github.com/hashicorp/serf/pull/611 to add primary key to list keyring output:

```json
[
  {
    "WAN": true,
    "Datacenter": "dc2",
    "Segment": "",
    "Keys": {
      "0OuM4oC3Os18OblWiBbZUaHA7Hk+tNs/6nhNYtaNduM=": 6,
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 6
    },
    "PrimaryKeys": {
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 6
    },
    "NumNodes": 6
  },
  {
    "WAN": false,
    "Datacenter": "dc2",
    "Segment": "",
    "Keys": {
      "0OuM4oC3Os18OblWiBbZUaHA7Hk+tNs/6nhNYtaNduM=": 8,
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 8
    },
    "PrimaryKeys": {
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 8
    },
    "NumNodes": 8
  },
  {
    "WAN": false,
    "Datacenter": "dc1",
    "Segment": "",
    "Keys": {
      "0OuM4oC3Os18OblWiBbZUaHA7Hk+tNs/6nhNYtaNduM=": 3,
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 8
    },
    "PrimaryKeys": {
      "SINm887hKTzmMWeBNKTJReaTLX3mBEJKriDyt88Ad+g=": 8
    },
    "NumNodes": 8
  }
]
```

I intentionally did not change the CLI output because I didn't find a good way of displaying this information. There are a couple of options that we could implement later:
* add a flag to show the primary keys
* add a flag to show json output

Fixes #3393.
This commit is contained in:
Hans Hasselberg 2020-08-18 09:50:24 +02:00 committed by GitHub
parent 647236bb17
commit 02de4c8b76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 59 additions and 23 deletions

7
.changelog/8522.txt Normal file
View File

@ -0,0 +1,7 @@
```release-note:improvement
serf: update to `v0.9.4` which supports primary keys in the ListKeys operation.
```
```release-note:improvement
api: `GET v1/operator/keyring` also lists primary keys.
```

View File

@ -414,10 +414,11 @@ func (m *Internal) executeKeyringOpWAN(args *structs.KeyringRequest) *structs.Ke
func translateKeyResponseToKeyringResponse(keyresponse *serf.KeyResponse, datacenter string, err error) structs.KeyringResponse { func translateKeyResponseToKeyringResponse(keyresponse *serf.KeyResponse, datacenter string, err error) structs.KeyringResponse {
resp := structs.KeyringResponse{ resp := structs.KeyringResponse{
Datacenter: datacenter, Datacenter: datacenter,
Messages: keyresponse.Messages, Messages: keyresponse.Messages,
Keys: keyresponse.Keys, Keys: keyresponse.Keys,
NumNodes: keyresponse.NumNodes, PrimaryKeys: keyresponse.PrimaryKeys,
NumNodes: keyresponse.NumNodes,
} }
if err != nil { if err != nil {
resp.Error = err.Error() resp.Error = err.Error()

View File

@ -2349,13 +2349,14 @@ func (r *KeyringRequest) RequestDatacenter() string {
// KeyringResponse is a unified key response and can be used for install, // KeyringResponse is a unified key response and can be used for install,
// remove, use, as well as listing key queries. // remove, use, as well as listing key queries.
type KeyringResponse struct { type KeyringResponse struct {
WAN bool WAN bool
Datacenter string Datacenter string
Segment string Segment string
Messages map[string]string `json:",omitempty"` Messages map[string]string `json:",omitempty"`
Keys map[string]int Keys map[string]int
NumNodes int PrimaryKeys map[string]int
Error string `json:",omitempty"` NumNodes int
Error string `json:",omitempty"`
} }
// KeyringResponses holds multiple responses to keyring queries. Each // KeyringResponses holds multiple responses to keyring queries. Each

View File

@ -22,6 +22,9 @@ type KeyringResponse struct {
// A map of the encryption keys to the number of nodes they're installed on // A map of the encryption keys to the number of nodes they're installed on
Keys map[string]int Keys map[string]int
// A map of the encryption primary keys to the number of nodes they're installed on
PrimaryKeys map[string]int
// The total number of nodes in this ring // The total number of nodes in this ring
NumNodes int NumNodes int
} }

2
go.mod
View File

@ -53,7 +53,7 @@ require (
github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69 github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69
github.com/hashicorp/raft v1.1.2 github.com/hashicorp/raft v1.1.2
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea
github.com/hashicorp/serf v0.9.3 github.com/hashicorp/serf v0.9.4
github.com/hashicorp/vault/api v1.0.4 github.com/hashicorp/vault/api v1.0.4
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d
github.com/imdario/mergo v0.3.6 github.com/imdario/mergo v0.3.6

2
go.sum
View File

@ -288,6 +288,8 @@ github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea h1:xykPFhrBA
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk= github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM= github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM=
github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.4 h1:xrZ4ZR0wT5Dz8oQHHdfOzr0ei1jMToWlFFz3hh/DI7I=
github.com/hashicorp/serf v0.9.4/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU= github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8= github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=

View File

@ -64,6 +64,9 @@ type nodeKeyResponse struct {
// Keys is used in listing queries to relay a list of installed keys // Keys is used in listing queries to relay a list of installed keys
Keys []string Keys []string
// PrimaryKey is used in listing queries to relay the primary key
PrimaryKey string
} }
// newSerfQueries is used to create a new serfQueries. We return an event // newSerfQueries is used to create a new serfQueries. We return an event
@ -346,7 +349,7 @@ SEND:
func (s *serfQueries) handleListKeys(q *Query) { func (s *serfQueries) handleListKeys(q *Query) {
response := nodeKeyResponse{Result: false} response := nodeKeyResponse{Result: false}
keyring := s.serf.config.MemberlistConfig.Keyring keyring := s.serf.config.MemberlistConfig.Keyring
var primaryKeyBytes []byte
if !s.serf.EncryptionEnabled() { if !s.serf.EncryptionEnabled() {
response.Message = "Keyring is empty (encryption not enabled)" response.Message = "Keyring is empty (encryption not enabled)"
s.logger.Printf("[ERR] serf: Keyring is empty (encryption not enabled)") s.logger.Printf("[ERR] serf: Keyring is empty (encryption not enabled)")
@ -360,6 +363,9 @@ func (s *serfQueries) handleListKeys(q *Query) {
key := base64.StdEncoding.EncodeToString(keyBytes) key := base64.StdEncoding.EncodeToString(keyBytes)
response.Keys = append(response.Keys, key) response.Keys = append(response.Keys, key)
} }
primaryKeyBytes = keyring.GetPrimaryKey()
response.PrimaryKey = base64.StdEncoding.EncodeToString(primaryKeyBytes)
response.Result = true response.Result = true
SEND: SEND:

View File

@ -31,6 +31,10 @@ type KeyResponse struct {
// Keys is a mapping of the base64-encoded value of the key bytes to the // Keys is a mapping of the base64-encoded value of the key bytes to the
// number of nodes that have the key installed. // number of nodes that have the key installed.
Keys map[string]int Keys map[string]int
// PrimaryKeys is a mapping of the base64-encoded value of the primary
// key bytes to the number of nodes that have the key installed.
PrimaryKeys map[string]int
} }
// KeyRequestOptions is used to contain optional parameters for a keyring operation // KeyRequestOptions is used to contain optional parameters for a keyring operation
@ -76,13 +80,11 @@ func (k *KeyManager) streamKeyResp(resp *KeyResponse, ch <-chan NodeResponse) {
// Currently only used for key list queries, this adds keys to a counter // Currently only used for key list queries, this adds keys to a counter
// and increments them for each node response which contains them. // and increments them for each node response which contains them.
for _, key := range nodeResponse.Keys { for _, key := range nodeResponse.Keys {
if _, ok := resp.Keys[key]; !ok { resp.Keys[key]++
resp.Keys[key] = 1
} else {
resp.Keys[key]++
}
} }
resp.PrimaryKeys[nodeResponse.PrimaryKey]++
NEXT: NEXT:
// Return early if all nodes have responded. This allows us to avoid // Return early if all nodes have responded. This allows us to avoid
// waiting for the full timeout when there is nothing left to do. // waiting for the full timeout when there is nothing left to do.
@ -97,8 +99,9 @@ func (k *KeyManager) streamKeyResp(resp *KeyResponse, ch <-chan NodeResponse) {
// KeyResponse for uniform response handling. // KeyResponse for uniform response handling.
func (k *KeyManager) handleKeyRequest(key, query string, opts *KeyRequestOptions) (*KeyResponse, error) { func (k *KeyManager) handleKeyRequest(key, query string, opts *KeyRequestOptions) (*KeyResponse, error) {
resp := &KeyResponse{ resp := &KeyResponse{
Messages: make(map[string]string), Messages: make(map[string]string),
Keys: make(map[string]int), Keys: make(map[string]int),
PrimaryKeys: make(map[string]int),
} }
qName := internalQueryName(query) qName := internalQueryName(query)

View File

@ -1102,8 +1102,21 @@ func (s *Serf) handleNodeLeaveIntent(leaveMsg *messageLeave) bool {
return false return false
} }
// Always set the lamport time so that if we retransmit below it won't echo // Always update the lamport time even when the status does not change
// around forever! // (despite the variable naming implying otherwise).
//
// By updating this statusLTime here we ensure that the earlier conditional
// on "leaveMsg.LTime <= member.statusLTime" will prevent an infinite
// rebroadcast when seeing two successive leave message for the same
// member. Without this fix a leave message that arrives after a member is
// already marked as leaving/left will cause it to be rebroadcast without
// marking it locally as witnessed. If more than one serf instance in the
// cluster experiences this series of events then they will rebroadcast
// each other's messages about the affected node indefinitely.
//
// This eventually leads to overflowing serf intent queues
// - https://github.com/hashicorp/consul/issues/8179
// - https://github.com/hashicorp/consul/issues/7960
member.statusLTime = leaveMsg.LTime member.statusLTime = leaveMsg.LTime
// State transition depends on current state // State transition depends on current state

2
vendor/modules.txt vendored
View File

@ -271,7 +271,7 @@ github.com/hashicorp/net-rpc-msgpackrpc
github.com/hashicorp/raft github.com/hashicorp/raft
# github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea # github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea
github.com/hashicorp/raft-boltdb github.com/hashicorp/raft-boltdb
# github.com/hashicorp/serf v0.9.3 # github.com/hashicorp/serf v0.9.4
github.com/hashicorp/serf/coordinate github.com/hashicorp/serf/coordinate
github.com/hashicorp/serf/serf github.com/hashicorp/serf/serf
# github.com/hashicorp/vault/api v1.0.4 # github.com/hashicorp/vault/api v1.0.4