Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ checkmd: $(MD_FILES)
.PHONY: test
test: vendor
gpg --import pgp/sops_functional_tests_key.asc 2>&1 1>/dev/null || exit 0
unset SOPS_AGE_KEY_FILE; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./...
unset SOPS_AGE_KEY_FILE; unset SOPS_AGE_KEY_CMD; LANG=en_US.UTF-8 $(GO) test $(GO_TEST_FLAGS) ./...

.PHONY: showcoverage
showcoverage: test
Expand Down
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ On macOS, this would be ``$HOME/Library/Application Support/sops/age/keys.txt``.
Windows, this would be ``%AppData%\sops\age\keys.txt``. You can specify the location
of this file manually by setting the environment variable **SOPS_AGE_KEY_FILE**.
Alternatively, you can provide the key(s) directly by setting the **SOPS_AGE_KEY**
environment variable.
environment variable. Alternatively, you can provide a command to output the age keys
by setting the **SOPS_AGE_KEY_CMD** environment variable.

The contents of this key file should be a list of age X25519 identities, one
per line. Lines beginning with ``#`` are considered comments and ignored. Each
Expand Down
19 changes: 18 additions & 1 deletion age/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import (
"bufio"
"bytes"
"errors"
"filippo.io/age/plugin"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"filippo.io/age/plugin"
"github.com/sirupsen/logrus"

"github.com/getsops/sops/v3/logging"
"github.com/google/shlex"
)

const (
Expand All @@ -27,6 +29,9 @@ const (
// SopsAgeKeyFileEnv can be set as an environment variable pointing to an
// age keys file.
SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE"
// SopsAgeKeyCmdEnv can be set as an environment variable with a command
// to execute that returns the age keys.
SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD"
// SopsAgeSshPrivateKeyFileEnv can be set as an environment variable pointing to
// a private SSH key file.
SopsAgeSshPrivateKeyFileEnv = "SOPS_AGE_SSH_PRIVATE_KEY_FILE"
Expand Down Expand Up @@ -310,6 +315,18 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
readers[SopsAgeKeyFileEnv] = f
}

if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
args, err := shlex.Split(ageKeyCmd)
if err != nil {
return nil, fmt.Errorf("failed to parse command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)
}
out, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, fmt.Errorf("failed to execute command %s from %s: %w", ageKeyCmd, SopsAgeKeyCmdEnv, err)
}
readers[SopsAgeKeyCmdEnv] = bytes.NewReader(out)
}

userConfigDir, err := getUserConfigDir()
if err != nil && len(readers) == 0 && len(identities) == 0 {
return nil, fmt.Errorf("user config directory could not be determined: %w", err)
Expand Down
27 changes: 27 additions & 0 deletions age/keysource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,33 @@ func TestMasterKey_loadIdentities(t *testing.T) {
assert.ErrorContains(t, err, fmt.Sprintf("failed to parse '%s' age identities", SopsAgeKeyEnv))
assert.Nil(t, got)
})

t.Run(SopsAgeKeyCmdEnv, func(t *testing.T) {
tmpDir := t.TempDir()
// Overwrite to ensure local config is not picked up by tests
overwriteUserConfigDir(t, tmpDir)

t.Setenv(SopsAgeKeyCmdEnv, "echo '"+mockIdentity+"'")

key := &MasterKey{}
got, err := key.loadIdentities()
assert.NoError(t, err)
assert.Len(t, got, 1)
})

t.Run("cmd error", func(t *testing.T) {
tmpDir := t.TempDir()
// Overwrite to ensure local config is not picked up by tests
overwriteUserConfigDir(t, tmpDir)

t.Setenv(SopsAgeKeyCmdEnv, "meow")

key := &MasterKey{}
got, err := key.loadIdentities()
assert.Error(t, err)
assert.ErrorContains(t, err, "failed to execute command meow")
assert.Nil(t, got)
})
}

// overwriteUserConfigDir sets the user config directory and the user home directory
Expand Down
Loading