rootkey

package
v0.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 20 Imported by: 0

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 — unless secrets.kms holds them in a key-management service instead (pkg/kms, Task 20433) — 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:

  1. 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;
  2. 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;
  3. 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

View Source
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.

View Source
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.

View Source
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

View Source
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.

View Source
var ErrNoKey = errors.New("rootkey: no key to derive from")

ErrNoKey is returned when there is no value to derive from.

Functions

func CacheID

func CacheID(purpose string, parts ...string) string

CacheID is the process-local index of a value under a purpose label, for the derivation caches here and in pkg/secretbroker.

func Current

func Current() (string, error)

Current returns the current root's value, "" when none is configured.

func DeriveCheckpointKey

func DeriveCheckpointKey(value string, iterations int) ([]byte, string, error)

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

func Fingerprint(value string) (string, error)

Fingerprint names value the way every checkpoint, sidecar, escrow, sealing key and hub member does: 16 hex characters.

func InsideCloopDir

func InsideCloopDir(path string) bool

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.

func Pin

func Pin(r Roots) (unpin func())

Pin makes every later Resolve in this process return r, and returns a function that undoes it. `cloop ui` pins what it resolved at startup; see the package comment.

func Pinned

func Pinned() bool

Pinned reports whether this process pinned its roots.

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.

func (Root) Describe

func (r Root) Describe() string

Describe names where the root came from, for a message. It never includes the value.

func (Root) Set

func (r Root) Set() bool

Set reports whether the slot holds a key.

type Roots

type Roots struct {
	Current  Root
	Previous Root
}

Roots is the current root and, while the root is being changed, the previous one.

func Resolve

func Resolve() (Roots, error)

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

func (r Roots) Changing() bool

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

func (r Roots) Fingerprints() (current, previous string, err error)

Fingerprints names the configured roots; "" for a slot that is not set.

func (Roots) Values

func (r Roots) Values() []string

Values returns the configured roots, current first: what a keyring derives from, in the order it tries them.

type Source

type Source string

Source is where a root was read from.

const (
	SourceFile        Source = "file"
	SourceCredential  Source = "credential"
	SourceEnvironment Source = "environment"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL