4005759d28
Move conflict resolution implementation into the state store with a new Apply RPC. This also makes the RPC for secure variables much more similar to Consul's KV, which will help us support soft deletes in a post-1.4.0 version of Nomad. Reimplement quotas in the state store functions. Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
601 lines
16 KiB
Go
601 lines
16 KiB
Go
package structs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
// note: this is aliased so that it's more noticeable if someone
|
|
// accidentally swaps it out for math/rand via running goimports
|
|
cryptorand "crypto/rand"
|
|
|
|
"github.com/hashicorp/nomad/helper"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
)
|
|
|
|
const (
|
|
// SecureVariablesApplyRPCMethod is the RPC method for upserting or
|
|
// deleting a secure variable by its namespace and path, with optional
|
|
// conflict detection.
|
|
//
|
|
// Args: SecureVariablesApplyRequest
|
|
// Reply: SecureVariablesApplyResponse
|
|
SecureVariablesApplyRPCMethod = "SecureVariables.Apply"
|
|
|
|
// SecureVariablesListRPCMethod is the RPC method for listing secure
|
|
// variables within Nomad.
|
|
//
|
|
// Args: SecureVariablesListRequest
|
|
// Reply: SecureVariablesListResponse
|
|
SecureVariablesListRPCMethod = "SecureVariables.List"
|
|
|
|
// SecureVariablesGetServiceRPCMethod is the RPC method for fetching a
|
|
// secure variable according to its namepace and path.
|
|
//
|
|
// Args: SecureVariablesByNameRequest
|
|
// Reply: SecureVariablesByNameResponse
|
|
SecureVariablesReadRPCMethod = "SecureVariables.Read"
|
|
|
|
// maxVariableSize is the maximum size of the unencrypted contents of
|
|
// a variable. This size is deliberately set low and is not
|
|
// configurable, to discourage DoS'ing the cluster
|
|
maxVariableSize = 16384
|
|
)
|
|
|
|
// SecureVariableMetadata is the metadata envelope for a Secure Variable, it
|
|
// is the list object and is shared data between an SecureVariableEncrypted and
|
|
// a SecureVariableDecrypted object.
|
|
type SecureVariableMetadata struct {
|
|
Namespace string
|
|
Path string
|
|
CreateIndex uint64
|
|
CreateTime int64
|
|
ModifyIndex uint64
|
|
ModifyTime int64
|
|
}
|
|
|
|
// SecureVariableEncrypted structs are returned from the Encrypter's encrypt
|
|
// method. They are the only form that should ever be persisted to storage.
|
|
type SecureVariableEncrypted struct {
|
|
SecureVariableMetadata
|
|
SecureVariableData
|
|
}
|
|
|
|
// SecureVariableData is the secret data for a Secure Variable
|
|
type SecureVariableData struct {
|
|
Data []byte // includes nonce
|
|
KeyID string // ID of root key used to encrypt this entry
|
|
}
|
|
|
|
// SecureVariableDecrypted structs are returned from the Encrypter's decrypt
|
|
// method. Since they contains sensitive material, they should never be
|
|
// persisted to disk.
|
|
type SecureVariableDecrypted struct {
|
|
SecureVariableMetadata
|
|
Items SecureVariableItems
|
|
}
|
|
|
|
// SecureVariableItems are the actual secrets stored in a secure variable. They
|
|
// are always encrypted and decrypted as a single unit.
|
|
type SecureVariableItems map[string]string
|
|
|
|
func (svi SecureVariableItems) Size() uint64 {
|
|
var out uint64
|
|
for k, v := range svi {
|
|
out += uint64(len(k))
|
|
out += uint64(len(v))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Equals checks both the metadata and items in a SecureVariableDecrypted
|
|
// struct
|
|
func (v1 SecureVariableDecrypted) Equals(v2 SecureVariableDecrypted) bool {
|
|
return v1.SecureVariableMetadata.Equals(v2.SecureVariableMetadata) &&
|
|
v1.Items.Equals(v2.Items)
|
|
}
|
|
|
|
// Equals is a convenience method to provide similar equality checking
|
|
// syntax for metadata and the SecureVariablesData or SecureVariableItems
|
|
// struct
|
|
func (sv SecureVariableMetadata) Equals(sv2 SecureVariableMetadata) bool {
|
|
return sv == sv2
|
|
}
|
|
|
|
// Equals performs deep equality checking on the cleartext items
|
|
// of a SecureVariableDecrypted. Uses reflect.DeepEqual
|
|
func (i1 SecureVariableItems) Equals(i2 SecureVariableItems) bool {
|
|
return reflect.DeepEqual(i1, i2)
|
|
}
|
|
|
|
// Equals checks both the metadata and encrypted data for a
|
|
// SecureVariableEncrypted struct
|
|
func (v1 SecureVariableEncrypted) Equals(v2 SecureVariableEncrypted) bool {
|
|
return v1.SecureVariableMetadata.Equals(v2.SecureVariableMetadata) &&
|
|
v1.SecureVariableData.Equals(v2.SecureVariableData)
|
|
}
|
|
|
|
// Equals performs deep equality checking on the encrypted data part
|
|
// of a SecureVariableEncrypted
|
|
func (d1 SecureVariableData) Equals(d2 SecureVariableData) bool {
|
|
return d1.KeyID == d2.KeyID &&
|
|
bytes.Equal(d1.Data, d2.Data)
|
|
}
|
|
|
|
func (sv SecureVariableDecrypted) Copy() SecureVariableDecrypted {
|
|
return SecureVariableDecrypted{
|
|
SecureVariableMetadata: sv.SecureVariableMetadata,
|
|
Items: sv.Items.Copy(),
|
|
}
|
|
}
|
|
|
|
func (sv SecureVariableItems) Copy() SecureVariableItems {
|
|
out := make(SecureVariableItems, len(sv))
|
|
for k, v := range sv {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (sv SecureVariableEncrypted) Copy() SecureVariableEncrypted {
|
|
return SecureVariableEncrypted{
|
|
SecureVariableMetadata: sv.SecureVariableMetadata,
|
|
SecureVariableData: sv.SecureVariableData.Copy(),
|
|
}
|
|
}
|
|
|
|
func (sv SecureVariableData) Copy() SecureVariableData {
|
|
out := make([]byte, len(sv.Data))
|
|
copy(out, sv.Data)
|
|
return SecureVariableData{
|
|
Data: out,
|
|
KeyID: sv.KeyID,
|
|
}
|
|
}
|
|
|
|
func (sv SecureVariableDecrypted) Validate() error {
|
|
|
|
if len(sv.Path) == 0 {
|
|
return fmt.Errorf("variable requires path")
|
|
}
|
|
parts := strings.Split(sv.Path, "/")
|
|
switch {
|
|
case len(parts) == 1 && parts[0] == "nomad":
|
|
return fmt.Errorf("\"nomad\" is a reserved top-level directory path, but you may write variables to \"nomad/jobs\" or below")
|
|
case len(parts) >= 2 && parts[0] == "nomad" && parts[1] != "jobs":
|
|
return fmt.Errorf("only paths at \"nomad/jobs\" or below are valid paths under the top-level \"nomad\" directory")
|
|
}
|
|
|
|
if len(sv.Items) == 0 {
|
|
return errors.New("empty variables are invalid")
|
|
}
|
|
if sv.Items.Size() > maxVariableSize {
|
|
return errors.New("variables are limited to 16KiB in total size")
|
|
}
|
|
if sv.Namespace == AllNamespacesSentinel {
|
|
return errors.New("can not target wildcard (\"*\")namespace")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sv *SecureVariableDecrypted) Canonicalize() {
|
|
if sv.Namespace == "" {
|
|
sv.Namespace = DefaultNamespace
|
|
}
|
|
}
|
|
|
|
// GetNamespace returns the secure variable's namespace. Used for pagination.
|
|
func (sv *SecureVariableMetadata) Copy() *SecureVariableMetadata {
|
|
var out SecureVariableMetadata = *sv
|
|
return &out
|
|
}
|
|
|
|
// GetNamespace returns the secure variable's namespace. Used for pagination.
|
|
func (sv SecureVariableMetadata) GetNamespace() string {
|
|
return sv.Namespace
|
|
}
|
|
|
|
// GetID returns the secure variable's path. Used for pagination.
|
|
func (sv SecureVariableMetadata) GetID() string {
|
|
return sv.Path
|
|
}
|
|
|
|
// GetCreateIndex returns the secure variable's create index. Used for pagination.
|
|
func (sv SecureVariableMetadata) GetCreateIndex() uint64 {
|
|
return sv.CreateIndex
|
|
}
|
|
|
|
// SecureVariablesQuota is used to track the total size of secure variables
|
|
// entries per namespace. The total length of SecureVariable.EncryptedData in
|
|
// bytes will be added to the SecureVariablesQuota table in the same transaction
|
|
// as a write, update, or delete. This tracking effectively caps the maximum
|
|
// size of secure variables in a given namespace to MaxInt64 bytes.
|
|
type SecureVariablesQuota struct {
|
|
Namespace string
|
|
Size int64
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
func (svq *SecureVariablesQuota) Copy() *SecureVariablesQuota {
|
|
if svq == nil {
|
|
return nil
|
|
}
|
|
nq := new(SecureVariablesQuota)
|
|
*nq = *svq
|
|
return nq
|
|
}
|
|
|
|
// ---------------------------------------
|
|
// RPC and FSM request/response objects
|
|
|
|
// SVOp constants give possible operations available in a transaction.
|
|
type SVOp string
|
|
|
|
const (
|
|
SVOpSet SVOp = "set"
|
|
SVOpDelete SVOp = "delete"
|
|
SVOpDeleteCAS SVOp = "delete-cas"
|
|
SVOpCAS SVOp = "cas"
|
|
)
|
|
|
|
// SVOpResult constants give possible operations results from a transaction.
|
|
type SVOpResult string
|
|
|
|
const (
|
|
SVOpResultOk SVOpResult = "ok"
|
|
SVOpResultConflict SVOpResult = "conflict"
|
|
SVOpResultRedacted SVOpResult = "conflict-redacted"
|
|
SVOpResultError SVOpResult = "error"
|
|
)
|
|
|
|
// SecureVariablesApplyRequest is used by users to operate on the secure variable store
|
|
type SecureVariablesApplyRequest struct {
|
|
Op SVOp // Operation to be performed during apply
|
|
Var *SecureVariableDecrypted // Variable-shaped request data
|
|
WriteRequest
|
|
}
|
|
|
|
// SecureVariablesApplyResponse is sent back to the user to inform them of success or failure
|
|
type SecureVariablesApplyResponse struct {
|
|
Op SVOp // Operation performed
|
|
Input *SecureVariableDecrypted // Input supplied
|
|
Result SVOpResult // Return status from operation
|
|
Error error // Error if any
|
|
Conflict *SecureVariableDecrypted // Conflicting value if applicable
|
|
Output *SecureVariableDecrypted // Operation Result if successful; nil for successful deletes
|
|
WriteMeta
|
|
}
|
|
|
|
func (r *SecureVariablesApplyResponse) IsOk() bool {
|
|
return r.Result == SVOpResultOk
|
|
}
|
|
|
|
func (r *SecureVariablesApplyResponse) IsConflict() bool {
|
|
return r.Result == SVOpResultConflict || r.Result == SVOpResultRedacted
|
|
}
|
|
|
|
func (r *SecureVariablesApplyResponse) IsError() bool {
|
|
return r.Result == SVOpResultError
|
|
}
|
|
|
|
func (r *SecureVariablesApplyResponse) IsRedacted() bool {
|
|
return r.Result == SVOpResultRedacted
|
|
}
|
|
|
|
// SVApplyStateRequest is used by the FSM to modify the secure variable store
|
|
type SVApplyStateRequest struct {
|
|
Op SVOp // Which operation are we performing
|
|
Var *SecureVariableEncrypted // Which directory entry
|
|
WriteRequest
|
|
}
|
|
|
|
// SVApplyStateResponse is used by the FSM to inform the RPC layer of success or failure
|
|
type SVApplyStateResponse struct {
|
|
Op SVOp // Which operation were we performing
|
|
Result SVOpResult // What happened (ok, conflict, error)
|
|
Error error // error if any
|
|
Conflict *SecureVariableEncrypted // conflicting secure variable if applies
|
|
WrittenSVMeta *SecureVariableMetadata // for making the SecureVariablesApplyResponse
|
|
WriteMeta
|
|
}
|
|
|
|
func (r *SVApplyStateRequest) ErrorResponse(raftIndex uint64, err error) *SVApplyStateResponse {
|
|
return &SVApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: SVOpResultError,
|
|
Error: err,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *SVApplyStateRequest) SuccessResponse(raftIndex uint64, meta *SecureVariableMetadata) *SVApplyStateResponse {
|
|
return &SVApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: SVOpResultOk,
|
|
WrittenSVMeta: meta,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *SVApplyStateRequest) ConflictResponse(raftIndex uint64, cv *SecureVariableEncrypted) *SVApplyStateResponse {
|
|
var cvCopy SecureVariableEncrypted
|
|
if cv != nil {
|
|
// make a copy so that we aren't sending
|
|
// the live state store version
|
|
cvCopy = cv.Copy()
|
|
}
|
|
return &SVApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: SVOpResultConflict,
|
|
Conflict: &cvCopy,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *SVApplyStateResponse) IsOk() bool {
|
|
return r.Result == SVOpResultOk
|
|
}
|
|
|
|
func (r *SVApplyStateResponse) IsConflict() bool {
|
|
return r.Result == SVOpResultConflict
|
|
}
|
|
|
|
func (r *SVApplyStateResponse) IsError() bool {
|
|
// FIXME: This is brittle and requires immense faith that
|
|
// the response is properly managed.
|
|
return r.Result == SVOpResultError
|
|
}
|
|
|
|
type SecureVariablesListRequest struct {
|
|
QueryOptions
|
|
}
|
|
|
|
type SecureVariablesListResponse struct {
|
|
Data []*SecureVariableMetadata
|
|
QueryMeta
|
|
}
|
|
|
|
type SecureVariablesReadRequest struct {
|
|
Path string
|
|
QueryOptions
|
|
}
|
|
|
|
type SecureVariablesReadResponse struct {
|
|
Data *SecureVariableDecrypted
|
|
QueryMeta
|
|
}
|
|
|
|
// ---------------------------------------
|
|
// Keyring state and RPC objects
|
|
|
|
// RootKey is used to encrypt and decrypt secure variables. It is
|
|
// never stored in raft.
|
|
type RootKey struct {
|
|
Meta *RootKeyMeta
|
|
Key []byte // serialized to keystore as base64 blob
|
|
}
|
|
|
|
// NewRootKey returns a new root key and its metadata.
|
|
func NewRootKey(algorithm EncryptionAlgorithm) (*RootKey, error) {
|
|
meta := NewRootKeyMeta()
|
|
meta.Algorithm = algorithm
|
|
|
|
rootKey := &RootKey{
|
|
Meta: meta,
|
|
}
|
|
|
|
switch algorithm {
|
|
case EncryptionAlgorithmAES256GCM:
|
|
const keyBytes = 32
|
|
key := make([]byte, keyBytes)
|
|
n, err := cryptorand.Read(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n < keyBytes {
|
|
return nil, fmt.Errorf("failed to generate key: entropy exhausted")
|
|
}
|
|
rootKey.Key = key
|
|
}
|
|
|
|
return rootKey, nil
|
|
}
|
|
|
|
// RootKeyMeta is the metadata used to refer to a RootKey. It is
|
|
// stored in raft.
|
|
type RootKeyMeta struct {
|
|
KeyID string // UUID
|
|
Algorithm EncryptionAlgorithm
|
|
CreateTime int64
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
State RootKeyState
|
|
}
|
|
|
|
// RootKeyState enum describes the lifecycle of a root key.
|
|
type RootKeyState string
|
|
|
|
const (
|
|
RootKeyStateInactive RootKeyState = "inactive"
|
|
RootKeyStateActive = "active"
|
|
RootKeyStateRekeying = "rekeying"
|
|
RootKeyStateDeprecated = "deprecated"
|
|
)
|
|
|
|
// NewRootKeyMeta returns a new RootKeyMeta with default values
|
|
func NewRootKeyMeta() *RootKeyMeta {
|
|
now := time.Now().UTC().UnixNano()
|
|
return &RootKeyMeta{
|
|
KeyID: uuid.Generate(),
|
|
Algorithm: EncryptionAlgorithmAES256GCM,
|
|
State: RootKeyStateInactive,
|
|
CreateTime: now,
|
|
}
|
|
}
|
|
|
|
// RootKeyMetaStub is for serializing root key metadata to the
|
|
// keystore, not for the List API. It excludes frequently-changing
|
|
// fields such as ModifyIndex so we don't have to sync them to the
|
|
// on-disk keystore when the fields are already in raft.
|
|
type RootKeyMetaStub struct {
|
|
KeyID string
|
|
Algorithm EncryptionAlgorithm
|
|
CreateTime int64
|
|
State RootKeyState
|
|
}
|
|
|
|
// Active indicates his key is the one currently being used for
|
|
// crypto operations (at most one key can be Active)
|
|
func (rkm *RootKeyMeta) Active() bool {
|
|
return rkm.State == RootKeyStateActive
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) SetActive() {
|
|
rkm.State = RootKeyStateActive
|
|
}
|
|
|
|
// Rekeying indicates that variables encrypted with this key should be
|
|
// rekeyed
|
|
func (rkm *RootKeyMeta) Rekeying() bool {
|
|
return rkm.State == RootKeyStateRekeying
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) SetRekeying() {
|
|
rkm.State = RootKeyStateRekeying
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) SetInactive() {
|
|
rkm.State = RootKeyStateInactive
|
|
}
|
|
|
|
// Deprecated indicates that variables encrypted with this key
|
|
// have been rekeyed
|
|
func (rkm *RootKeyMeta) Deprecated() bool {
|
|
return rkm.State == RootKeyStateDeprecated
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) SetDeprecated() {
|
|
rkm.State = RootKeyStateDeprecated
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) Stub() *RootKeyMetaStub {
|
|
if rkm == nil {
|
|
return nil
|
|
}
|
|
return &RootKeyMetaStub{
|
|
KeyID: rkm.KeyID,
|
|
Algorithm: rkm.Algorithm,
|
|
CreateTime: rkm.CreateTime,
|
|
State: rkm.State,
|
|
}
|
|
|
|
}
|
|
func (rkm *RootKeyMeta) Copy() *RootKeyMeta {
|
|
if rkm == nil {
|
|
return nil
|
|
}
|
|
out := *rkm
|
|
return &out
|
|
}
|
|
|
|
func (rkm *RootKeyMeta) Validate() error {
|
|
if rkm == nil {
|
|
return fmt.Errorf("root key metadata is required")
|
|
}
|
|
if rkm.KeyID == "" || !helper.IsUUID(rkm.KeyID) {
|
|
return fmt.Errorf("root key UUID is required")
|
|
}
|
|
if rkm.Algorithm == "" {
|
|
return fmt.Errorf("root key algorithm is required")
|
|
}
|
|
switch rkm.State {
|
|
case RootKeyStateInactive, RootKeyStateActive,
|
|
RootKeyStateRekeying, RootKeyStateDeprecated:
|
|
default:
|
|
return fmt.Errorf("root key state %q is invalid", rkm.State)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncryptionAlgorithm chooses which algorithm is used for
|
|
// encrypting / decrypting entries with this key
|
|
type EncryptionAlgorithm string
|
|
|
|
const (
|
|
EncryptionAlgorithmAES256GCM EncryptionAlgorithm = "aes256-gcm"
|
|
)
|
|
|
|
type KeyringRotateRootKeyRequest struct {
|
|
Algorithm EncryptionAlgorithm
|
|
Full bool
|
|
WriteRequest
|
|
}
|
|
|
|
// KeyringRotateRootKeyResponse returns the full key metadata
|
|
type KeyringRotateRootKeyResponse struct {
|
|
Key *RootKeyMeta
|
|
WriteMeta
|
|
}
|
|
|
|
type KeyringListRootKeyMetaRequest struct {
|
|
// TODO: do we need any fields here?
|
|
QueryOptions
|
|
}
|
|
|
|
type KeyringListRootKeyMetaResponse struct {
|
|
Keys []*RootKeyMeta
|
|
QueryMeta
|
|
}
|
|
|
|
// KeyringUpdateRootKeyRequest is used internally for key replication
|
|
// only and for keyring restores. The RootKeyMeta will be extracted
|
|
// for applying to the FSM with the KeyringUpdateRootKeyMetaRequest
|
|
// (see below)
|
|
type KeyringUpdateRootKeyRequest struct {
|
|
RootKey *RootKey
|
|
Rekey bool
|
|
WriteRequest
|
|
}
|
|
|
|
type KeyringUpdateRootKeyResponse struct {
|
|
WriteMeta
|
|
}
|
|
|
|
// KeyringGetRootKeyRequest is used internally for key replication
|
|
// only and for keyring restores.
|
|
type KeyringGetRootKeyRequest struct {
|
|
KeyID string
|
|
QueryOptions
|
|
}
|
|
|
|
type KeyringGetRootKeyResponse struct {
|
|
Key *RootKey
|
|
QueryMeta
|
|
}
|
|
|
|
// KeyringUpdateRootKeyMetaRequest is used internally for key
|
|
// replication so that we have a request wrapper for writing the
|
|
// metadata to the FSM without including the key material
|
|
type KeyringUpdateRootKeyMetaRequest struct {
|
|
RootKeyMeta *RootKeyMeta
|
|
Rekey bool
|
|
WriteRequest
|
|
}
|
|
|
|
type KeyringUpdateRootKeyMetaResponse struct {
|
|
WriteMeta
|
|
}
|
|
|
|
type KeyringDeleteRootKeyRequest struct {
|
|
KeyID string
|
|
WriteRequest
|
|
}
|
|
|
|
type KeyringDeleteRootKeyResponse struct {
|
|
WriteMeta
|
|
}
|