mirror of
https://github.com/getsops/sops.git
synced 2026-02-05 12:45:21 +01:00
Add an --exec flag to pass decrypted secrets via environment variables to a child process (#504)
* first pass: add --exec flag * fix spacing * subcommand for exec as well as other bits n bobs --placeholder to pass files to child procs (similar to `find(1)`'s -exec flag) --background to background processes if you don't need them to be interactive * break the 2 execs into 2 subcommands * add a non-fifo option for people who like files instead * added a setuid flag just in case * oups, used the wrong functions * Update README.rst * typo
This commit is contained in:
98
README.rst
98
README.rst
@@ -803,6 +803,104 @@ By default ``sops`` just dumps all the output to the standard output. We can use
|
||||
``--output`` flag followed by a filename to save the output to the file specified.
|
||||
Beware using both ``--in-place`` and ``--output`` flags will result in an error.
|
||||
|
||||
Passing Secrets to Other Processes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
In addition to writing secrets to standard output and to files on disk, ``sops``
|
||||
has two commands for passing decrypted secrets to a new process: ``exec-env``
|
||||
and ``exec-file``. These commands will place all output into the environment of
|
||||
a child process and into a temporary file, respectively. For example, if a
|
||||
program looks for credentials in its environment, ``exec-env`` can be used to
|
||||
ensure that the decrypted contents are available only to this process and never
|
||||
written to disk.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# print secrets to stdout to confirm values
|
||||
$ sops -d out.json
|
||||
{
|
||||
"database_password": "jf48t9wfw094gf4nhdf023r",
|
||||
"AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
|
||||
"AWS_SECRET_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
}
|
||||
|
||||
# decrypt out.json and run a command
|
||||
# the command prints the environment variable and runs a script that uses it
|
||||
$ sops exec-env out.json 'echo secret: $database_password; ./database-import'
|
||||
secret: jf48t9wfw094gf4nhdf023r
|
||||
|
||||
# launch a shell with the secrets available in its environment
|
||||
$ sops exec-env out.json 'sh'
|
||||
sh-3.2# echo $database_password
|
||||
jf48t9wfw094gf4nhdf023r
|
||||
|
||||
# the secret is not accessible anywhere else
|
||||
sh-3.2$ exit
|
||||
$ echo your password: $database_password
|
||||
your password:
|
||||
|
||||
|
||||
If the command you want to run only operates on files, you can use ``exec-file``
|
||||
instead. By default ``sops`` will use a FIFO to pass the contents of the
|
||||
decrypted file to the new program. Using a FIFO, secrets are only passed in
|
||||
memory which has two benefits: the plaintext secrets never touch the disk, and
|
||||
the child process can only read the secrets once. In contexts where this won't
|
||||
work, such as platforms where FIFOs are not available or secret files need to be
|
||||
available to the child process longer term, the ``--no-fifo`` flag can be used
|
||||
to instruct ``sops`` to use a traditional temporary file that will get cleaned
|
||||
up once the process is finished executing. ``exec-file`` behaves similar to
|
||||
``find(1)`` in that ``{}`` is used as a placeholder in the command which will be
|
||||
substituted with the temporary file path (whether a FIFO or an actual file).
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# operating on the same file as before, but as a file this time
|
||||
$ sops exec-file out.json 'echo your temporary file: {}; cat {}'
|
||||
your temporary file: /tmp/.sops894650499/tmp-file
|
||||
{
|
||||
"database_password": "jf48t9wfw094gf4nhdf023r",
|
||||
"AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
|
||||
"AWS_SECRET_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
}
|
||||
|
||||
# launch a shell with a variable TMPFILE pointing to the temporary file
|
||||
$ sops exec-file --no-fifo out.json 'TMPFILE={} sh'
|
||||
sh-3.2$ echo $TMPFILE
|
||||
/tmp/.sops506055069/tmp-file291138648
|
||||
sh-3.2$ cat $TMPFILE
|
||||
{
|
||||
"database_password": "jf48t9wfw094gf4nhdf023r",
|
||||
"AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
|
||||
"AWS_SECRET_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
}
|
||||
sh-3.2$ ./program --config $TMPFILE
|
||||
sh-3.2$ exit
|
||||
|
||||
# try to open the temporary file from earlier
|
||||
$ cat /tmp/.sops506055069/tmp-file291138648
|
||||
cat: /tmp/.sops506055069/tmp-file291138648: No such file or directory
|
||||
|
||||
Additionally, both ``exec-env`` and ``exec-file`` support dropping privileges
|
||||
before executing the new program via the ``--user <username>`` flag. This is
|
||||
particularly useful in cases where the encrypted file is only readable by root,
|
||||
but the target program does not need root privileges to function. This flag
|
||||
should be used where possible for added security.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# the encrypted file can't be read by the current user
|
||||
$ cat out.json
|
||||
cat: out.json: Permission denied
|
||||
|
||||
# execute sops as root, decrypt secrets, then drop privileges
|
||||
$ sudo sops exec-env --user nobody out.json 'sh'
|
||||
sh-3.2$ echo $database_password
|
||||
jf48t9wfw094gf4nhdf023r
|
||||
|
||||
# dropped privileges, still can't load the original file
|
||||
sh-3.2$ id
|
||||
uid=4294967294(nobody) gid=4294967294(nobody) groups=4294967294(nobody)
|
||||
sh-3.2$ cat out.json
|
||||
cat: out.json: Permission denied
|
||||
|
||||
Using the publish command
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
107
cmd/sops/main.go
107
cmd/sops/main.go
@@ -19,6 +19,7 @@ import (
|
||||
"go.mozilla.org/sops/azkv"
|
||||
"go.mozilla.org/sops/cmd/sops/codes"
|
||||
"go.mozilla.org/sops/cmd/sops/common"
|
||||
"go.mozilla.org/sops/cmd/sops/subcommand/exec"
|
||||
"go.mozilla.org/sops/cmd/sops/subcommand/groups"
|
||||
keyservicecmd "go.mozilla.org/sops/cmd/sops/subcommand/keyservice"
|
||||
publishcmd "go.mozilla.org/sops/cmd/sops/subcommand/publish"
|
||||
@@ -106,6 +107,111 @@ func main() {
|
||||
For more information, see the README at github.com/mozilla/sops`
|
||||
app.EnableBashCompletion = true
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "exec-env",
|
||||
Usage: "execute a command with decrypted values inserted into the environment",
|
||||
ArgsUsage: "[file to decrypt] [command to run]",
|
||||
Flags: append([]cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "background",
|
||||
Usage: "background the process and don't wait for it to complete",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user",
|
||||
Usage: "the user to run the command as",
|
||||
},
|
||||
}, keyserviceFlags...),
|
||||
Action: func(c *cli.Context) error {
|
||||
if len(c.Args()) != 2 {
|
||||
return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
|
||||
}
|
||||
|
||||
fileName := c.Args()[0]
|
||||
command := c.Args()[1]
|
||||
|
||||
inputStore := inputStore(c, fileName)
|
||||
|
||||
|
||||
svcs := keyservices(c)
|
||||
opts := decryptOpts{
|
||||
OutputStore: &dotenv.Store{},
|
||||
InputStore: inputStore,
|
||||
InputPath: fileName,
|
||||
Cipher: aes.NewCipher(),
|
||||
KeyServices: svcs,
|
||||
IgnoreMAC: c.Bool("ignore-mac"),
|
||||
}
|
||||
|
||||
output, err := decrypt(opts)
|
||||
if err != nil {
|
||||
return toExitError(err)
|
||||
}
|
||||
|
||||
exec.ExecWithEnv(exec.ExecOpts{
|
||||
Command: command,
|
||||
Plaintext: output,
|
||||
Background: c.Bool("background"),
|
||||
User: c.String("user"),
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "exec-file",
|
||||
Usage: "execute a command with the decrypted contents as a temporary file",
|
||||
ArgsUsage: "[file to decrypt] [command to run]",
|
||||
Flags: append([]cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "background",
|
||||
Usage: "background the process and don't wait for it to complete",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-fifo",
|
||||
Usage: "use a regular file instead of a fifo to temporarily hold the decrypted contents",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user",
|
||||
Usage: "the user to run the command as",
|
||||
},
|
||||
}, keyserviceFlags...),
|
||||
Action: func(c *cli.Context) error {
|
||||
if len(c.Args()) != 2 {
|
||||
return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
|
||||
}
|
||||
|
||||
fileName := c.Args()[0]
|
||||
command := c.Args()[1]
|
||||
|
||||
inputStore := inputStore(c, fileName)
|
||||
outputStore := outputStore(c, fileName)
|
||||
|
||||
svcs := keyservices(c)
|
||||
opts := decryptOpts{
|
||||
OutputStore: outputStore,
|
||||
InputStore: inputStore,
|
||||
InputPath: fileName,
|
||||
Cipher: aes.NewCipher(),
|
||||
KeyServices: svcs,
|
||||
IgnoreMAC: c.Bool("ignore-mac"),
|
||||
}
|
||||
|
||||
output, err := decrypt(opts)
|
||||
if err != nil {
|
||||
return toExitError(err)
|
||||
}
|
||||
|
||||
exec.ExecWithFile(exec.ExecOpts{
|
||||
Command: command,
|
||||
Plaintext: output,
|
||||
Background: c.Bool("background"),
|
||||
Fifo: !c.Bool("no-fifo"),
|
||||
User: c.String("user"),
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "publish",
|
||||
Usage: "Publish sops file to a configured destination",
|
||||
@@ -706,6 +812,7 @@ func main() {
|
||||
}
|
||||
|
||||
outputFile := os.Stdout
|
||||
|
||||
if c.String("output") != "" {
|
||||
file, err := os.Create(c.String("output"))
|
||||
if err != nil {
|
||||
|
||||
152
cmd/sops/subcommand/exec/exec.go
Normal file
152
cmd/sops/subcommand/exec/exec.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"syscall"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
type ExecOpts struct {
|
||||
Command string
|
||||
Plaintext []byte
|
||||
Background bool
|
||||
Fifo bool
|
||||
User string
|
||||
}
|
||||
|
||||
func WritePipe(pipe string, contents []byte) {
|
||||
handle, err := os.OpenFile(pipe, os.O_WRONLY, 0600)
|
||||
|
||||
if err != nil {
|
||||
os.Remove(pipe)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
handle.Write(contents)
|
||||
handle.Close()
|
||||
}
|
||||
|
||||
func GetPipe(dir string) string {
|
||||
tmpfn := filepath.Join(dir, "tmp-file")
|
||||
err := syscall.Mkfifo(tmpfn, 0600)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return tmpfn
|
||||
}
|
||||
|
||||
func GetFile(dir string) *os.File {
|
||||
handle, err := ioutil.TempFile(dir, "tmp-file")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
func SwitchUser(username string) {
|
||||
user, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
uid, _ := strconv.Atoi(user.Uid)
|
||||
|
||||
err = syscall.Setgid(uid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = syscall.Setuid(uid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = syscall.Setreuid(uid, uid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = syscall.Setregid(uid, uid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExecWithFile(opts ExecOpts) {
|
||||
if opts.User != "" {
|
||||
SwitchUser(opts.User)
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir("/tmp/", ".sops")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var filename string
|
||||
if opts.Fifo {
|
||||
// fifo handling needs to be async, even opening to write
|
||||
// will block if there is no reader present
|
||||
filename = GetPipe(dir)
|
||||
go WritePipe(filename, opts.Plaintext)
|
||||
} else {
|
||||
handle := GetFile(dir)
|
||||
handle.Write(opts.Plaintext)
|
||||
handle.Close()
|
||||
filename = handle.Name()
|
||||
}
|
||||
|
||||
placeholdered := strings.Replace(opts.Command, "{}", filename, -1)
|
||||
cmd := exec.Command("/bin/sh", "-c", placeholdered)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
if opts.Background {
|
||||
cmd.Start()
|
||||
} else {
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func ExecWithEnv(opts ExecOpts) {
|
||||
if opts.User != "" {
|
||||
SwitchUser(opts.User)
|
||||
}
|
||||
|
||||
env := os.Environ()
|
||||
lines := bytes.Split(opts.Plaintext, []byte("\n"))
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
env = append(env, string(line))
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/sh", "-c", opts.Command)
|
||||
cmd.Env = env
|
||||
|
||||
if opts.Background {
|
||||
cmd.Start()
|
||||
} else {
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user