Documentation
¶
Overview ¶
Package encryption encrypts dashboard-managed secrets at rest.
Secret fields (provider API keys, MCP headers, guardrail secrets) are sealed with AES-256-GCM under a data key (DEK) before a store writes them. Each database holds its own random DEKs in the encryption_keys table, wrapped by a key-encryption key (KEK): either one derived from GOMODEL_ENCRYPTION_KEY with Argon2id, or an extension's KeyWrapper (a KMS). See ADR-0014.
A sealed value reads enc:v1:<key-id>:<base64(nonce||ciphertext)>. The additional authenticated data binds it to one entity and field, so a ciphertext copied to another row or field fails to open instead of decrypting there.
Index ¶
- Constants
- Variables
- func AAD(kind, id, field string) []byte
- func IsSealed(value string) bool
- type Box
- func (b *Box) ActiveKeyID() string
- func (b *Box) ConfirmSeal(reseal func() error, fields ...Field) error
- func (b *Box) Enabled() bool
- func (b *Box) IsCurrent(value string) bool
- func (b *Box) NeedsReseal(fields ...Field) bool
- func (b *Box) Open(aad []byte, value string) (string, error)
- func (b *Box) OpenFields(kind, id string, fields ...Field) error
- func (b *Box) RotatedSinceSeal(fields ...Field) (bool, error)
- func (b *Box) Seal(aad []byte, plaintext string) (string, error)
- func (b *Box) SealFields(kind, id string, fields ...Field) error
- type Field
- type Key
- type KeyStore
- type MongoDBKeyStore
- type Options
- type Report
- type RowOutcome
- type SQLKeyStore
- type Wrapper
Constants ¶
const LocalWrapperID = "local"
LocalWrapperID is the wrapper id recorded for data keys wrapped with the KEK derived from GOMODEL_ENCRYPTION_KEY.
Variables ¶
var ErrKeyRequired = errors.New("value is encrypted but GOMODEL_ENCRYPTION_KEY is not set")
ErrKeyRequired is returned when a sealed value is read without an encryption key configured.
var ErrSealUnconfirmed = errors.New("the secret was saved, but its data key could not be confirmed; save it again or run `gomodel secrets reencrypt`")
ErrSealUnconfirmed reports a save that was written but whose data key could not be confirmed as current afterwards, so it may still be sealed with a key a rotation replaced. The stored value stays readable.
Functions ¶
Types ¶
type Box ¶
type Box struct {
// contains filtered or unexported fields
}
Box seals and opens secret field values. A Box without keys (Disabled, or a nil *Box) passes plaintext through unchanged, which is the behaviour of a deployment without GOMODEL_ENCRYPTION_KEY.
A Box is safe for concurrent use.
func Disabled ¶
func Disabled() *Box
Disabled returns a Box that stores secrets in plaintext. It still logs one warning the first time it reads a plaintext secret.
func Open ¶
Open loads the database's data keys and returns the Box that seals and opens secret fields with them.
Without a configured KEK it returns a disabled Box, unless the database already holds data keys: their secrets would be unreadable, so that is an error. With a KEK and no data key yet, it creates the first one. A data key wrapped by an older KEK (GOMODEL_ENCRYPTION_KEY_PREVIOUS, or the local KEK when an extension wrapper is now configured) is re-wrapped with the current one.
func RotateDataKey ¶
RotateDataKey creates a new data key, wraps it with the current KEK, and makes it active. Values sealed with older data keys stay readable; `gomodel secrets reencrypt` moves them to the new one.
func (*Box) ActiveKeyID ¶
ActiveKeyID returns the id of the data key new values are sealed with, or "" when encryption is disabled.
func (*Box) ConfirmSeal ¶
ConfirmSeal is what a store runs right after writing freshly sealed fields: when RotatedSinceSeal reports a rotation, it calls reseal to rewrite the row with the new key. A failed check is retried once after a forced key reload. Any remaining failure wraps ErrSealUnconfirmed: the row is written, so the caller must not treat the save as lost, but it must not report a clean save either.
func (*Box) Enabled ¶
Enabled reports whether the Box seals new values. It never changes over the life of a Box: a reload only adds keys or moves the active one.
func (*Box) IsCurrent ¶
IsCurrent reports whether value is sealed with the active data key, so re-encryption can skip it.
func (*Box) NeedsReseal ¶
NeedsReseal reports whether any field holds plaintext or a value sealed with a data key other than the active one. It is always false for a disabled Box.
func (*Box) Open ¶
Open decrypts a sealed value. Plaintext is returned as-is: rows written before encryption was enabled stay readable and are sealed on their next write.
func (*Box) OpenFields ¶
OpenFields opens every field of one entity in place.
func (*Box) RotatedSinceSeal ¶
RotatedSinceSeal reports whether the data key that sealed fields is no longer active, according to the key store. Stores call it right after writing freshly sealed values, which closes the race with a data key rotation: if the key is still active, any rotation activates after the write, so the rotation's re-encryption pass will read the row; if it is not, the store re-seals the row itself. A Box without a key store, or fields with nothing sealed, never report a rotation. It fails when the key store cannot say which key is active.
func (*Box) Seal ¶
Seal encrypts plaintext under the active data key. Empty values and every value of a disabled Box are returned unchanged.
A Box backed by a key store first checks which data key is active, so a gateway that outlived a data key rotation never seals with the replaced key. Seals only happen on admin writes, so the extra read is cheap.
type Field ¶
Field points at one secret value of an entity. Name is the field name bound into the value's additional authenticated data.
type Key ¶
type Key struct {
// ID is what sealed values name. Ids are decimal sequence numbers: the
// first key is "1", so instances starting together against an empty
// database race on the primary key rather than creating two keys.
ID string
// Wrapped is the data key encrypted by the wrapper named in WrapperID.
Wrapped []byte
WrapperID string
// KDF, KDFParams and Salt describe how the local KEK is derived from
// GOMODEL_ENCRYPTION_KEY. They are empty for an extension wrapper.
KDF string
KDFParams string
Salt []byte
// Active marks the key new values are sealed with.
Active bool
CreatedAt time.Time
}
Key is one row of the encryption_keys table: a data key wrapped by a key-encryption key. The plaintext data key is never stored.
type KeyStore ¶
type KeyStore interface {
// List returns every key.
List(ctx context.Context) ([]Key, error)
// Insert adds a key and reports false when the id is already taken.
Insert(ctx context.Context, key Key) (bool, error)
// UpdateWrapping replaces how a key is wrapped (after a KEK rotation).
UpdateWrapping(ctx context.Context, key Key) error
// Activate marks id, the newest key, active and clears the flag of
// older keys.
Activate(ctx context.Context, id string) error
}
KeyStore persists wrapped data keys.
type MongoDBKeyStore ¶
type MongoDBKeyStore struct {
// contains filtered or unexported fields
}
MongoDBKeyStore stores wrapped data keys in MongoDB.
func NewMongoDBKeyStore ¶
NewMongoDBKeyStore uses the encryption_keys collection. It needs no indexes: the collection holds a handful of documents keyed by _id.
func (*MongoDBKeyStore) Activate ¶
func (s *MongoDBKeyStore) Activate(ctx context.Context, id string) error
Activate sets the new key active, then clears the flag of every older key. MongoDB transactions need a replica set, which a standalone server lacks, so the two steps are not atomic. Clearing only lower ids keeps concurrent rotations from clearing each other's key: whatever interleaving, the newest activated key stays active, and readers pick the newest of several.
func (*MongoDBKeyStore) UpdateWrapping ¶
func (s *MongoDBKeyStore) UpdateWrapping(ctx context.Context, key Key) error
type Options ¶
type Options struct {
// Key is GOMODEL_ENCRYPTION_KEY. It derives the local KEK.
Key string
// PreviousKey is GOMODEL_ENCRYPTION_KEY_PREVIOUS: a local KEK that is
// only used to unwrap data keys so they can be re-wrapped with the
// current one.
PreviousKey string
// Wrapper is an extension's KMS-backed wrapper. When set it replaces the
// local KEK; Key and PreviousKey then only unwrap data keys that still
// need to be moved to it.
Wrapper Wrapper
// PreviousWrappers unwrap data keys held by wrappers Wrapper replaces,
// so they can be re-wrapped with it.
PreviousWrappers []Wrapper
}
Options selects the key-encryption key.
type Report ¶
type Report struct {
// Entity names the table or collection.
Entity string
// Rows is how many rows were examined.
Rows int
// Reencrypted is how many rows were rewritten.
Reencrypted int
// Skipped is how many rows kept changing under the pass and were left
// for the next run.
Skipped int
}
Report counts what a re-encryption pass did to one entity kind. It never carries values.
func ReencryptRows ¶
func ReencryptRows(entity string, names []string, rewrite func(name string) (RowOutcome, error)) (Report, error)
ReencryptRows runs rewrite for each row and tallies the outcomes. A row in conflict is re-read and retried a few times, then counted as skipped.
type RowOutcome ¶
type RowOutcome int
RowOutcome is what re-encrypting one row did.
const ( // RowUnchanged: the row needed no rewrite, or no longer exists. RowUnchanged RowOutcome = iota // RowRewritten: the row's secrets were re-sealed. RowRewritten // RowConflict: the row's secrets changed between the read and the // conditional write, so nothing was written. RowConflict )
type SQLKeyStore ¶
type SQLKeyStore struct {
// contains filtered or unexported fields
}
SQLKeyStore stores wrapped data keys in SQLite or PostgreSQL. Binary values are stored as base64 text so one schema serves both engines.
func NewSQLKeyStore ¶
NewSQLKeyStore creates the encryption_keys table if needed.
func (*SQLKeyStore) Activate ¶
func (s *SQLKeyStore) Activate(ctx context.Context, id string) error
Activate flips every row in one statement, so readers never see two active keys or none.
func (*SQLKeyStore) UpdateWrapping ¶
func (s *SQLKeyStore) UpdateWrapping(ctx context.Context, key Key) error
type Wrapper ¶
type Wrapper interface {
ID() string
WrapKey(ctx context.Context, dek []byte) ([]byte, error)
UnwrapKey(ctx context.Context, wrapped []byte) ([]byte, error)
}
Wrapper wraps and unwraps data keys with a key-encryption key held elsewhere, such as a KMS. It has the method set of config.KeyWrapper, so an extension's wrapper is passed straight through.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package encryptiontest provides an in-memory key store and ready Boxes for tests of stores that seal secrets.
|
Package encryptiontest provides an in-memory key store and ready Boxes for tests of stores that seal secrets. |