credz

package
v1.801.472 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package credz is how a cloud process gets its credentials.

THE PROBLEM. Every subsystem reads its secrets from the process environment — CLOUD_AI_API_KEY, IAM_CLIENT_SECRET, DO_API_TOKEN, driverName, ~50 names across 108 apps. When those apps ran as one binary that was one environment to fill. Now they are child processes: zip spawns each with `os.Environ() + ZIP_ADDR`, so whatever the launcher holds, EVERY child holds — all 50 secrets in all 108 processes, readable in each one's /proc/<pid>/environ and inherited by anything any of them execs. The alternative that was actually happening is worse: nothing was filled in at all, so a lazily-spawned child booted with no data-plane key and no provider credential and failed or served 503.

THE SHAPE OF THE ANSWER. One process holds the root key. Every other process asks it, over a unix socket, for the credentials of the app it is — and gets only those. Nothing secret is passed at spawn, so nothing secret is at rest in any child's environment.

WHY THE ENVIRONMENT IS STILL THE INTERFACE. The bundle is installed with os.Setenv, so all 108 apps keep reading os.Getenv and not one of them changes. That is the DRY choice AND the secure one: a value set after execve never appears in /proc/<pid>/environ, which is the kernel's snapshot of the argument page as it was passed. Same interface, none of the exposure.

WHERE IDENTITY COMES FROM. The launcher, and only the launcher. It stamps a per-child token into zip.Plugin.Env at spawn; the child presents it here; the broker verifies it against the secret it holds (credz/launch). Nothing the peer chose about itself decides its scope.

It used to. The broker took SO_PEERCRED's pid and read the peer's argv out of /proc — but execve takes argv from the caller, so any same-uid process could exec itself as `billing` and be handed billing's scope, and the broker logged it as a legitimate grant. SO_PEERCRED remains, doing the two things it can actually do: the uid check that says the peer is one of this deployment's own processes, and the pid in the audit line.

THE HONEST LIMIT. The token is in the child's environment, which the same uid can read at /proc/<pid>/environ, and every plugin in the pod IS that uid. So the cost of impersonating an app went from nothing to "first steal a live peer's token", and a stolen token buys exactly the one app it was stolen from. That is a real boundary against accident and against casual forgery; it is NOT a boundary against a peer that reads its neighbours. Making it one means the socket becomes the credential — the launcher pre-connects and passes the fd as an ExtraFile, which nothing can name or copy — and that is a change to zip's spawn contract, not to this package. See credz/launch for the full statement.

THE THREE POSTURES, resolved once at Boot:

Root   — CLOUD_KMS_MASTER_KEY_REF was in MY environment. I hold the root key,
         I scrub it so my children do not inherit it, and (if I also own the
         secret store) I serve the broker.
Leaf   — no root key; I pulled my bundle from the broker.
Dev    — no root key and no broker, on a build that cannot be production
         (the live SQLCipher codec is not linked). Deterministic dev key, zero
         configuration: `make host` works with nothing provisioned.

A production build with no key and no broker resolves to none of these and fails closed at the first store open, exactly as it did before.

ORDERING IS THE WHOLE BUG. cek memoizes the master key on first use, so the key must be installed before the first store opens — which is inside BuildDeps, not after it. Boot is sync.Once-guarded and called from the two entry points that precede every open (Serve, BuildDeps); calling it twice is free and calling it late is impossible.

Index

Constants

View Source
const RootEnv = launch.RootEnv

RootEnv is the ONE variable a production deployment provisions. It carries the base64 32-byte key that both unseals the KMS secret store and encrypts the cek data plane; every other secret lives inside that store. cek names the same variable — one key, one name, no second gate. It is defined on the launch leaf (credz/launch) because the light host, which cannot import credz, scrubs this same name from its children; there is one spelling of it, and it is there.

View Source
const SockName = "credz.sock"

SockName is the broker's socket, inside the data directory. That directory is already the deployment's private state (RWO volume, 0700), so the filesystem is the ACL — the same rule zip uses for the plugin sockets themselves.

Variables

This section is empty.

Functions

func Err

func Err() error

Err reports why a broker pull failed, or nil. A Leaf that could not reach the broker degrades to Dev or Unkeyed rather than dying, so this is the only place the reason survives.

func Key

func Key() []byte

Key returns the root key this process holds, or nil. It is the ONE accessor: the environment variable is scrubbed at Boot so a child never inherits it, and everything downstream that needs the key (the embedded KMS store, cek) reads it from here instead of re-reading an environment that is deliberately empty.

func KeyB64

func KeyB64() string

KeyB64 is Key in the base64 form cloud.Config and the embedded KMS client carry. Empty when no key is held.

func LaunchSecret added in v1.801.307

func LaunchSecret() string

LaunchSecret is the key this process signs its children's identities with, and the key its broker verifies them against. One value for both because in the fused topology they are one process: /cloud holds the root key, serves the broker, AND spawns the plugin children whose tokens it will later be asked to open. Two values there would be two ways to spell one fact, and the second one would be the bug.

