Documentation
¶
Overview ¶
Package keyring holds the cluster's key hierarchy for secrets at rest and performs the envelope encryption around it.
Each stored version is sealed with its own random data key (DEK), and only that DEK is wrapped by a cluster key-encryption key (KEK). Three things fall out of the indirection. Rotation stays cheap: rolling a KEK re-wraps the handful of bytes in each DEK and never touches the (arbitrarily large) ciphertext. The KEK only ever performs small fixed-size wrap and unwrap operations, so it can move behind a KMS or HSM boundary later without streaming every secret value across it. And a leaked DEK exposes exactly one version rather than every secret sharing a key.
The keyring holds up to five KEKs so keys can rotate without a flag day: a new KEK is added and becomes what new writes wrap with, existing DEKs are re-wrapped off the outgoing key in the background, and the old key retires once nothing points at it.
Index ¶
- Constants
- Variables
- func Path(dataPath string) string
- func Save(path string, ring *Keyring) error
- type Key
- type Keyring
- func (r *Keyring) Current() (Key, error)
- func (r *Keyring) CurrentID() string
- func (r *Keyring) Keys() []Key
- func (r *Keyring) MAC(value []byte) (string, error)
- func (r *Keyring) Open(s Sealed) ([]byte, error)
- func (r *Keyring) Retire(id string) (*Keyring, error)
- func (r *Keyring) Rewrap(s Sealed) (Sealed, error)
- func (r *Keyring) Rotate() (*Keyring, Key, error)
- func (r *Keyring) Seal(value []byte) (Sealed, error)
- type Sealed
Constants ¶
const Filename = "secrets.keyring"
Filename is the keyring's name under the server's data directory.
const MaxKeys = 5
MaxKeys caps how many KEKs the ring holds at once. Rotation needs the outgoing key alive alongside the incoming one; five leaves room for several overlapping rotations without letting retired keys accumulate forever.
Variables ¶
var ( // ErrUnknownKEK means a stored version names a KEK the ring no longer // holds, so its DEK cannot be unwrapped. Fail closed: this is data loss, // not a reason to try another key. ErrUnknownKEK = errors.New("secret was wrapped with a key this cluster no longer holds") // ErrNoCurrentKey means the ring holds no key to wrap new writes with. ErrNoCurrentKey = errors.New("keyring has no current key") // ErrRingFull means a rotation would exceed MaxKeys without first retiring // an old key. ErrRingFull = fmt.Errorf("keyring already holds the maximum of %d keys", MaxKeys) )
Functions ¶
Types ¶
type Key ¶
type Key struct {
// ID identifies the key in a stored version's kek_id, so a resolve knows
// which key to unwrap with and a rotation knows which rows still point at a
// retiring key.
ID string
// Material is the raw AES-256 key.
Material []byte
// CreatedAt is when the key was minted. Rotation is driven off the current
// key's age, so this is what decides when a cluster rotates on its own.
// Zero for a ring written before keys carried a time — treated as "old
// enough to rotate", which is the safe direction for a key of unknown age.
CreatedAt time.Time
}
Key is one key-encryption key in the ring.
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring is the cluster's set of KEKs. The current key is what new writes wrap with; the rest are retained so already-stored versions stay resolvable while they are re-wrapped.
A Keyring is immutable once built. Rotation produces a new ring rather than mutating one in place, so a concurrent resolve never observes a half-rotated ring.
func Ensure ¶
Ensure loads the cluster's keyring from the data directory, generating one on first use.
Losing this file makes every stored secret permanently unrecoverable, so a keyring that exists but cannot be read is a hard error: regenerating over it would silently orphan every value the cluster holds. That mirrors how the CA refuses to regenerate over an unreadable cert.
func Generate ¶
Generate builds a keyring holding a single fresh key. Used the first time a cluster stores a secret.
func New ¶
New builds a keyring from an ordered set of keys, treating currentID as the key new writes wrap with.
func (*Keyring) Keys ¶
Keys returns the ring's keys. The slice is a copy, but the key material is shared — callers must not mutate it.
func (*Keyring) MAC ¶
MAC returns a keyed hash of a plaintext under the current KEK, used to recognize that a value is already stored without decrypting anything.
It is keyed rather than a bare digest on purpose. A stored SHA-256 of the plaintext would be a precomputable oracle: an attacker who reads the store could grind a rainbow table against short or low-entropy secrets without ever touching the ciphertext. Under a key they do not hold, the digest tells them nothing.
Because the MAC is keyed by the current KEK, a value's MAC changes when keys rotate. That only costs a redundant version on the first write after a rotation — never correctness.
func (*Keyring) Retire ¶
Retire drops a key from the ring. It refuses to retire the current key, and the caller is responsible for having re-wrapped everything that pointed at it — anything still naming a retired key becomes unresolvable.
func (*Keyring) Rewrap ¶
Rewrap moves a sealed payload onto the current KEK without decrypting the value: it unwraps the DEK under its old key and re-wraps it under the new one, so a rotation touches wrapped_dek and leaves ciphertext untouched.
The value's MAC is recomputed under the current key so that duplicate detection keeps working after a rotation. That is the one place a rewrap needs the plaintext's MAC, and it is derived from the DEK-decrypted value held only in memory.
type Sealed ¶
type Sealed struct {
// Ciphertext is the value encrypted under a per-version DEK.
Ciphertext []byte
// WrappedDEK is that DEK encrypted under the KEK named by KEKID.
WrappedDEK []byte
// KEKID names which KEK wrapped the DEK.
KEKID string
// ValueMAC is a keyed hash of the plaintext, so a later write can recognize
// an identical value without decrypting anything. See MAC.
ValueMAC string
}
Sealed is the stored form of one secret version's payload.