1
0
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:
Omar
2019-09-09 16:49:05 -04:00
committed by AJ Bahnken
parent ebf0705182
commit f103af7237
3 changed files with 357 additions and 0 deletions

View File

@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -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 {

View 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()
}
}