Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ func main() {
cli.VersionPrinter = version.PrintVersion
app := cli.NewApp()

// Bridge --age-key-file into its environment variable before any command
// runs, so it applies to every decrypt path (the top-level action as well
// as the decrypt/edit/exec-env/exec-file subcommands, which have their own
// actions). Like other global flags (e.g. --config, --verbose), it must be
// given before the subcommand.
app.Before = ageKeyFileFlagBefore

keyserviceFlags := []cli.Flag{
cli.BoolTFlag{
Name: "enable-local-keyservice",
Expand Down Expand Up @@ -1734,6 +1741,11 @@ func main() {
Usage: "comma separated list of age recipients",
EnvVar: "SOPS_AGE_RECIPIENTS",
},
cli.StringFlag{
Name: "age-key-file",
Usage: "path to a file containing age identities used for decryption",
EnvVar: "SOPS_AGE_KEY_FILE",
},
cli.BoolFlag{
Name: "in-place, i",
Usage: "write output back to the same file instead of stdout",
Expand Down Expand Up @@ -2300,6 +2312,28 @@ func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, out
}, nil
}

// ageKeyFileFlagBefore is the app-level Before hook that applies the
// --age-key-file flag. It runs ahead of every command (the implicit top-level
// action and each subcommand), so the flag takes effect on all decrypt paths.
func ageKeyFileFlagBefore(c *cli.Context) error {
return applyAgeKeyFileFlag(c.String("age-key-file"))
}

// applyAgeKeyFileFlag bridges the --age-key-file CLI flag into the
// SOPS_AGE_KEY_FILE environment variable, which the age keysource reads in
// loadIdentities to locate the identity file used for decryption. Decrypt-time
// age identities are reconstructed from a file's metadata, so there is no
// MasterKey to inject the path into directly; setting the environment the
// keysource already consumes is the least-invasive way to expose this as a
// POSIX-style flag. An explicitly passed flag therefore overrides any
// pre-existing SOPS_AGE_KEY_FILE value. A no-op when the flag is unset.
func applyAgeKeyFileFlag(ageKeyFile string) error {
if ageKeyFile == "" {
return nil
}
return os.Setenv(age.SopsAgeKeyFileEnv, ageKeyFile)
}

func toExitError(err error) error {
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
return cliErr
Expand Down
78 changes: 78 additions & 0 deletions cmd/sops/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"flag"
"os"
"testing"

"github.com/getsops/sops/v3/age"
"github.com/urfave/cli"
)

func TestApplyAgeKeyFileFlag(t *testing.T) {
t.Run("sets the environment when the flag is provided", func(t *testing.T) {
// t.Setenv registers restoration of the original value; clearing it
// afterwards gives the subtest a known, unset starting point.
t.Setenv(age.SopsAgeKeyFileEnv, "")
if err := os.Unsetenv(age.SopsAgeKeyFileEnv); err != nil {
t.Fatalf("failed to unset %s: %v", age.SopsAgeKeyFileEnv, err)
}

const want = "/path/to/keys.txt"
if err := applyAgeKeyFileFlag(want); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := os.Getenv(age.SopsAgeKeyFileEnv); got != want {
t.Errorf("%s = %q, want %q", age.SopsAgeKeyFileEnv, got, want)
}
})

t.Run("overrides a pre-existing environment value", func(t *testing.T) {
t.Setenv(age.SopsAgeKeyFileEnv, "/from/env")

const want = "/from/flag"
if err := applyAgeKeyFileFlag(want); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := os.Getenv(age.SopsAgeKeyFileEnv); got != want {
t.Errorf("%s = %q, want %q", age.SopsAgeKeyFileEnv, got, want)
}
})

t.Run("leaves the environment untouched when the flag is empty", func(t *testing.T) {
const want = "/from/env"
t.Setenv(age.SopsAgeKeyFileEnv, want)

if err := applyAgeKeyFileFlag(""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := os.Getenv(age.SopsAgeKeyFileEnv); got != want {
t.Errorf("%s = %q, want %q", age.SopsAgeKeyFileEnv, got, want)
}
})
}

// TestAgeKeyFileFlagBefore exercises the actual app.Before hook through a
// cli.Context carrying the --age-key-file flag. Because the hook is wired to
// app.Before (not app.Action), it runs for every command path, including the
// decrypt/exec-env/exec-file subcommands that have their own actions.
func TestAgeKeyFileFlagBefore(t *testing.T) {
t.Setenv(age.SopsAgeKeyFileEnv, "")
if err := os.Unsetenv(age.SopsAgeKeyFileEnv); err != nil {
t.Fatalf("failed to unset %s: %v", age.SopsAgeKeyFileEnv, err)
}

const want = "/path/to/keys.txt"
set := flag.NewFlagSet("test", flag.ContinueOnError)
set.String("age-key-file", "", "")
if err := set.Set("age-key-file", want); err != nil {
t.Fatalf("failed to set flag: %v", err)
}

if err := ageKeyFileFlagBefore(cli.NewContext(nil, set, nil)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := os.Getenv(age.SopsAgeKeyFileEnv); got != want {
t.Errorf("%s = %q, want %q", age.SopsAgeKeyFileEnv, got, want)
}
}