Derive named secrets from one root secret, declared in hiera.
Companion to sc_random, with
the same ergonomics and a different guarantee. sc_random produces repeatable
values from fqdn_rand, which is a pure function of the node's FQDN and the
resource name — fine for spreading cron minutes across a fleet, and not a secret,
because anyone who can guess a hostname can reproduce every value it makes. This
module produces values that cannot be reproduced without the root secret.
Not from this module. $root defaults to the top-scope $sc_project_secret,
which has to be set from outside — how that value is produced is not this
module's concern, and it never learns. It only derives from it.
Declare secrets in hiera:
sc_secrets::secrets:
grafana-admin: # no body: purpose is the key, size is 32
archive-store:
purpose: archive-store-key
size: 48size is in hex characters and defaults to 32, which is 128 bits. purpose
defaults to the key name and is what actually gets hashed.
Then interpolate them anywhere hiera does — most often into another class's parameter:
grafana::admin_password: "%{sc_secrets::secret.grafana-admin}"Or from Puppet code:
include sc_secrets
$password = $sc_secrets::secret['grafana-admin']The class is sugar over one function, which takes the root explicitly and knows nothing else:
sc_secrets::derive($root, $purpose, $size = 32)$token = sc_secrets::derive($sc_project_secret, 'metrics-scrape-token')Reach for it directly when you need a secret in Puppet code without declaring it in hiera, or when the root is not the default.
HMAC-SHA256(root, purpose), lowercase hex, truncated to size.
Hex rather than a denser alphabet is deliberate: these values land in my.cnf
files, YAML, service environments and shell, and a character set with no quoting
hazards is worth more than the few bits per character it costs.
Two purposes never collide, and one derived secret does not yield another — so a leaked database password does not hand over the archive store.
The key name is the default purpose, and the purpose is what gets hashed.
Renaming a hiera key therefore rotates that secret, exactly as renaming an
sc_random resource re-rolls its value. Set purpose explicitly if you want a
name you can change freely, and treat any change to purpose or size as a
password rotation on every consumer.
Declare secrets at the narrowest hierarchy layer that needs them. Every entry
in sc_secrets::secrets is computed into the catalog of every node that reaches
that layer, and is interpolatable from any yaml that node reads. A secret
declared in common.yaml exists on every node; the same secret declared in a
role exists only on nodes holding that role.
Rotation is not staged. Change the root and every derived secret changes at once, on each node's next run — so two nodes sharing a derived password are briefly out of step. Whichever side reconciles first fails against the other until both have run. Every consumer must be managed by Puppet, or it is stranded at the old value with nothing to update it.
There is no escrow. Derived values are not stored anywhere; lose the root and every secret it produced is gone, not recoverable. That is the point, and it makes the root the one value you have to be right about.
secrets |
Hash of secrets to derive, keyed by name. Per entry: purpose (defaults to the key) and size in hex characters (defaults to 32). |
root |
The root secret every entry is derived from, set from outside this module. Defaults to the top-scope $sc_project_secret. Undef is an error only when secrets are declared, so a node that declares none still compiles where no root is configured. |
Puppet >= 6, puppetlabs-stdlib. The derivation runs wherever catalogs are
compiled — on a primary server that is the server, not the agent, so neither the
root nor any derived value is computed on a node.