secrets

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package secrets is the Go port of .lok8s/libs/secrets — secret cache management with SOPS/age encryption.

The lok8s secret plugin generates values into the secrets store as plaintext cache files (Secret.<name>.<ns>.<key>). This package adds SOPS/age encryption so those cache files can be committed safely.

Layout (single-instance project — flat store):

.secrets/Secret.myapp.default.PASSWORD       ← plaintext (gitignored)
.secrets/Secret.myapp.default.PASSWORD.enc   ← SOPS-encrypted (committed)
.secrets/tls/                                ← mkcert certs (gitignored)

Per-INSTANCE isolation (multi-domain repos): a domain keeps its OWN secrets under clusters/<domain>/secrets/, and the CLI + `lo build` use only that store for that domain. This is the secure default for anything serving multiple environments — dev/prod never share a generated value, and SOPS creation_rules can encrypt each domain's store to DIFFERENT recipients so a dev/CI key cannot decrypt prod. There is deliberately NO shared tier: a value needed in two instances is a manual copy (the operator consenting to that exposure), never a silent fallback. See docs/guide/secrets.md.

Encryption uses age keys derived from SSH keys (the ssh-to-age scheme). No new key ceremony — your existing ~/.ssh/id_ed25519 is your encryption identity.

Index

Constants

This section is empty.

Variables

View Source
var ErrHandled = ui.ErrHandled // one sentinel for every package; see internal/ui

ErrHandled marks an error whose message was already printed in the bash implementation's own format ([error] … on stderr). Callers exit non-zero without printing anything further.

Functions

func CheckFlatShadows

func CheckFlatShadows(flat, domainDir string, out io.Writer) bool

CheckFlatShadows reports deprecated flat-store shadows of a domain's per-domain secrets (bash: secrets::check_flat_shadows).

