Documentation
¶
Overview ¶
Package rootkey resolves the hub's root secret — CLOOP_SECRET_KEY, and during a change of root the key it replaces — from the one place every reader asks (Task 20425).
Why one resolver ¶
Every key-encryption key the secret broker seals under, every audit checkpoint seal, every backup sidecar seal, the key escrow and the legacy .cloop/secrets.enc derive from this one value. It used to be read by a dozen direct os.Getenv calls, each its own idea of where the key lives, so the key could live in exactly one place: the process environment. That is the worst of the places on offer. An environment variable is in /proc/<pid>/environ for anyone who can read it, in `docker inspect` and `kubectl get pod -o yaml`, in `systemctl show`, and in the environment of every child the process starts without scrubbing it.
Resolve reads each slot from three sources, in this order:
- a file named by CLOOP_SECRET_KEY_FILE (CLOOP_SECRET_KEY_PREVIOUS_FILE): an absolute path to a regular file this process owns, mode 0400 or 0600, outside every .cloop/ directory;
- a systemd credential, cloop-secret-key (cloop-secret-key-previous), in $CREDENTIALS_DIRECTORY — which is how LoadCredentialEncrypted= with systemd-creds, and a TPM, reaches the hub without the key ever being written to disk in the clear;
- the environment variable itself.
Two sources that both hold a value must hold the same one. Disagreeing sources are refused rather than ranked: an operator who moved the key into a file and left a different one in hub.env has two keys, and which one the hub silently picked would decide whether it opens anything.
Why there is a previous key ¶
The keyring holds a current root, which mints new key-encryption keys and opens, and optionally a previous one, which only opens. That is what turns changing the root from a re-mint of every credential into a rolling restart followed by `cloop hub key rotate --retire-previous`; see pkg/secretbroker/keyring.go.
The hub pins what it started with ¶
A serving hub resolves once, at startup, and Pin makes every later Resolve in the process return that answer. Its cluster peers are told the fingerprints it started with, and a key file that changes under a running process — a Kubernetes Secret update arrives in a mounted file within a minute — would otherwise change the root mid-flight, under work that already resolved the old one. A new root takes effect at a restart, which is the procedure.
Index ¶
- Constants
- Variables
- func CacheID(purpose string, parts ...string) string
- func Current() (string, error)
- func DeriveCheckpointKey(value string, iterations int) ([]byte, string, error)
- func Fingerprint(value string) (string, error)
- func InsideCloopDir(path string) bool
- func Pin(r Roots) (unpin func())
- func Pinned() bool
- type Root
- type Roots
- type Source
Constants ¶
const ( // EnvKey is the current root, read from the environment. EnvKey = "CLOOP_SECRET_KEY" // EnvKeyFile names a file holding the current root. EnvKeyFile = "CLOOP_SECRET_KEY_FILE" // EnvPrevious is the root being replaced, read from the environment. It // only opens: nothing new is ever derived from it. EnvPrevious = "CLOOP_SECRET_KEY_PREVIOUS" // EnvPreviousFile names a file holding the previous root. EnvPreviousFile = "CLOOP_SECRET_KEY_PREVIOUS_FILE" // EnvCredentialsDirectory is where systemd puts a service's credentials // (LoadCredential=, LoadCredentialEncrypted=, SetCredential=). EnvCredentialsDirectory = "CREDENTIALS_DIRECTORY" // Credential is the systemd credential holding the current root. Credential = "cloop-secret-key" // PreviousCredential is the systemd credential holding the previous root. PreviousCredential = "cloop-secret-key-previous" )
The variables and credential names each slot is read from.
const ( EndpointCurrent = "root_key" EndpointPrevious = "root_key_previous" )
Member meta keys under which a hub cluster member advertises the fingerprints of the roots it holds (hub_members.meta). `cloop hub key rotate --retire-previous` refuses until every live member advertises the new root as its current one.
const CheckpointKDFIterations = 600_000
CheckpointKDFIterations is the PBKDF2-HMAC-SHA256 work factor of the audit checkpoint key, and so of every fingerprint: a record carrying one travels through log pipelines, and each is a test of a guessed key.
Variables ¶
var ( // ErrConflict: two sources of one slot hold different keys. ErrConflict = errors.New("rootkey: sources disagree") // ErrRefused: a key file failed a check — its path, type, owner or mode. ErrRefused = errors.New("rootkey: key file refused") // ErrUnreadable: a configured file could not be read. ErrUnreadable = errors.New("rootkey: key file unreadable") // ErrPreviousAlone: a previous root with no current one. ErrPreviousAlone = errors.New("rootkey: a previous key without a current one") // ErrSameKey: the previous root is the current one. ErrSameKey = errors.New("rootkey: the previous key is the current key") )
Sentinels. Every error Resolve returns wraps one of them.
var ErrNoKey = errors.New("rootkey: no key to derive from")
ErrNoKey is returned when there is no value to derive from.
Functions ¶
func CacheID ¶
CacheID is the process-local index of a value under a purpose label, for the derivation caches here and in pkg/secretbroker.
func DeriveCheckpointKey ¶
DeriveCheckpointKey derives the audit checkpoint key from value at the given work factor, and the fingerprint that names value. The result is cached for the life of the process: the derivation costs a tenth of a second, and every keyring, checkpoint and sidecar check asks for it. The caller owns the returned slice.
iterations is a parameter so pkg/auditcheckpoint's tests can lower it for themselves; everything else passes CheckpointKDFIterations.
func Fingerprint ¶
Fingerprint names value the way every checkpoint, sidecar, escrow, sealing key and hub member does: 16 hex characters.
func InsideCloopDir ¶
InsideCloopDir reports whether path has a .cloop component. A key file, like an escrow, has to be kept apart from the directory a database backup, a snapshot or a seeded sandbox carries: the key protects that directory's contents, and kept inside it, it protects nothing from whoever gets a copy.
Types ¶
type Root ¶
type Root struct {
// Value is the secret itself. It is never printed, logged or marshalled.
Value string `json:"-"`
// Source is where it was read from; "" when the slot is not configured.
Source Source `json:"source,omitempty"`
// Path is the file a file or credential source read.
Path string `json:"path,omitempty"`
// Variable names the setting that selected the source: the environment
// variable, or the credential name.
Variable string `json:"variable,omitempty"`
// Also lists the further sources holding the same value. They agree, so
// nothing is ambiguous — but a copy left in the environment keeps every
// exposure the file was meant to end, which `cloop hub doctor` says.
Also []Source `json:"also,omitempty"`
}
Root is one root secret and where it came from.
type Roots ¶
Roots is the current root and, while the root is being changed, the previous one.
func Resolve ¶
Resolve returns the hub's roots: the pinned ones in a serving hub, otherwise read from the sources now. A slot nothing configures is returned unset with no error — a hub with no key is a supported, degraded state — but a source that is configured and wrong is an error, never an empty slot.
func (Roots) Changing ¶
Changing reports whether a previous root is configured: a change of root is in progress until `cloop hub key rotate --retire-previous` has run and the previous root has been removed from every member.
func (Roots) Fingerprints ¶
Fingerprints names the configured roots; "" for a slot that is not set.