Documentation
¶
Overview ¶
Package secrets implements the credential-layer machinery described in docs/SPEC-SECRETS.md. It provides four verbs -- exec, names, check, and keygen -- as a thin wrapper over sops and age-keygen, linking no cryptography of its own.
THE ONE RULE THIS PACKAGE EXISTS TO KEEP: a credential value is never printed, logged, put in an argument list, or written anywhere but the child process environment that asked for it by name.
Index ¶
- Constants
- Variables
- func CheckAgeKeygenVersion(run execCommand, ageKeygenPath string) (string, error)
- func CheckInvariant6(keyPath string) error
- func CheckSopsVersion(run execCommand, sopsPath string) (string, error)
- func DecryptFile(run execCommand, sopsPath, keyPath, filePath string) ([]byte, error)
- func ExtractPublicKeyFromKeyFile(keyPath string) (string, error)
- func GitBlobSHA1(content []byte) [20]byte
- func IsValidAgePublicKey(s string) bool
- func IsValidAsName(name string) bool
- func IsValidEnvVar(s string) bool
- func Leaks(text string, s Secret) bool
- func ParseDecryptedSecrets(data []byte) (map[string]Secret, []string, error)
- func ReadFleetMachines(path string) (map[string]FleetMachine, error)
- func ReadHEADTreeBlobs(storeDir string) (map[string]string, error)
- func ReadRecoveryPub(storeDir string) (string, error)
- func ReadUnitKeys(u UnitKeyLogin) (map[string]Secret, error)
- func RouteKey(model string, names []string) string
- func RunCheck(storeDir, asName, keyPath, sopsPath string, maxShown int) (okLine string, failLines []string, moreLines []string, summaryLine string, ...)
- func RunExec(storeDir, asName, keyPath, sopsPath, onlyArg string, required []string, ...) (int, error)
- func RunGate(in GateInput) (string, int)
- func RunKeygen(asName, keyPath, ageKeygenPath, storeDir string) (lines []string, err error)
- func RunPlace(in PlaceInput) (string, error)
- func RunPlaced(in PlacedInput) (string, []string, error)
- func RunSeal(opts SealOptions) (line string, err error)
- func RunSeatAdd(opts SeatAddOptions) ([]string, error)
- func RunSeatInject(opts SeatInjectOptions) (string, error)
- func ValidateAdmissibleStore(storeDir string) (status GitRefStatus, headBlobs map[string]string, indexData *GitIndexData, ...)
- type CheckFailure
- func CheckInvariant1(storeDir string, sopsCfg *SopsConfig, recoveryKey string) []CheckFailure
- func CheckInvariant2(storeDir string, sopsCfg *SopsConfig, files []string) []CheckFailure
- func CheckInvariant3(storeDir string, sopsCfg *SopsConfig, files []string) (failures []CheckFailure, sealedCount int, clearCount int)
- func CheckInvariant4(storeDir, sopsPath, keyPath, seatPubKey string, files []string) (failures []CheckFailure, mineCount int, foreignCount int)
- func CheckInvariant5(storeDir, keyPath string) []CheckFailure
- func CheckInvariant7(storeDir string, trackedFiles map[string]bool) []CheckFailure
- type CheckSummary
- type CreationRule
- type FleetMachine
- type GateInput
- type GitIndexData
- type GitIndexEntry
- type GitRefStatus
- type Login
- type NameRow
- type NamesReport
- type PlaceInput
- type PlacedInput
- type SealOptions
- type SeatAddOptions
- type SeatFile
- type SeatInjectOptions
- type Secret
- func (s Secret) Empty() bool
- func (s Secret) Format(f fmt.State, verb rune)
- func (s Secret) GoString() string
- func (s Secret) Loaded() bool
- func (s Secret) MarshalJSON() ([]byte, error)
- func (s Secret) MarshalText() ([]byte, error)
- func (s Secret) String() string
- func (s Secret) Use(fn func(string) error) error
- type SopsConfig
- type StoreFileKey
- type UnitKeyLogin
Constants ¶
const DecisionKey = "JEV_API_KEY"
DecisionKey is the secret the sprint server's decision loop reads. It is the same name decide.JevSecret carries; secrets does not import that package.
const KeygenNextLine = "SECRETS RULE NEXT: add these two lines to .sops.yaml (or run `nova-secrets seat add`)"
KeygenNextLine is the one line that tells the reader what is left to do in the rule block. It is a NEXT STEP and says so: the same text as a state of the world ("the placeholder stands unfilled") read as a failure at the end of a green run.
const MinAgeKeygenVersion = "1.3.2"
const MinLeakFragment = 6
MinLeakFragment is how many consecutive bytes of a secret count as a leak. Six, not "the whole value", because the leak that actually happens is a truncated one.
const MinSopsVersion = "3.13.3"
const Redacted = "[redacted]"
Redacted is the only string a Secret ever yields to a formatter. One word, in one place, so no caller can produce a "helpfully" partial one.
const SeatMarkKey = "NOVA_SECRETS_WRITTEN_BY"
SeatMarkKey is the root key every verb-written seat file carries in the clear. A rule that lists it in `unencrypted_regex` keeps sops from sealing it, and the gate reads it.
Variables ¶
var MarkVersion = "dev"
MarkVersion is the tool version the mark names; the command sets it from its build stamp.
Functions ¶
func CheckAgeKeygenVersion ¶
CheckAgeKeygenVersion probes the age-keygen binary.
func CheckInvariant6 ¶
CheckInvariant6 verifies that the key file mode is 0600 and directory is 0700.
func CheckSopsVersion ¶
CheckSopsVersion probes the sops binary with --disable-version-check to prevent network calls.
func DecryptFile ¶
DecryptFile invokes sops -d on a file using an isolated environment. It sets SOPS_AGE_KEY_FILE to keyPath, strips all other SOPS_* variables, and sets HOME and XDG_CONFIG_HOME to an empty temporary directory.
func ExtractPublicKeyFromKeyFile ¶
ExtractPublicKeyFromKeyFile parses '# public key: (age1...)' from an age private key file.
func GitBlobSHA1 ¶
GitBlobSHA1 computes the SHA1 hash of a git blob object for content.
func IsValidAgePublicKey ¶
IsValidAgePublicKey verifies an age public key string (bech32).
func IsValidAsName ¶
IsValidAsName checks if a seat name matches [A-Za-z0-9_-]+
func IsValidEnvVar ¶
IsValidEnvVar verifies an environment variable name: ^[A-Z][A-Z0-9_]*$
func Leaks ¶
Leaks reports whether text contains this secret, whole or in a fragment of at least MinLeakFragment consecutive bytes.
func ParseDecryptedSecrets ¶
ParseDecryptedSecrets parses the output of 'sops -d' into Secret values. Rejects multi-line values, NUL bytes, and illegal variable names.
func ReadFleetMachines ¶
func ReadFleetMachines(path string) (map[string]FleetMachine, error)
ReadFleetMachines parses the place machine table: name, ssh target, home, and the optional fourth column the pulse fleet file carries. It refuses the gate registry's seven-column format rather than treating os/arch as a home path (SPEC-SECRETS "place"). Blank lines and `#` comments are skipped; a line with fewer than two fields, more than four fields, or an empty name or target is refused.
func ReadHEADTreeBlobs ¶
ReadHEADTreeBlobs reads the tree of the HEAD commit and returns a map of relative path -> blob SHA1. It inspects loose objects directly in pure Go, with fallback to git ls-tree if needed.
func ReadRecoveryPub ¶
ReadRecoveryPub reads and validates the recovery.pub file at store root.
func ReadUnitKeys ¶
func ReadUnitKeys(u UnitKeyLogin) (map[string]Secret, error)
ReadUnitKeys reads each named secret in this process through OpenSeatFile, once. The values come back as Secrets and go into no environment. A field left empty, a name that is not a secret name, a seat that does not open, and a name the seat does not hold or holds empty are each a refusal naming that name and the next command; none of them is ever an empty key.
func RouteKey ¶
RouteKey is the one name of names a route needs. The provider of model (provider/model, the slash optional) wants <PROVIDER>_API_KEY when that name is listed. Otherwise the only listed name is the route's key. The decision key is never a route key. "" when none of that is so: more than one name and none is the route's, which is not the whole set.
func RunCheck ¶
func RunCheck(storeDir, asName, keyPath, sopsPath string, maxShown int) (okLine string, failLines []string, moreLines []string, summaryLine string, exitCode int, err error)
RunCheck executes the complete verification suite for 'check'.
func RunExec ¶
func RunExec(storeDir, asName, keyPath, sopsPath, onlyArg string, required []string, cmdArgs []string) (int, error)
RunExec validates all preflight invariants, decrypts <store>/<as>.yaml, applies --only and --require filters, prints the OK line to stderr, sets RLIMIT_CORE to 0, and replaces the current process with cmdArgs. The OK line goes out before the exec call, so a failure of that call returns 125 with a FAIL line saying the command never started.
func RunGate ¶
RunGate is the seat-rule gate as a verb: the store's shell gate, called by the workflow. It diffs --base..--head with git (no GitHub) and either prints "GATE APPROVE files=<n> machines=<registry|->" at exit 0 or "GATE FAILED rule=<n> check=<k> file=<f>: <why>" at exit 1; a gate that could not run prints "SECRETS GATE REFUSED: <why>; run: nova-secrets gate -h" at exit 2, so a CI step reads a broken change apart from a gate that never ran (skeleton contract 1.2). Independent inputs and findings are reported together in the check order (SPEC-SECRETS "gate").
func RunKeygen ¶
RunKeygen generates a new age private key and formats the .sops.yaml rule block. It returns the receipt as ordered lines; the caller prints them in that order.
func RunPlace ¶
func RunPlace(in PlaceInput) (string, error)
RunPlace copies one secret to one machine and records a receipt. It returns the one OK line, or an error whose text is safe to print (it never contains the value).
func RunPlaced ¶
func RunPlaced(in PlacedInput) (string, []string, error)
RunPlaced lists the receipts written for one machine: each secret, its remote path, the sealed file it was placed from (file, head, blob) and its stamp. A receipt from an older build lists as identity=unknown, and a NOTE says its file still holds an old digest on disk and how to rewrite or remove it.
func RunSeal ¶
func RunSeal(opts SealOptions) (line string, err error)
RunSeal reads one value, folds it into the seat file under --name, and carries the change through a branch, a commit and, unless --no-pr, a pull request to its merge.
func RunSeatAdd ¶
func RunSeatAdd(opts SeatAddOptions) ([]string, error)
RunSeatAdd re-seals the named values out of a source seat into a new seat's file and writes that seat's rule. It returns the receipt as ordered lines, the verdict last. No value appears on any of them, ever.
func RunSeatInject ¶
func RunSeatInject(opts SeatInjectOptions) (string, error)
RunSeatInject re-seals the --only values out of the source seat into an existing seat's file, encrypted to that file's own recipients, and carries the change to the store as `seal` does. It returns the one OK line; no value appears on it, ever.
func ValidateAdmissibleStore ¶
func ValidateAdmissibleStore(storeDir string) (status GitRefStatus, headBlobs map[string]string, indexData *GitIndexData, failures []CheckFailure, refusal error)
ValidateAdmissibleStore verifies that the git working copy at storeDir is an admissible store. It verifies: 1. Invariant 8 ref tracking: HEAD equals remote-tracking ref (via CheckGitWorkingCopy). 2. HEAD commit tree and git index can be read. 3. Every tracked store file (*.yaml, .sops.yaml, recovery.pub) in HEAD tree or git index:
- exists in working copy as a regular file (no deletions or non-regular artifact replacements)
- is tracked in index and committed in HEAD tree (no uncommitted additions or index drift)
- working copy blob SHA1 matches HEAD tree blob SHA1 and git index blob SHA1 (no unstaged or staged modifications)
Types ¶
type CheckFailure ¶
type CheckFailure struct {
Kind string // invariant kind: "rule-shape", "recipients-drift", "unsealed", "decrypt-failed", "foreign-openable", "store-private-key", "untracked-plaintext", "stale-working-copy"
File string // file path or subject
Reason string // diagnostic detail
}
CheckFailure records one invariant failure found during 'check'.
func CheckInvariant1 ¶
func CheckInvariant1(storeDir string, sopsCfg *SopsConfig, recoveryKey string) []CheckFailure
CheckInvariant1 checks .sops.yaml syntax and shape against the declared recovery key.
func CheckInvariant2 ¶
func CheckInvariant2(storeDir string, sopsCfg *SopsConfig, files []string) []CheckFailure
CheckInvariant2 verifies that each file's sops recipient block matches its creation rule.
func CheckInvariant3 ¶
func CheckInvariant3(storeDir string, sopsCfg *SopsConfig, files []string) (failures []CheckFailure, sealedCount int, clearCount int)
CheckInvariant3 verifies that every file is sealed and only permitted keys are in the clear.
func CheckInvariant4 ¶
func CheckInvariant4(storeDir, sopsPath, keyPath, seatPubKey string, files []string) (failures []CheckFailure, mineCount int, foreignCount int)
CheckInvariant4 tests that the seat key opens exactly the files listing its public key.
func CheckInvariant5 ¶
func CheckInvariant5(storeDir, keyPath string) []CheckFailure
CheckInvariant5 verifies that no private key is stored under storeDir.
func CheckInvariant7 ¶
func CheckInvariant7(storeDir string, trackedFiles map[string]bool) []CheckFailure
CheckInvariant7 verifies that untracked files hold no plaintext secrets.
type CheckSummary ¶
type CheckSummary struct {
Recipients int // unique recipient public keys across all rules
Files int // count of *.yaml files (excluding .sops.yaml)
Sealed int // count of files that are sealed
Mine int // count of files whose recipients list this seat
Foreign int // count of files whose recipients do not list this seat
Clear int // count of keys in the clear under unencrypted_regex
Failures []CheckFailure
}
CheckSummary aggregates counts and failures across the store.
type CreationRule ¶
type CreationRule struct {
PathRegex string
UnencryptedRegex string
Recipients []string
NonAgeRecipients []string
}
CreationRule defines one rule in .sops.yaml.
func FindMatchingRule ¶
func FindMatchingRule(cfg *SopsConfig, relPath string) (*CreationRule, error)
FindMatchingRule locates the creation rule that governs filePath.
type FleetMachine ¶
FleetMachine is one machine in the fleet registry: a name, the ssh target that reaches it, and its home directory (used as the default root for a placed secret).
type GateInput ¶
type GateInput struct {
StoreDir string
Base string
Head string
MachinesPath string // the fleet machines registry; "" leaves the recipient rule dormant
}
GateInput is one call of the gate: the store, the two refs, and -- optionally -- the fleet's machines registry, the only thing that can vouch for a seat nobody has seen before.
type GitIndexData ¶
type GitIndexData struct {
Entries map[string]GitIndexEntry
}
GitIndexData holds all parsed entries from .git/index.
func ReadGitIndex ¶
func ReadGitIndex(storeDir string) (*GitIndexData, error)
ReadGitIndex reads .git/index directly to find all tracked files and their blob SHAs. Supports index formats v2, v3, and v4 (prefix compression).
type GitIndexEntry ¶
GitIndexEntry holds path and blob SHA1 parsed from .git/index.
type GitRefStatus ¶
type GitRefStatus struct {
HeadSHA string
RemoteRef string
RemoteSHA string
Clean bool
Refusal string
}
GitRefStatus holds the result of verifying invariant 8 against a git working copy.
func CheckGitWorkingCopy ¶
func CheckGitWorkingCopy(storeDir string) (GitRefStatus, error)
CheckGitWorkingCopy inspects .git directly without invoking the git binary. It verifies invariant 8: HEAD equals the remote-tracking ref it tracks. Refusals are returned for: - .git is missing or not a directory (a file indicates submodule or worktree) - detached HEAD - branch with no remote tracking upstream
type Login ¶
type Login struct {
Store string // the store's working copy (--store)
As string // the seat (--as)
Key string // the seat's age key file (--key)
Sops string // the sops binary (--sops)
Name string // the secret's name in the seat's file (--secret)
}
Login is where a tool's store login reads its password in its own process: one name in one seat of a secrets store, opened with the seat's key and sops. It holds no secret and cannot: a tool may record a Login in its config file and print every field of it (nova-sprint seat login, docs/SPEC-SECRETS.md, "A tool's store login").
func (Login) Missing ¶
Missing names every field of the login left empty, as the flags that set them, in one list: "" when none is.
func (Login) String ¶
String is the login on one line, every field a fact and none of them a secret.
func (Login) UnitKeyLogin ¶
func (l Login) UnitKeyLogin(names ...string) UnitKeyLogin
UnitKeyLogin returns a UnitKeyLogin for the seat named by l, naming names.
type NameRow ¶
NameRow is one key name in a seat's file and whether the store keeps it in the clear. It holds no value, sealed or clear.
type NamesReport ¶
NamesReport is what names found: the seat, the rows shown (at most --max), and the counts over the whole file. Lines renders it as typed lines; the caller renders the same value as JSON, so the two cannot drift.
type PlaceInput ¶
type PlaceInput struct {
StoreDir string
AsName string
KeyPath string
SopsPath string
Machine string
Secret string
RemotePath string
Machines string
Receipts string
SSH string
// DryRun prints the plan and writes nothing: the seat file is decrypted (the read the
// verb needs to know the secret exists) but no ssh child runs and no receipt is written.
DryRun bool
Now func() time.Time
// Exec replaces the os/exec child process, as it does for seal: the sops decrypt, the
// version probe and the ssh delivery all run through it. Tests set a strict fake so
// place's refusals run with no real sops and no host reached.
Exec execCommand
// Guard replaces the process-wide host guard for the ssh delivery. A test passes
// testguard.NewGuard(true) so it can assert the seam refuses without setting
// NOVA_TEST_NO_HOST in the process environment, which would race every parallel
// test. Nil uses testguard's process-wide default, the production path.
Guard *testguard.Guard
}
PlaceInput is one `nova-secrets place` invocation.
type PlacedInput ¶
PlacedInput is one `nova-secrets placed` invocation.
type SealOptions ¶
type SealOptions struct {
StoreDir string
AsName string
KeyPath string
SopsPath string
Name string
GHPath string
GitPath string
NoPR bool
UseStdin bool
// DryRun prints the plan and writes nothing: no value is read, nothing is encrypted,
// no file is written, and no git write, push, gh or sops encrypt runs. The plan is
// read off the same carry the real run walks (sealCarry), so the two cannot differ.
DryRun bool
Stdin io.Reader
StdinIsTerminal bool
// Progress receives one short line per step that can take time (nil = silent).
// A person at a terminal must be able to tell waiting from hung: any interactive
// verb says what it is doing before each step that can exceed a blink. The lines
// go to stderr so the one-line result on stdout stays the whole machine answer.
Progress io.Writer
Now func() time.Time
Sleep func(time.Duration)
Check func(storeDir, asName, keyPath, sopsPath string) error
// Exec replaces the os/exec child process. Tests set it to a pure-Go fake
// so the seal path runs where a POSIX shell-script fake cannot.
Exec execCommand
}
SealOptions carries one seal request and the test seams around it.
The value travels from Stdin (or the controlling terminal) to the encrypt child's stdin and nowhere else: not argv, not a file in the clear, not an output line.
type SeatAddOptions ¶
type SeatAddOptions struct {
StoreDir string
AsName string // the new seat
Pub string // the new seat's age public key
From string // a seat this machine can open
Only string // comma-separated key names to carry over
KeyPath string // this machine's key: the source seat's identity
SopsPath string
// Progress receives one short line per step that can take time (nil = silent).
// It never carries a value: step names and public facts only.
Progress io.Writer
// Exec replaces the os/exec child process, as it does for seal.
Exec execCommand
}
SeatAddOptions carries one `seat add` request and the test seam around it.
KeyPath is THIS machine's key, the one that opens From. Pub is the NEW seat's public half, which arrives from the new bench's own `keygen` receipt -- never a private key, which never leaves the bench that made it.
type SeatFile ¶
type SeatFile struct {
Path string // <store>/<seat>.yaml
HeadSHA string // the store's HEAD the file was read at
Secrets map[string]Secret // every key in the file, by name
}
SeatFile is one seat's file, opened. Values stay inside Secret: nothing here prints, logs or returns one as a string.
func OpenSeatFile ¶
OpenSeatFile runs every check exec runs before it decrypts -- the store is a directory working copy with a .sops.yaml and the seat's file, invariant 8 (HEAD equals its upstream, nothing uncommitted or untracked), invariant 1 (recovery.pub and the creation rules), invariant 6 (the key file's mode) and the sops version -- then decrypts <store>/<seat>.yaml with sops isolated from the caller's identities and parses it. It is the one path from a seat to its values: `nova-secrets exec` takes it before it replaces itself with the command, and a nova tool given --seat takes it in its own process (pkg/seatcred), so no shell wrapper stands between the two and neither can check less than the other.
type SeatInjectOptions ¶
type SeatInjectOptions struct {
StoreDir string
AsName string // the existing seat receiving the values
From string // a seat this machine can open
Only string // comma-separated key names to deliver
KeyPath string // this machine's key: the source seat's identity
SopsPath string
GHPath string
GitPath string
NoPR bool
// DryRun prints the plan and writes nothing: the source is decrypted (the read the
// verb needs to know the names exist) but nothing is encrypted, no file is written
// and no git write, push or gh call runs. The plan is read off the same carry the
// real run walks, so the two cannot differ.
DryRun bool
// Progress receives one short line per step that can take time (nil = silent).
// It never carries a value: step names and public facts only.
Progress io.Writer
Now func() time.Time
Check func(storeDir, asName, keyPath, sopsPath string) error
// Exec replaces the os/exec child process, as it does for seal.
Exec execCommand
}
SeatInjectOptions carries one `seat inject` request and the test seams around it.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret carries a credential value. The zero value is "never looked up", which is distinct from "looked up and found empty".
WHY A TYPE AND NOT A string: every formatting route is closed -- String, GoString, Format (which covers %v %s %q %x %#v and every other verb), MarshalText and MarshalJSON all yield Redacted. There is deliberately no accessor that RETURNS the value: Use hands it to a callback and takes it back, so an exposure is a visible, deliberate shape rather than an interpolation.
func ReadLogin ¶
ReadLogin reads the login's secret in this process through OpenSeatFile, the one path exec takes, so it checks everything exec checks before it decrypts. The value comes back as a Secret, never a string, and goes into no environment. A login with a field missing, a seat that does not open, and a name the seat does not hold or holds empty are each a refusal naming the login and the next thing to run; none of them is ever an empty password.
func (Secret) Empty ¶
Empty reports whether the value is the empty string. A present-but-empty entry is not a credential.
func (Secret) Format ¶
Format closes every fmt verb at once. String() alone is not enough: %#v goes to GoStringer, %x and %d bypass Stringer entirely. ignored: fmt.State's write has no caller to report to; the redacted word is all it would print
func (Secret) MarshalJSON ¶
func (Secret) MarshalText ¶
type SopsConfig ¶
type SopsConfig struct {
CreationRules []CreationRule
}
SopsConfig holds creation rules parsed from .sops.yaml.
func ParseSopsConfig ¶
func ParseSopsConfig(storeDir string) (*SopsConfig, error)
ParseSopsConfig parses creation rules from .sops.yaml in storeDir.
type StoreFileKey ¶
StoreFileKey represents a top-level key and clear status in a store file.
func ParseStoreFileWithoutDecrypting ¶
func ParseStoreFileWithoutDecrypting(filePath string) (keys []StoreFileKey, recipients []string, hasSops bool, err error)
ParseStoreFileWithoutDecrypting extracts keys, clear status, and recipients from a sealed yaml.
type UnitKeyLogin ¶
UnitKeyLogin is a seat login that names every secret a sprint unit reads in its own process, not only the store password. Names is the setting: the decision key, each provider key. It holds no secret, so a tool may record it and print every field.
func (UnitKeyLogin) Missing ¶
func (u UnitKeyLogin) Missing() string
Missing names every field left empty, as the flags that set them. "" when none is. A name list left empty is --keys.
func (UnitKeyLogin) String ¶
func (u UnitKeyLogin) String() string
String is the login on one line. Every field is a fact; none is a secret.