A domain with its OWN store (clusters/<domain>/secrets/) must not ALSO keep the same Secret.<name>.<ns>.<key> in the flat store: the two diverge silently and different tools read different stores (lo build/Tilt → per-domain; a manual PATH_SECRETS=.../.secrets kustomize → flat), which can re-key a live cluster from the WRONG store (this orphaned a dev IdP's etcd encryption once). Per-domain is authoritative; nothing is legitimately flat-only since v0.4.0 (the registry TLS cert lives in a docker volume, internal/driver/lo/registrytls.go).

Emits one line per shadow to out (lint wraps each as a warning); returns false if any shadow exists, true otherwise. An identical copy is a stale duplicate; a DIFFERING copy is active drift — the dangerous case.

func CheckUnencrypted

func CheckUnencrypted(dir string, warnTo io.Writer) bool

CheckUnencrypted reports plaintext secrets without a fresh .enc twin in dir (used by lint; bash: secrets::check_unencrypted). Returns false if any plaintext secret lacks a corresponding up-to-date .enc file, true when all secrets are encrypted or no secrets exist.

func DecryptYAMLFile

func DecryptYAMLFile(path string) ([]byte, error)

DecryptYAMLFile decrypts a sops-encrypted YAML file (a restore.d/*.sops.yaml manifest) IN MEMORY and returns the plaintext. The counterpart of the bash `sops -d "${f}"` in bootstrap::_restore_d — key discovery is ambient (SOPS_AGE_KEY / SOPS_AGE_KEY_FILE / the age keys dir), exactly like the CLI. Capturing the plaintext in memory (instead of a sops|kubectl pipe) is deliberate: piping sops's 2>&1 into kubectl once corrupted the YAML stream with sops warnings — see the bash comment at the bootstrap::_restore_d call site.

Types

type Context

type Context struct {
	Paths  *config.Paths
	Domain string // resolved domain ("" = flat store only)
	Out    io.Writer
	ErrOut io.Writer

	// Stdin and StdinIsTTY feed `set`'s value fallback (piped stdin vs the
	// interactive prompt). ReadPassword is the silent tty read; injectable
	// for tests.
	Stdin        io.Reader
	StdinIsTTY   func() bool
	ReadPassword func() (string, error)

	// Kubeconfig is the KUBECONFIG value the bash entrypoint exported
	// (PATH_BASE/.kubeconfig/<cluster>.yaml) — the live-drift check's kubectl
	// runs against exactly that file, never the caller's ambient KUBECONFIG.
	Kubeconfig string

	// Runner runs the two external tools this package still shells out to
	// (kubectl for the live-drift check, the clipboard tool for print
	// --copy). nil = execx.NewRunner(Paths); tests install a fake.
	Runner execx.Runner
}

Context carries the resolved environment a secrets operation runs in.

func (*Context) AddKey

func (c *Context) AddKey(key string, all, skipOrphans bool) error

AddKey adds a recipient to .sops.yaml and re-keys every cache file so they can actually read it (bash: secrets::add-key). Both halves are required: appending the key alone leaves the new recipient unable to decrypt ANYTHING, because sops only rewrites a file's recipients when that file is re-encrypted.

Was documented before it was implemented (issue #73). The manual equivalent — edit .sops.yaml, `touch` every plaintext twin (Encrypt skips files whose .enc is newer, so a recipient change re-encrypts nothing), then encrypt per domain — is exactly what this removes.

FAILS CLOSED on an orphan: an `.enc` with no decrypted twin cannot be re-keyed here, and silently leaving it behind is the worst outcome — the new recipient would appear to be added while some secrets stayed unreadable to them. Decrypt first, or pass skipOrphans to accept the gap knowingly. (Like bash, the orphan check runs AFTER .sops.yaml was already modified — the ordering is part of the observable contract.)

func (*Context) Allow

func (c *Context) Allow() error

Allow approves bash: generators: it collects the per-entry hashes from existing .sha files and writes the approved SET to .bash-allow (bash: secrets::allow).

The set is one hash per line, sorted + unique. The Go plugin treats .bash-allow as a set and requires every bash entry in a (per-target) build to be a member — so a subset build still matches. (Was a single hash-of-all-hashes, which a per-target build's subset could never match — see kustomize/plugins/secret/generator/bash.go verifyBashAllow.)

func (*Context) Decrypt

func (c *Context) Decrypt(sshKey string) error

Decrypt restores plaintext cache files from .enc twins (bash: secrets::decrypt).

func (*Context) Encrypt

func (c *Context) Encrypt(name string) error

Encrypt sweeps plaintext cache files into .enc twins (bash: secrets::encrypt). name narrows the sweep to ONE Secret's cache files.

Without it this walks the whole store and encrypts every plaintext whose .enc is older — including entries the operator was mid-edit on, or values they were only trying out. That is fine as the deliberate "stage everything" move and wrong as the default for "I changed one secret", which is the common case; committing an .enc for a value nobody meant to publish is not something the store can walk back.

The sweep includes .sha files on purpose — they get .sha.enc twins.

func (*Context) Env

func (c *Context) Env(name, namespace string) error

Env emits `export KEY=value` lines for every key of a cached secret, so provisioning-time credentials live in the managed (SOPS-encrypted, per-domain) store instead of a loose plaintext env file (bash: secrets::env). Each cache KEY becomes the exported variable name — name the keys after the vars you want (e.g. HCLOUD_TOKEN). Load them with: eval "$(lo secrets --domain <domain> env --name hetzner)" Read-only; values are shell-quoted with %q so the eval is injection-safe.

func (*Context) Init

func (c *Context) Init(sshKey string) error

Init configures SOPS/age encryption from an SSH public key (bash: secrets::init). The bash `command -v ssh-to-age/sops` gates disappear — both are libraries in the Go build.

func (*Context) List

func (c *Context) List() error

List lists the store's entries with their encryption state (bash: secrets::list).

func (*Context) Print

func (c *Context) Print(ctx context.Context, patterns []string, onlyOne, toClipboard bool) error

Print prints secret(s) whose basenames match every pattern (case-insensitive, regex) (bash: secrets::print). toClipboard implies onlyOne.

func (*Context) Set

func (c *Context) Set(ctx context.Context, name, namespace, key, value string, doEncrypt bool) error

Set writes a literal value into the secret cache (bash: secrets::set). value semantics mirror the argsh spec: a bare "-" reads stdin, an empty/omitted value falls back to a silent tty prompt or piped stdin (command-substitution semantics: trailing newlines stripped).

func (*Context) StorePath

func (c *Context) StorePath() string

StorePath resolves the secrets store for the current context (bash: secrets::path). A domain with its own clusters/<domain>/secrets/ uses THAT store exclusively (per-instance isolation, no flat fallback for it); otherwise the flat PATH_SECRETS store (single-instance projects). `lo build` mirrors this — see build::_export_secrets_path.

String concatenation on purpose (not filepath.Join): bash interpolates the domain into the path verbatim, and `lo secrets path` must print the same bytes.

Jump to

Keyboard shortcuts

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