e5ac6464f6
Move the secure variables quota enforcement calls into the state store to ensure quota checks are atomic with quota updates (in the same transaction). Switch to a machine-size int instead of a uint64 for quota tracking. The ENT-side quota spec is described as int, and negative values have a meaning as "not permitted at all". Using the same type for tracking will make it easier to the math around checks, and uint64 is infeasibly large anyways. Add secure vars to quota HTTP API and CLI outputs and API docs.
518 lines
13 KiB
Go
518 lines
13 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 (
|
|
// SecureVariablesUpsertRPCMethod is the RPC method for upserting
|
|
// secure variables into Nomad state.
|
|
//
|
|
// Args: SecureVariablesUpsertRequest
|
|
// Reply: SecureVariablesUpsertResponse
|
|
SecureVariablesUpsertRPCMethod = "SecureVariables.Upsert"
|
|
|
|
// SecureVariablesDeleteRPCMethod is the RPC method for deleting
|
|
// a secure variable by its namespace and path.
|
|
//
|
|
// Args: SecureVariablesDeleteRequest
|
|
// Reply: SecureVariablesDeleteResponse
|
|
SecureVariablesDeleteRPCMethod = "SecureVariables.Delete"
|
|
|
|
// 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
|
|
}
|
|
|
|
type SecureVariablesUpsertRequest struct {
|
|
Data []*SecureVariableDecrypted
|
|
CheckIndex *uint64
|
|
WriteRequest
|
|
}
|
|
|
|
func (svur *SecureVariablesUpsertRequest) SetCheckIndex(ci uint64) {
|
|
svur.CheckIndex = &ci
|
|
}
|
|
|
|
type SecureVariablesEncryptedUpsertRequest struct {
|
|
Data []*SecureVariableEncrypted
|
|
WriteRequest
|
|
}
|
|
|
|
type SecureVariablesUpsertResponse struct {
|
|
Conflicts []*SecureVariableDecrypted
|
|
WriteMeta
|
|
}
|
|
|
|
type SecureVariablesListRequest struct {
|
|
QueryOptions
|
|
}
|
|
|
|
type SecureVariablesListResponse struct {
|
|
Data []*SecureVariableMetadata
|
|
QueryMeta
|
|
}
|
|
|
|
type SecureVariablesReadRequest struct {
|
|
Path string
|
|
QueryOptions
|
|
}
|
|
|
|
type SecureVariablesReadResponse struct {
|
|
Data *SecureVariableDecrypted
|
|
QueryMeta
|
|
}
|
|
|
|
type SecureVariablesDeleteRequest struct {
|
|
Path string
|
|
CheckIndex *uint64
|
|
WriteRequest
|
|
}
|
|
|
|
func (svdr *SecureVariablesDeleteRequest) SetCheckIndex(ci uint64) {
|
|
svdr.CheckIndex = &ci
|
|
}
|
|
|
|
type SecureVariablesDeleteResponse struct {
|
|
Conflict *SecureVariableDecrypted
|
|
WriteMeta
|
|
}
|
|
|
|
// 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
|
|
}
|