It is adopted from the environment when it is there and minted when it is not, which is the difference between the two topologies rather than a mode:

  • Fused (/cloud) — nothing to adopt, so it is minted here and never leaves the process. Children get tokens; nothing gets the secret.
  • Host (cmd/cloud) — the launcher is the host and the broker is a child, so the secret has to cross that one edge. cmd/cloud mints it and hands it to the broker child ALONE, which adopts it here.

Scrubbed on adoption for the same reason RootEnv is: a process holding this can mint any app's identity, and leaving it in the environment hands that power to anything the broker ever execs.

func Publish

func Publish(p Posture, src Source, dataDir, adminOrg string, log Logger) (io.Closer, error)

Publish starts the broker, and is a no-op unless this process is both the root of the credential tree (p) and the owner of the secret store (src). Both halves matter:

  • Not Root — a Leaf holds the same data-plane key it was handed, but it was handed ONE app's scope. Letting it broker would let any app answer for any other, which is the whole boundary.
  • No Source — a process reaching KMS over RPC has no store to read and no authority to delegate. Exactly one process in a deployment owns the sealed store, so exactly one can be the broker, and it is not a choice.

The posture is a parameter rather than a read of this package's own state because it is the caller's fact: Boot resolved it, and passing it makes "who may broker" a value at the call site instead of an invisible precondition.

The returned Closer stops accepting and removes the socket. A nil Closer means this process does not broker, which is the normal case for 107 of 108 apps.

The process that WAS asked to broker and declined says which half was missing. That silence cost real time: on 2026-07-29 the pubsub child died with "CLOUD_KMS_MASTER_KEY_REF is required" while the broker child booted clean, and nothing in either log said the deployment had no broker at all. Because a child launched by the host always carries a CREDZ_TOKEN, and resolve() refuses to fall back to a dev key once a token is present, a missing broker means EVERY child resolves Unkeyed and fails closed at its first store open — the loudest possible consequence from the quietest possible cause. A declining broker is only boring when it was never the broker (not Root); the other two reasons are a misconfiguration this line names.

func Scope

func Scope(adminOrg, app string) []string

Scope is the answer to "which secrets may this app read", and it is derived, not configured. There is no second registry: the manifest already names every app in the fleet, and the KMS store already holds every secret. An app's scope is the path its own name spells.

/orgs/{adminOrg}/svc/_shared/{NAME}   every app
/orgs/{adminOrg}/svc/{app}/{NAME}     that app only

{NAME} is the environment variable the app already reads — CLOUD_AI_API_KEY, driverName, DO_API_TOKEN. So provisioning is "put the value where the name says", the broker needs to know nothing about what any app wants, and the set of credentials is data in the store rather than a table in code that drifts from it. The path is built from the app the LAUNCHER stamped on the peer, never from anything in the request — so a peer cannot spell a path, only present a token for the one it was started as.

The paths sit under the admin org rather than the reserved platform partition so they are reachable through the KMS surface that already exists (GET /v1/kms/orgs/{adminOrg}/secrets/svc/{app}/{NAME}), gated by the org check that already guards it. One store, one authorization model, one way to provision.

Types

type Logger

type Logger interface {
	Info(msg string, ctx ...interface{})
	Warn(msg string, ctx ...interface{})
	Error(msg string, ctx ...interface{})
}

Logger is the slice of the platform logger this package uses. Declared here rather than imported so credz stays a leaf: it is linked into every child, and a credential path should not drag a logging tree behind it.

type Posture

type Posture string

Posture is how this process resolved its credentials. It exists to be logged: the failure this package was written to fix announced a dev key while none was installed, so the resolved posture is now a value, not a guess.

const (
	Root    Posture = "root"    // root key from my own environment
	Leaf    Posture = "leaf"    // bundle pulled from the broker
	Dev     Posture = "dev"     // deterministic dev key; no key and no broker
	Unkeyed Posture = "unkeyed" // production build, nothing configured — opens fail closed
)

func Boot

func Boot(dataDir string) Posture

Boot resolves this process's credentials and installs them: the root key into cek, the scoped service credentials into the environment. It runs exactly once and must run before the first store opens.

It never fails. Every posture that cannot serve is expressed as the posture itself plus a reason in Err(), because the honest failure is the first store open refusing to run unencrypted — not a boot that dies before the logger is up and takes the reason with it.

func Resolved

func Resolved() (Posture, int, string)

Resolved reports the posture, how many scoped credentials were installed, and where they came from. Callers log it; nothing branches on it.

type Source

type Source interface {
	Names(path, env string) ([]string, error)
	Get(path, name, env string) ([]byte, error)
}

Source is the secret store the broker reads. It is deliberately two methods: enumerate a scope, read one name. Nothing here knows what a secret means, so adding a credential is a write to the store and not a change to this package.

The embedded KMS client satisfies it. An RPC or disabled KMS client does not, and that is the point: only the process that OWNS the sealed store can broker it, so which process is the broker is decided by the deployment topology rather than by a flag.

Directories

Path Synopsis
Package launch is how a launcher tells the credential broker which app it started.
Package launch is how a launcher tells the credential broker which app it started.

Jump to

Keyboard shortcuts

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