Repository navigation
feat!: bind provider tools locally instead of in the tracked scope file - #19
Merged
Merged
Conversation
Schema 1 stored sops_executable, sops_sha256, and plugin_path in
.dotenv-sec.yaml, a tracked and trust-hashed file. Three consequences: a
routine sops upgrade failed the checksum and required editing a tracked
file, which then broke the scope for every other machine; one committed
file could not describe more than one OS or architecture, so cross-platform
use was unrepresentable; and a colleague resolving the conflict could only
paste in whatever checksum their own machine produced, which is not an
integrity property.
It also contradicted the documented protocol invariant that repository
configuration cannot provide executable paths or inherited environment:
run.go fed the repository's plugin_path into the provider's PATH, and the
SOPS provider took the repository's sops_executable as the binary to run.
The threat model anchors trust in the per-user registry and excludes a
compromised SOPS from scope, so the checksum's real job was preventing the
repository from selecting an executable. Recording it in the repository
inverted that.
Scope schema 2 keeps portable policy only, with an optional sops_min_version
floor. The binding moves to the local provider registry as plugin_path plus
tools{executable,sha256}. Protocol v2 carries tools and identity_path as
dedicated fields that the parent overwrites from the registry immediately
before dispatch, so a repository-supplied value can never reach them; the
provider fails closed rather than falling back to a PATH lookup. Existing
scopes convert with dotenvsec migrate, then re-approve.
Fix three defects found while implementing this:
Homebrew-installed age plugins were reported missing. Plugin discovery
rejected every symlink, which is how Homebrew publishes all binaries, while
doctor used a weaker os.Stat check that followed them, so doctor passed
while loading failed. Resolution now canonicalizes and checks the target,
shared by both paths. Tool bindings canonicalize on write for the same
reason, keeping the checksummed file and the executed file identical.
Re-registering a provider silently discarded its tool bindings, so any
upgrade that moved the bundled executable would have unbound SOPS and
stranded every scope on the machine.
ValidateExactEnvironment reported a name mismatch without naming it,
requiring the encrypted document's keys to be read to find the offender.
The names are already cleartext in both the scope file and those keys, so
they are now reported; values still never appear.
BREAKING CHANGE: scope schema 2 and provider protocol v2. Run dotenvsec
migrate per scope, then dotenvsec allow on every machine.
Version comparison discarded the fmt.Sscanf error return, which errcheck rejected. Parsing with strconv and surfacing the failure also removes the silent-zero behaviour: unparsable input is now an error rather than a value that could satisfy a version floor by accident.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
.dotenv-sec.yamlis tracked and trust-hashed, yet schema 1 stored three machine-specific values in it:sops_executable(absolute path),sops_sha256(per OS/arch/version), andplugin_path(absolute PATH list, typically containing a home directory).That produced the two failures reported:
brew upgrade sops→sops checksum mismatchonallow. The only fix was editing a tracked file, which then broke the scope for everyone else.It also contradicted the project's own documented invariant.
docs/provider-protocol.mdstates repository configuration cannot provide executable paths or inherited environment, butrun.gofed the repo'splugin_pathinto the provider'sPATHand the SOPS provider took the repo'ssops_executableas the binary to run.docs/threat-model.mdanchors trust in the per-user registry and explicitly excludes a compromised SOPS from scope — so the checksum's real job is stopping the repository from selecting an executable. Storing it in the repository inverted that.What changed
Scope schema 2 holds portable policy only, plus an optional
sops_min_versionfloor. The binding moves to the local provider registry:Protocol v2 carries
toolsandidentity_pathas dedicated fields the parent overwrites from the registry immediately before dispatch, so a repository-supplied value cannot reach them. The provider fails closed instead of falling back to aPATHlookup.Tool checksums are verified before every launch but deliberately stay out of the approval hash — re-approving after a routine
brew upgrade sopsadds friction without review value. Drift surfaces indoctor.Three defects found while implementing
doctorused a weakeros.Statcheck that followed them. Sodoctorpassed while loading failed. Both now share one resolver that canonicalizes and checks the target. This is the issue reported by the user on a Homebrew-installedage-plugin-yubikey.ValidateExactEnvironmentdid not name the offending variable, so diagnosingprovider output names do not match declared environmentmeant reading the encrypted document's keys. Those names are already cleartext in the scope file and the ciphertext keys; values still never appear.Migration
migraterefuses to run twice. The trust hash changes, so every machine re-approves once.Verification
go build,go vet,gofmtclean; all 10 packages pass. New coverage: migration (incl. non-idempotence), machine-local key rejection, version floor, Homebrew symlink resolution, unsafe-target rejection, binding preservation across re-registration, mismatch-naming without value leakage.Exercised end-to-end against a copy of a real 6-recipient scope in an isolated
HOME: schema 1 refused →migrate→allow→doctorall-OK. A simulated SOPS upgrade (corrupted pin) fails closed withtool sops checksum mismatch: … re-pin with dotenvsec provider retool, recovers via one local command, and leaves the scope file and approval untouched.One behaviour was corrected mid-review: verifying tools inside
Verifybrokestatusanddoctorthemselves under a stale binding — the very commands needed to diagnose it. Tools are now verified at dispatch, sodoctorreportsFAIL SOPS <reason>and read-only commands keep working.BREAKING CHANGE: scope schema 2 and provider protocol v2.