mirror of
https://github.com/getsops/sops.git
synced 2026-02-05 12:45:21 +01:00
2544 lines
84 KiB
Go
2544 lines
84 KiB
Go
package main // import "github.com/getsops/sops/v3/cmd/sops"
|
|
|
|
import (
|
|
"context"
|
|
encodingjson "encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
osExec "os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
"github.com/getsops/sops/v3"
|
|
"github.com/getsops/sops/v3/aes"
|
|
"github.com/getsops/sops/v3/age"
|
|
_ "github.com/getsops/sops/v3/audit"
|
|
"github.com/getsops/sops/v3/azkv"
|
|
"github.com/getsops/sops/v3/cmd/sops/codes"
|
|
"github.com/getsops/sops/v3/cmd/sops/common"
|
|
"github.com/getsops/sops/v3/cmd/sops/subcommand/exec"
|
|
filestatuscmd "github.com/getsops/sops/v3/cmd/sops/subcommand/filestatus"
|
|
"github.com/getsops/sops/v3/cmd/sops/subcommand/groups"
|
|
keyservicecmd "github.com/getsops/sops/v3/cmd/sops/subcommand/keyservice"
|
|
publishcmd "github.com/getsops/sops/v3/cmd/sops/subcommand/publish"
|
|
"github.com/getsops/sops/v3/cmd/sops/subcommand/updatekeys"
|
|
"github.com/getsops/sops/v3/config"
|
|
"github.com/getsops/sops/v3/gcpkms"
|
|
"github.com/getsops/sops/v3/hcvault"
|
|
"github.com/getsops/sops/v3/keys"
|
|
"github.com/getsops/sops/v3/keyservice"
|
|
"github.com/getsops/sops/v3/kms"
|
|
"github.com/getsops/sops/v3/logging"
|
|
"github.com/getsops/sops/v3/pgp"
|
|
"github.com/getsops/sops/v3/stores"
|
|
"github.com/getsops/sops/v3/stores/dotenv"
|
|
"github.com/getsops/sops/v3/stores/json"
|
|
"github.com/getsops/sops/v3/version"
|
|
)
|
|
|
|
var (
|
|
log *logrus.Logger
|
|
|
|
// Whether the config file warning was already shown to the user.
|
|
// Used and set by findConfigFile().
|
|
showedConfigFileWarning bool
|
|
)
|
|
|
|
func init() {
|
|
log = logging.NewLogger("CMD")
|
|
}
|
|
|
|
func warnMoreThanOnePositionalArgument(c *cli.Context) {
|
|
if c.NArg() > 1 {
|
|
log.Warn("More than one positional argument provided. Only the first one will be used!")
|
|
potentialFlag := ""
|
|
for i, value := range c.Args() {
|
|
if i > 0 && strings.HasPrefix(value, "-") {
|
|
potentialFlag = value
|
|
}
|
|
}
|
|
if potentialFlag != "" {
|
|
log.Warn(fmt.Sprintf("Note that one of the ignored positional argument is %q, which looks like a flag. Flags must always be provided before the first positional argument!", potentialFlag))
|
|
}
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
cli.VersionPrinter = version.PrintVersion
|
|
app := cli.NewApp()
|
|
|
|
keyserviceFlags := []cli.Flag{
|
|
cli.BoolTFlag{
|
|
Name: "enable-local-keyservice",
|
|
Usage: "use local key service",
|
|
EnvVar: "SOPS_ENABLE_LOCAL_KEYSERVICE",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "keyservice",
|
|
Usage: "Specify the key services to use in addition to the local one. Can be specified more than once. Syntax: protocol://address. Example: tcp://myserver.com:5000",
|
|
EnvVar: "SOPS_KEYSERVICE",
|
|
},
|
|
}
|
|
app.Name = "sops"
|
|
app.Usage = "sops - encrypted file editor with AWS KMS, GCP KMS, Azure Key Vault, age, and GPG support"
|
|
app.ArgsUsage = "sops [options] file"
|
|
app.Version = version.Version
|
|
app.Authors = []cli.Author{
|
|
{Name: "CNCF Maintainers"},
|
|
}
|
|
app.UsageText = `sops is an editor of encrypted files that supports AWS KMS, GCP, AZKV,
|
|
PGP, and Age
|
|
|
|
To encrypt or decrypt a document with AWS KMS, specify the KMS ARN
|
|
in the -k flag or in the SOPS_KMS_ARN environment variable.
|
|
(you need valid credentials in ~/.aws/credentials or in your env)
|
|
|
|
To encrypt or decrypt a document with GCP KMS, specify the
|
|
GCP KMS resource ID in the --gcp-kms flag or in the SOPS_GCP_KMS_IDS
|
|
environment variable.
|
|
(You need to setup Google application default credentials. See
|
|
https://developers.google.com/identity/protocols/application-default-credentials)
|
|
|
|
|
|
To encrypt or decrypt a document with HashiCorp Vault's Transit Secret
|
|
Engine, specify the Vault key URI name in the --hc-vault-transit flag
|
|
or in the SOPS_VAULT_URIS environment variable (for example
|
|
https://vault.example.org:8200/v1/transit/keys/dev, where
|
|
'https://vault.example.org:8200' is the vault server, 'transit' the
|
|
enginePath, and 'dev' is the name of the key).
|
|
(You need to enable the Transit Secrets Engine in Vault. See
|
|
https://www.vaultproject.io/docs/secrets/transit/index.html)
|
|
|
|
To encrypt or decrypt a document with Azure Key Vault, specify the
|
|
Azure Key Vault key URL in the --azure-kv flag or in the
|
|
SOPS_AZURE_KEYVAULT_URL environment variable.
|
|
(Authentication is based on environment variables, see
|
|
https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
|
|
The user/sp needs the key/encrypt and key/decrypt permissions.)
|
|
|
|
To encrypt or decrypt using age, specify the recipient in the -a flag,
|
|
or in the SOPS_AGE_RECIPIENTS environment variable.
|
|
|
|
To encrypt or decrypt using PGP, specify the PGP fingerprint in the
|
|
-p flag or in the SOPS_PGP_FP environment variable.
|
|
|
|
To use multiple KMS or PGP keys, separate them by commas. For example:
|
|
$ sops -p "10F2...0A, 85D...B3F21" file.yaml
|
|
|
|
The -p, -k, --gcp-kms, --hc-vault-transit, and --azure-kv flags are only
|
|
used to encrypt new documents. Editing or decrypting existing documents
|
|
can be done with "sops file" or "sops decrypt file" respectively. The KMS and
|
|
PGP keys listed in the encrypted documents are used then. To manage master
|
|
keys in existing documents, use the "add-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}"
|
|
and "rm-{kms,pgp,gcp-kms,azure-kv,hc-vault-transit}" flags with --rotate
|
|
or the updatekeys command.
|
|
|
|
To use a different GPG binary than the one in your PATH, set SOPS_GPG_EXEC.
|
|
|
|
To select a different editor than the default (vim), set SOPS_EDITOR or
|
|
EDITOR.
|
|
|
|
Note that flags must always be provided before the filename to operate on.
|
|
Otherwise, they will be ignored.
|
|
|
|
For more information, see the README at https://github.com/getsops/sops`
|
|
app.EnableBashCompletion = true
|
|
app.Commands = []cli.Command{
|
|
{
|
|
Name: "completion",
|
|
Usage: "Generate shell completion scripts",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "bash",
|
|
Usage: fmt.Sprintf("Generate bash completions. To load completions: `$ source <(%s completion bash)`", app.Name),
|
|
Action: func(c *cli.Context) error {
|
|
fmt.Fprint(c.App.Writer, GenBashCompletion(app.Name))
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "zsh",
|
|
Usage: fmt.Sprintf("Generate zsh completions. To load completions: `$ source <(%s completion zsh)`", app.Name),
|
|
Action: func(c *cli.Context) error {
|
|
fmt.Fprint(c.App.Writer, GenZshCompletion(app.Name))
|
|
return nil
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
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 (DEPRECATED)",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "pristine",
|
|
Usage: "insert only the decrypted values into the environment without forwarding existing environment variables",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "user",
|
|
Usage: "the user to run the command as",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "same-process",
|
|
Usage: "run command in the current process instead of in a child process",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 2 {
|
|
return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
|
|
}
|
|
|
|
fileName := c.Args()[0]
|
|
command := c.Args()[1]
|
|
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
opts := decryptOpts{
|
|
OutputStore: &dotenv.Store{},
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
}
|
|
|
|
if c.Bool("background") {
|
|
log.Warn("exec-env's --background option is deprecated and will be removed in a future version of sops")
|
|
|
|
if c.Bool("same-process") {
|
|
return common.NewExitError("Error: The --same-process flag cannot be used with --background", codes.ErrorConflictingParameters)
|
|
}
|
|
}
|
|
|
|
tree, err := decryptTree(opts)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
var env []string
|
|
for _, item := range tree.Branches[0] {
|
|
if dotenv.IsComplexValue(item.Value) {
|
|
return cli.NewExitError(fmt.Errorf("cannot use complex value in environment: %s", item.Value), codes.ErrorGeneric)
|
|
}
|
|
if _, ok := item.Key.(sops.Comment); ok {
|
|
continue
|
|
}
|
|
key, ok := item.Key.(string)
|
|
if !ok {
|
|
return cli.NewExitError(fmt.Errorf("cannot use non-string keys in environment, got %T", item.Key), codes.ErrorGeneric)
|
|
}
|
|
if strings.Contains(key, "=") {
|
|
return cli.NewExitError(fmt.Errorf("cannot use keys with '=' in environment: %s", key), codes.ErrorGeneric)
|
|
}
|
|
value, ok := item.Value.(string)
|
|
if !ok {
|
|
value = stores.ValToString(item.Value)
|
|
}
|
|
env = append(env, fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
|
|
if err := exec.ExecWithEnv(exec.ExecOpts{
|
|
Command: command,
|
|
Plaintext: []byte{},
|
|
Background: c.Bool("background"),
|
|
Pristine: c.Bool("pristine"),
|
|
User: c.String("user"),
|
|
SameProcess: c.Bool("same-process"),
|
|
Env: env,
|
|
}); err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
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 (DEPRECATED)",
|
|
},
|
|
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",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "filename",
|
|
Usage: fmt.Sprintf("filename for the temporarily file (default: %s)", exec.FallbackFilename),
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 2 {
|
|
return common.NewExitError(fmt.Errorf("error: missing file to decrypt"), codes.ErrorGeneric)
|
|
}
|
|
|
|
fileName := c.Args()[0]
|
|
command := c.Args()[1]
|
|
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
opts := decryptOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
}
|
|
|
|
output, err := decrypt(opts)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
if c.Bool("background") {
|
|
log.Warn("exec-file's --background option is deprecated and will be removed in a future version of sops")
|
|
}
|
|
|
|
if err := exec.ExecWithFile(exec.ExecOpts{
|
|
Command: command,
|
|
Plaintext: output,
|
|
Background: c.Bool("background"),
|
|
Fifo: !c.Bool("no-fifo"),
|
|
User: c.String("user"),
|
|
Filename: c.String("filename"),
|
|
}); err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "publish",
|
|
Usage: "Publish sops file or directory to a configured destination",
|
|
ArgsUsage: `file`,
|
|
Flags: append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "yes, y",
|
|
Usage: `pre-approve all changes and run non-interactively`,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "omit-extensions",
|
|
Usage: "Omit file extensions in destination path when publishing sops file to configured destinations",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "recursive",
|
|
Usage: "If the source path is a directory, publish all its content recursively",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "verbose",
|
|
Usage: "Enable verbose logging output",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") || c.GlobalBool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
var configPath string
|
|
var err error
|
|
if c.GlobalString("config") != "" {
|
|
configPath = c.GlobalString("config")
|
|
} else {
|
|
configPath, err = findConfigFile()
|
|
if err != nil {
|
|
return common.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
}
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
path := c.Args()[0]
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if info.IsDir() && !c.Bool("recursive") {
|
|
return fmt.Errorf("can't operate on a directory without --recursive flag.")
|
|
}
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
err = filepath.Walk(path, func(subPath string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if !info.IsDir() {
|
|
inputStore, err := inputStore(c, subPath)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
err = publishcmd.Run(publishcmd.Opts{
|
|
ConfigPath: configPath,
|
|
InputPath: subPath,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: keyservices(c),
|
|
DecryptionOrder: order,
|
|
InputStore: inputStore,
|
|
Interactive: !c.Bool("yes"),
|
|
OmitExtensions: c.Bool("omit-extensions"),
|
|
Recursive: c.Bool("recursive"),
|
|
})
|
|
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
|
|
return cliErr
|
|
} else if err != nil {
|
|
return common.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "keyservice",
|
|
Usage: "start a SOPS key service server",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "network, net",
|
|
Usage: "network to listen on, e.g. 'tcp' or 'unix'",
|
|
Value: "tcp",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "address, addr",
|
|
Usage: "address to listen on, e.g. '127.0.0.1:5000' or '/tmp/sops.sock'",
|
|
Value: "127.0.0.1:5000",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "prompt",
|
|
Usage: "Prompt user to confirm every incoming request",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "verbose",
|
|
Usage: "Enable verbose logging output",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") || c.GlobalBool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
err := keyservicecmd.Run(keyservicecmd.Opts{
|
|
Network: c.String("network"),
|
|
Address: c.String("address"),
|
|
Prompt: c.Bool("prompt"),
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Error running keyservice: %s", err)
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "filestatus",
|
|
Usage: "check the status of the file, returning encryption status",
|
|
ArgsUsage: `file`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
|
|
fileName := c.Args()[0]
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
opts := filestatuscmd.Opts{
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
}
|
|
|
|
status, err := filestatuscmd.FileStatus(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
json, err := encodingjson.Marshal(status)
|
|
if err != nil {
|
|
return common.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
|
|
fmt.Println(string(json))
|
|
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "groups",
|
|
Usage: "modify the groups on a SOPS file",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "add",
|
|
Usage: "add a new group to a SOPS file",
|
|
Flags: append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "file, f",
|
|
Usage: "the file to add the group to",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "pgp",
|
|
Usage: "the PGP fingerprints the new group should contain. Can be specified more than once",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "kms",
|
|
Usage: "the KMS ARNs the new group should contain. Can be specified more than once",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "aws-profile",
|
|
Usage: "The AWS profile to use for requests to AWS",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "gcp-kms",
|
|
Usage: "the GCP KMS Resource ID the new group should contain. Can be specified more than once",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "azure-kv",
|
|
Usage: "the Azure Key Vault key URL the new group should contain. Can be specified more than once",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "hc-vault-transit",
|
|
Usage: "the full vault path to the key used to encrypt/decrypt. Make you choose and configure a key with encryption/decryption enabled (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev'). Can be specified more than once",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "age",
|
|
Usage: "the age recipient the new group should contain. Can be specified more than once",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encryption-context",
|
|
Usage: "comma separated list of KMS encryption context key:value pairs",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
pgpFps := c.StringSlice("pgp")
|
|
kmsArns := c.StringSlice("kms")
|
|
gcpKmses := c.StringSlice("gcp-kms")
|
|
vaultURIs := c.StringSlice("hc-vault-transit")
|
|
azkvs := c.StringSlice("azure-kv")
|
|
ageRecipients := c.StringSlice("age")
|
|
if c.NArg() != 0 {
|
|
return common.NewExitError(fmt.Errorf("error: no positional arguments allowed"), codes.ErrorGeneric)
|
|
}
|
|
var group sops.KeyGroup
|
|
for _, fp := range pgpFps {
|
|
group = append(group, pgp.NewMasterKeyFromFingerprint(fp))
|
|
}
|
|
for _, arn := range kmsArns {
|
|
group = append(group, kms.NewMasterKeyFromArn(arn, kms.ParseKMSContext(c.String("encryption-context")), c.String("aws-profile")))
|
|
}
|
|
for _, kms := range gcpKmses {
|
|
group = append(group, gcpkms.NewMasterKeyFromResourceID(kms))
|
|
}
|
|
for _, uri := range vaultURIs {
|
|
k, err := hcvault.NewMasterKeyFromURI(uri)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to add key")
|
|
continue
|
|
}
|
|
group = append(group, k)
|
|
}
|
|
for _, url := range azkvs {
|
|
k, err := azkv.NewMasterKeyFromURL(url)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to add key")
|
|
continue
|
|
}
|
|
group = append(group, k)
|
|
}
|
|
for _, recipient := range ageRecipients {
|
|
keys, err := age.MasterKeysFromRecipients(recipient)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to add key")
|
|
continue
|
|
}
|
|
for _, key := range keys {
|
|
group = append(group, key)
|
|
}
|
|
}
|
|
inputStore, err := inputStore(c, c.String("file"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, c.String("file"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
return groups.Add(groups.AddOpts{
|
|
InputPath: c.String("file"),
|
|
InPlace: c.Bool("in-place"),
|
|
InputStore: inputStore,
|
|
OutputStore: outputStore,
|
|
Group: group,
|
|
GroupThreshold: c.Int("shamir-secret-sharing-threshold"),
|
|
KeyServices: keyservices(c),
|
|
})
|
|
},
|
|
},
|
|
{
|
|
Name: "delete",
|
|
Usage: "delete a key group from a SOPS file",
|
|
Flags: append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "file, f",
|
|
Usage: "the file to add the group to",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
}, keyserviceFlags...),
|
|
ArgsUsage: `[index]`,
|
|
|
|
Action: func(c *cli.Context) error {
|
|
if c.NArg() != 1 {
|
|
return common.NewExitError(fmt.Errorf("error: exactly one positional argument (index) required"), codes.ErrorGeneric)
|
|
}
|
|
group, err := strconv.ParseUint(c.Args().First(), 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse [index] argument: %s", err)
|
|
}
|
|
|
|
inputStore, err := inputStore(c, c.String("file"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, c.String("file"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
return groups.Delete(groups.DeleteOpts{
|
|
InputPath: c.String("file"),
|
|
InPlace: c.Bool("in-place"),
|
|
InputStore: inputStore,
|
|
OutputStore: outputStore,
|
|
Group: uint(group),
|
|
GroupThreshold: c.Int("shamir-secret-sharing-threshold"),
|
|
KeyServices: keyservices(c),
|
|
})
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "updatekeys",
|
|
Usage: "update the keys of SOPS files using the config file",
|
|
ArgsUsage: `file`,
|
|
Flags: append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "yes, y",
|
|
Usage: `pre-approve all changes and run non-interactively`,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
var err error
|
|
var configPath string
|
|
if c.GlobalString("config") != "" {
|
|
configPath = c.GlobalString("config")
|
|
} else {
|
|
configPath, err = findConfigFile()
|
|
if err != nil {
|
|
return common.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
}
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
failedCounter := 0
|
|
for _, path := range c.Args() {
|
|
err := updatekeys.UpdateKeys(updatekeys.Opts{
|
|
InputPath: path,
|
|
ShamirThreshold: c.Int("shamir-secret-sharing-threshold"),
|
|
KeyServices: keyservices(c),
|
|
Interactive: !c.Bool("yes"),
|
|
ConfigPath: configPath,
|
|
InputType: c.String("input-type"),
|
|
})
|
|
|
|
if c.NArg() == 1 {
|
|
// a single argument was given, keep compatibility of the error
|
|
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
|
|
return cliErr
|
|
} else if err != nil {
|
|
return common.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
}
|
|
|
|
// multiple arguments given (patched functionality),
|
|
// finish updating of remaining files and fail afterwards
|
|
if err != nil {
|
|
failedCounter++
|
|
log.Error(err)
|
|
}
|
|
}
|
|
if failedCounter > 0 {
|
|
return common.NewExitError(fmt.Errorf("failed updating %d key(s)", failedCounter), codes.ErrorGeneric)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "decrypt",
|
|
Usage: "decrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
|
|
ArgsUsage: `[file]`,
|
|
Flags: append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "extract",
|
|
Usage: "extract a specific key or branch from the input document. Example: --extract '[\"somekey\"][0]'",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output",
|
|
Usage: "Save the output after decryption to the file specified",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ignore-mac",
|
|
Usage: "ignore Message Authentication Code during decryption",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "filename-override",
|
|
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Should be provided when reading from stdin.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
readFromStdin := c.NArg() == 0
|
|
if readFromStdin && c.Bool("in-place") {
|
|
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
if c.Bool("in-place") && c.String("output") != "" {
|
|
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
|
|
}
|
|
var fileName string
|
|
var err error
|
|
if !readFromStdin {
|
|
fileName, err = filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
|
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
|
|
}
|
|
}
|
|
fileNameOverride := c.String("filename-override")
|
|
if fileNameOverride == "" {
|
|
fileNameOverride = fileName
|
|
} else {
|
|
fileNameOverride, err = filepath.Abs(fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
var extract []interface{}
|
|
extract, err = parseTreePath(c.String("extract"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
|
|
}
|
|
output, err := decrypt(decryptOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
ReadFromStdin: readFromStdin,
|
|
Cipher: aes.NewCipher(),
|
|
Extract: extract,
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
})
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
if c.Bool("in-place") {
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
}
|
|
|
|
outputFile := os.Stdout
|
|
if c.String("output") != "" {
|
|
file, err := os.Create(c.String("output"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
outputFile = file
|
|
}
|
|
_, err = outputFile.Write(output)
|
|
return toExitError(err)
|
|
},
|
|
},
|
|
{
|
|
Name: "encrypt",
|
|
Usage: "encrypt a file, and output the results to stdout. If no filename is provided, stdin will be used.",
|
|
ArgsUsage: `[file]`,
|
|
Flags: append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output",
|
|
Usage: "Save the output after decryption to the file specified",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "kms, k",
|
|
Usage: "comma separated list of KMS ARNs",
|
|
EnvVar: "SOPS_KMS_ARN",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "aws-profile",
|
|
Usage: "The AWS profile to use for requests to AWS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "gcp-kms",
|
|
Usage: "comma separated list of GCP KMS resource IDs",
|
|
EnvVar: "SOPS_GCP_KMS_IDS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "azure-kv",
|
|
Usage: "comma separated list of Azure Key Vault URLs",
|
|
EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "hc-vault-transit",
|
|
Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
|
|
EnvVar: "SOPS_VAULT_URIS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "pgp, p",
|
|
Usage: "comma separated list of PGP fingerprints",
|
|
EnvVar: "SOPS_PGP_FP",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "age, a",
|
|
Usage: "comma separated list of age recipients",
|
|
EnvVar: "SOPS_AGE_RECIPIENTS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-suffix",
|
|
Usage: "override the unencrypted key suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-suffix",
|
|
Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-regex",
|
|
Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-regex",
|
|
Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encryption-context",
|
|
Usage: "comma separated list of KMS encryption context key:value pairs",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "filename-override",
|
|
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type. Required when reading from stdin.",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
readFromStdin := c.NArg() == 0
|
|
if readFromStdin {
|
|
if c.Bool("in-place") {
|
|
return common.NewExitError("Error: cannot use --in-place when reading from stdin", codes.ErrorConflictingParameters)
|
|
}
|
|
if c.String("filename-override") == "" {
|
|
return common.NewExitError("Error: must specify --filename-override when reading from stdin", codes.ErrorConflictingParameters)
|
|
}
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
if c.Bool("in-place") && c.String("output") != "" {
|
|
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
|
|
}
|
|
var fileName string
|
|
var err error
|
|
if !readFromStdin {
|
|
fileName, err = filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
|
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
|
|
}
|
|
}
|
|
fileNameOverride := c.String("filename-override")
|
|
if fileNameOverride == "" {
|
|
fileNameOverride = fileName
|
|
} else {
|
|
fileNameOverride, err = filepath.Abs(fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, nil)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err := encrypt(encryptOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
ReadFromStdin: readFromStdin,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
encryptConfig: encConfig,
|
|
})
|
|
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
if c.Bool("in-place") {
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
}
|
|
|
|
outputFile := os.Stdout
|
|
if c.String("output") != "" {
|
|
file, err := os.Create(c.String("output"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
outputFile = file
|
|
}
|
|
_, err = outputFile.Write(output)
|
|
return toExitError(err)
|
|
},
|
|
},
|
|
{
|
|
Name: "rotate",
|
|
Usage: "generate a new data encryption key and reencrypt all values with the new key",
|
|
ArgsUsage: `file`,
|
|
Flags: append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output",
|
|
Usage: "Save the output after decryption to the file specified",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encryption-context",
|
|
Usage: "comma separated list of KMS encryption context key:value pairs",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-gcp-kms",
|
|
Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-gcp-kms",
|
|
Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-azure-kv",
|
|
Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-azure-kv",
|
|
Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-kms",
|
|
Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-kms",
|
|
Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-hc-vault-transit",
|
|
Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-hc-vault-transit",
|
|
Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-age",
|
|
Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-age",
|
|
Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-pgp",
|
|
Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-pgp",
|
|
Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "filename-override",
|
|
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
if c.Bool("in-place") && c.String("output") != "" {
|
|
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
|
|
}
|
|
fileName, err := filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
|
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
|
|
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
|
|
return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use the `edit` subcommand instead.", fileName), codes.CannotChangeKeysFromNonExistentFile)
|
|
}
|
|
}
|
|
fileNameOverride := c.String("filename-override")
|
|
if fileNameOverride == "" {
|
|
fileNameOverride = fileName
|
|
} else {
|
|
fileNameOverride, err = filepath.Abs(fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err := rotate(rotateOpts)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
if c.Bool("in-place") {
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
}
|
|
|
|
outputFile := os.Stdout
|
|
if c.String("output") != "" {
|
|
file, err := os.Create(c.String("output"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
outputFile = file
|
|
}
|
|
_, err = outputFile.Write(output)
|
|
return toExitError(err)
|
|
},
|
|
},
|
|
{
|
|
Name: "edit",
|
|
Usage: "edit an encrypted file",
|
|
ArgsUsage: `file`,
|
|
Flags: append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "kms, k",
|
|
Usage: "comma separated list of KMS ARNs",
|
|
EnvVar: "SOPS_KMS_ARN",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "aws-profile",
|
|
Usage: "The AWS profile to use for requests to AWS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "gcp-kms",
|
|
Usage: "comma separated list of GCP KMS resource IDs",
|
|
EnvVar: "SOPS_GCP_KMS_IDS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "azure-kv",
|
|
Usage: "comma separated list of Azure Key Vault URLs",
|
|
EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "hc-vault-transit",
|
|
Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
|
|
EnvVar: "SOPS_VAULT_URIS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "pgp, p",
|
|
Usage: "comma separated list of PGP fingerprints",
|
|
EnvVar: "SOPS_PGP_FP",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "age, a",
|
|
Usage: "comma separated list of age recipients",
|
|
EnvVar: "SOPS_AGE_RECIPIENTS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-suffix",
|
|
Usage: "override the unencrypted key suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-suffix",
|
|
Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-regex",
|
|
Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-regex",
|
|
Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encryption-context",
|
|
Usage: "comma separated list of KMS encryption context key:value pairs",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "show-master-keys, s",
|
|
Usage: "display master encryption keys in the file during editing",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ignore-mac",
|
|
Usage: "ignore Message Authentication Code during decryption",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
fileName, err := filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
var output []byte
|
|
_, statErr := os.Stat(fileName)
|
|
fileExists := statErr == nil
|
|
opts := editOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
ShowMasterKeys: c.Bool("show-master-keys"),
|
|
}
|
|
if fileExists {
|
|
output, err = edit(opts)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
} else {
|
|
// File doesn't exist, edit the example file instead
|
|
encConfig, err := getEncryptConfig(c, fileName, inputStore, nil)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err = editExample(editExampleOpts{
|
|
editOpts: opts,
|
|
encryptConfig: encConfig,
|
|
})
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "set",
|
|
Usage: `set a specific key or branch in the input document. value must be a JSON encoded string, for example '/path/to/file ["somekey"][0] {"somevalue":true}', or a path if --value-file is used, or omitted if --value-stdin is used`,
|
|
ArgsUsage: `file index [ value ]`,
|
|
Flags: append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "value-file",
|
|
Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-stdin",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "value-stdin",
|
|
Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-file",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ignore-mac",
|
|
Usage: "ignore Message Authentication Code during decryption",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "idempotent",
|
|
Usage: "do nothing if the given index already has the given value",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
if c.Bool("value-file") && c.Bool("value-stdin") {
|
|
return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric)
|
|
}
|
|
if c.Bool("value-stdin") {
|
|
if c.NArg() != 2 {
|
|
return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified)
|
|
}
|
|
} else {
|
|
if c.NArg() != 3 {
|
|
return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified)
|
|
}
|
|
}
|
|
fileName, err := filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
path, err := parseTreePath(c.Args()[1])
|
|
if err != nil {
|
|
return common.NewExitError("Invalid set index format", codes.ErrorInvalidSetFormat)
|
|
}
|
|
|
|
var data string
|
|
if c.Bool("value-stdin") {
|
|
content, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
data = string(content)
|
|
} else if c.Bool("value-file") {
|
|
filename := c.Args()[2]
|
|
content, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
data = string(content)
|
|
} else {
|
|
data = c.Args()[2]
|
|
}
|
|
value, err := jsonValueToTreeInsertableValue(data)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, changed, err := set(setOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
Value: value,
|
|
TreePath: path,
|
|
})
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
if !changed && c.Bool("idempotent") {
|
|
log.Info("File not written due to no change")
|
|
return nil
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "unset",
|
|
Usage: `unset a specific key or branch in the input document.`,
|
|
ArgsUsage: `file index`,
|
|
Flags: append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ignore-mac",
|
|
Usage: "ignore Message Authentication Code during decryption",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "idempotent",
|
|
Usage: "do nothing if the given index does not exist",
|
|
},
|
|
}, keyserviceFlags...),
|
|
Action: func(c *cli.Context) error {
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
if c.NArg() != 2 {
|
|
return common.NewExitError("Error: no file specified, or index is missing", codes.NoFileSpecified)
|
|
}
|
|
fileName, err := filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileName)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
path, err := parseTreePath(c.Args()[1])
|
|
if err != nil {
|
|
return common.NewExitError("Invalid unset index format", codes.ErrorInvalidSetFormat)
|
|
}
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err := unset(unsetOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
TreePath: path,
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(*sops.SopsKeyNotFound); ok && c.Bool("idempotent") {
|
|
return nil
|
|
}
|
|
return toExitError(err)
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
app.Flags = append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "decrypt, d",
|
|
Usage: "decrypt a file and output the result to stdout",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "encrypt, e",
|
|
Usage: "encrypt a file and output the result to stdout",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "rotate, r",
|
|
Usage: "generate a new data encryption key and reencrypt all values with the new key",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "disable-version-check",
|
|
Usage: "do not check whether the current version is latest during --version",
|
|
EnvVar: "SOPS_DISABLE_VERSION_CHECK",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "check-for-updates",
|
|
Usage: "do check whether the current version is latest during --version",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "kms, k",
|
|
Usage: "comma separated list of KMS ARNs",
|
|
EnvVar: "SOPS_KMS_ARN",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "aws-profile",
|
|
Usage: "The AWS profile to use for requests to AWS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "gcp-kms",
|
|
Usage: "comma separated list of GCP KMS resource IDs",
|
|
EnvVar: "SOPS_GCP_KMS_IDS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "azure-kv",
|
|
Usage: "comma separated list of Azure Key Vault URLs",
|
|
EnvVar: "SOPS_AZURE_KEYVAULT_URLS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "hc-vault-transit",
|
|
Usage: "comma separated list of vault's key URI (e.g. 'https://vault.example.org:8200/v1/transit/keys/dev')",
|
|
EnvVar: "SOPS_VAULT_URIS",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "pgp, p",
|
|
Usage: "comma separated list of PGP fingerprints",
|
|
EnvVar: "SOPS_PGP_FP",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "age, a",
|
|
Usage: "comma separated list of age recipients",
|
|
EnvVar: "SOPS_AGE_RECIPIENTS",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "in-place, i",
|
|
Usage: "write output back to the same file instead of stdout",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "extract",
|
|
Usage: "extract a specific key or branch from the input document. Decrypt mode only. Example: --extract '[\"somekey\"][0]'",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "input-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output-type",
|
|
Usage: "currently json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "show-master-keys, s",
|
|
Usage: "display master encryption keys in the file during editing",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-gcp-kms",
|
|
Usage: "add the provided comma-separated list of GCP KMS key resource IDs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-gcp-kms",
|
|
Usage: "remove the provided comma-separated list of GCP KMS key resource IDs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-azure-kv",
|
|
Usage: "add the provided comma-separated list of Azure Key Vault key URLs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-azure-kv",
|
|
Usage: "remove the provided comma-separated list of Azure Key Vault key URLs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-kms",
|
|
Usage: "add the provided comma-separated list of KMS ARNs to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-kms",
|
|
Usage: "remove the provided comma-separated list of KMS ARNs from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-hc-vault-transit",
|
|
Usage: "add the provided comma-separated list of Vault's URI key to the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-hc-vault-transit",
|
|
Usage: "remove the provided comma-separated list of Vault's URI key from the list of master keys on the given file ( eg. https://vault.example.org:8200/v1/transit/keys/dev)",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-age",
|
|
Usage: "add the provided comma-separated list of age recipients fingerprints to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-age",
|
|
Usage: "remove the provided comma-separated list of age recipients from the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "add-pgp",
|
|
Usage: "add the provided comma-separated list of PGP fingerprints to the list of master keys on the given file",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "rm-pgp",
|
|
Usage: "remove the provided comma-separated list of PGP fingerprints from the list of master keys on the given file",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ignore-mac",
|
|
Usage: "ignore Message Authentication Code during decryption",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "mac-only-encrypted",
|
|
Usage: "compute MAC only over values which end up encrypted",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-suffix",
|
|
Usage: "override the unencrypted key suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-suffix",
|
|
Usage: "override the encrypted key suffix. When empty, all keys will be encrypted, unless otherwise marked with unencrypted-suffix.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-regex",
|
|
Usage: "set the unencrypted key regex. When specified, only keys matching the regex will be left unencrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-regex",
|
|
Usage: "set the encrypted key regex. When specified, only keys matching the regex will be encrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "unencrypted-comment-regex",
|
|
Usage: "set the unencrypted comment suffix. When specified, only keys that have comment matching the regex will be left unencrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encrypted-comment-regex",
|
|
Usage: "set the encrypted comment suffix. When specified, only keys that have comment matching the regex will be encrypted.",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "config",
|
|
Usage: "path to sops' config file. If set, sops will not search for the config file recursively.",
|
|
EnvVar: "SOPS_CONFIG",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "encryption-context",
|
|
Usage: "comma separated list of KMS encryption context key:value pairs",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "set",
|
|
Usage: `set a specific key or branch in the input document. value must be a json encoded string. (edit mode only). eg. --set '["somekey"][0] {"somevalue":true}'`,
|
|
},
|
|
cli.IntFlag{
|
|
Name: "shamir-secret-sharing-threshold",
|
|
Usage: "the number of master keys required to retrieve the data key with shamir",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "indent",
|
|
Usage: "the number of spaces to indent YAML or JSON encoded file",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "verbose",
|
|
Usage: "Enable verbose logging output",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "output",
|
|
Usage: "Save the output after encryption or decryption to the file specified",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "filename-override",
|
|
Usage: "Use this filename instead of the provided argument for loading configuration, and for determining input type and output type",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "decryption-order",
|
|
Usage: "comma separated list of decryption key types",
|
|
EnvVar: "SOPS_DECRYPTION_ORDER",
|
|
},
|
|
}, keyserviceFlags...)
|
|
|
|
app.Action = func(c *cli.Context) error {
|
|
isDecryptMode := c.Bool("decrypt")
|
|
isEncryptMode := c.Bool("encrypt")
|
|
isRotateMode := c.Bool("rotate")
|
|
isSetMode := c.String("set") != ""
|
|
isEditMode := !isEncryptMode && !isDecryptMode && !isRotateMode && !isSetMode
|
|
|
|
if c.Bool("verbose") {
|
|
logging.SetLevel(logrus.DebugLevel)
|
|
}
|
|
if c.NArg() < 1 {
|
|
return common.NewExitError("Error: no file specified", codes.NoFileSpecified)
|
|
}
|
|
warnMoreThanOnePositionalArgument(c)
|
|
if c.Bool("in-place") && c.String("output") != "" {
|
|
return common.NewExitError("Error: cannot operate on both --output and --in-place", codes.ErrorConflictingParameters)
|
|
}
|
|
fileName, err := filepath.Abs(c.Args()[0])
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
|
if c.String("add-kms") != "" || c.String("add-pgp") != "" || c.String("add-gcp-kms") != "" || c.String("add-hc-vault-transit") != "" || c.String("add-azure-kv") != "" || c.String("add-age") != "" ||
|
|
c.String("rm-kms") != "" || c.String("rm-pgp") != "" || c.String("rm-gcp-kms") != "" || c.String("rm-hc-vault-transit") != "" || c.String("rm-azure-kv") != "" || c.String("rm-age") != "" {
|
|
return common.NewExitError(fmt.Sprintf("Error: cannot add or remove keys on non-existent file %q, use `--kms` and `--pgp` instead.", fileName), codes.CannotChangeKeysFromNonExistentFile)
|
|
}
|
|
if isEncryptMode || isDecryptMode || isRotateMode {
|
|
return common.NewExitError(fmt.Sprintf("Error: cannot operate on non-existent file %q", fileName), codes.NoFileSpecified)
|
|
}
|
|
}
|
|
fileNameOverride := c.String("filename-override")
|
|
if fileNameOverride == "" {
|
|
fileNameOverride = fileName
|
|
} else {
|
|
fileNameOverride, err = filepath.Abs(fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
commandCount := 0
|
|
if isDecryptMode {
|
|
commandCount++
|
|
}
|
|
if isEncryptMode {
|
|
commandCount++
|
|
}
|
|
if isRotateMode {
|
|
commandCount++
|
|
}
|
|
if isSetMode {
|
|
commandCount++
|
|
}
|
|
if commandCount > 1 {
|
|
log.Warn("More than one command (--encrypt, --decrypt, --rotate, --set) has been specified. Only the changes made by the last one will be visible. Note that this behavior is deprecated and will cause an error eventually.")
|
|
}
|
|
|
|
// Load configuration here for backwards compatibility (error out in case of bad config files),
|
|
// but only when not just decrypting (https://github.com/getsops/sops/issues/868)
|
|
needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode
|
|
var config *config.Config
|
|
if needsCreationRule {
|
|
config, err = loadConfig(c, fileNameOverride, nil)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
inputStore, err := inputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
outputStore, err := outputStore(c, fileNameOverride)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
svcs := keyservices(c)
|
|
|
|
order, err := decryptionOrder(c.String("decryption-order"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
var output []byte
|
|
if isEncryptMode {
|
|
encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err = encrypt(encryptOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
encryptConfig: encConfig,
|
|
})
|
|
// While this check is also done below, the `err` in this scope shadows
|
|
// the `err` in the outer scope. **Only** do this in case --decrypt,
|
|
// --rotate-, and --set are not specified, though, to keep old behavior.
|
|
if err != nil && !isDecryptMode && !isRotateMode && !isSetMode {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
if isDecryptMode {
|
|
var extract []interface{}
|
|
extract, err = parseTreePath(c.String("extract"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
|
|
}
|
|
output, err = decrypt(decryptOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
Extract: extract,
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
})
|
|
}
|
|
if isRotateMode {
|
|
rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
output, err = rotate(rotateOpts)
|
|
// While this check is also done below, the `err` in this scope shadows
|
|
// the `err` in the outer scope
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
|
|
if isSetMode {
|
|
var path []interface{}
|
|
var value interface{}
|
|
path, value, err = extractSetArguments(c.String("set"))
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, _, err = set(setOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
Value: value,
|
|
TreePath: path,
|
|
})
|
|
}
|
|
|
|
if isEditMode {
|
|
_, statErr := os.Stat(fileName)
|
|
fileExists := statErr == nil
|
|
opts := editOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: order,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
ShowMasterKeys: c.Bool("show-master-keys"),
|
|
}
|
|
if fileExists {
|
|
output, err = edit(opts)
|
|
} else {
|
|
// File doesn't exist, edit the example file instead
|
|
encConfig, err := getEncryptConfig(c, fileNameOverride, inputStore, config)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
output, err = editExample(editExampleOpts{
|
|
editOpts: opts,
|
|
encryptConfig: encConfig,
|
|
})
|
|
// While this check is also done below, the `err` in this scope shadows
|
|
// the `err` in the outer scope
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
|
|
// We open the file *after* the operations on the tree have been
|
|
// executed to avoid truncating it when there's errors
|
|
if c.Bool("in-place") || isEditMode || isSetMode {
|
|
file, err := os.Create(fileName)
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
_, err = file.Write(output)
|
|
if err != nil {
|
|
return toExitError(err)
|
|
}
|
|
log.Info("File written successfully")
|
|
return nil
|
|
}
|
|
|
|
outputFile := os.Stdout
|
|
if c.String("output") != "" {
|
|
file, err := os.Create(c.String("output"))
|
|
if err != nil {
|
|
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
|
|
}
|
|
defer file.Close()
|
|
outputFile = file
|
|
}
|
|
_, err = outputFile.Write(output)
|
|
return toExitError(err)
|
|
}
|
|
err := app.Run(os.Args)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func getEncryptConfig(c *cli.Context, fileName string, inputStore common.Store, optionalConfig *config.Config) (encryptConfig, error) {
|
|
unencryptedSuffix := c.String("unencrypted-suffix")
|
|
encryptedSuffix := c.String("encrypted-suffix")
|
|
encryptedRegex := c.String("encrypted-regex")
|
|
unencryptedRegex := c.String("unencrypted-regex")
|
|
encryptedCommentRegex := c.String("encrypted-comment-regex")
|
|
unencryptedCommentRegex := c.String("unencrypted-comment-regex")
|
|
macOnlyEncrypted := c.Bool("mac-only-encrypted")
|
|
var err error
|
|
if optionalConfig == nil {
|
|
optionalConfig, err = loadConfig(c, fileName, nil)
|
|
if err != nil {
|
|
return encryptConfig{}, toExitError(err)
|
|
}
|
|
}
|
|
if optionalConfig != nil {
|
|
// command line options have precedence
|
|
if unencryptedSuffix == "" {
|
|
unencryptedSuffix = optionalConfig.UnencryptedSuffix
|
|
}
|
|
if encryptedSuffix == "" {
|
|
encryptedSuffix = optionalConfig.EncryptedSuffix
|
|
}
|
|
if encryptedRegex == "" {
|
|
encryptedRegex = optionalConfig.EncryptedRegex
|
|
}
|
|
if unencryptedRegex == "" {
|
|
unencryptedRegex = optionalConfig.UnencryptedRegex
|
|
}
|
|
if encryptedCommentRegex == "" {
|
|
encryptedCommentRegex = optionalConfig.EncryptedCommentRegex
|
|
}
|
|
if unencryptedCommentRegex == "" {
|
|
unencryptedCommentRegex = optionalConfig.UnencryptedCommentRegex
|
|
}
|
|
if !macOnlyEncrypted {
|
|
macOnlyEncrypted = optionalConfig.MACOnlyEncrypted
|
|
}
|
|
}
|
|
|
|
isSingleValueStore := false
|
|
if svs, ok := inputStore.(sops.SingleValueStore); ok {
|
|
isSingleValueStore = svs.IsSingleValueStore()
|
|
}
|
|
|
|
if isSingleValueStore {
|
|
// Warn about settings that potentially disable encryption of the single key.
|
|
if unencryptedSuffix != "" {
|
|
log.Warn(fmt.Sprintf("Using an unencrypted suffix does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name()))
|
|
}
|
|
if encryptedSuffix != "" {
|
|
log.Warn(fmt.Sprintf("Using an encrypted suffix does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name()))
|
|
}
|
|
if encryptedRegex != "" {
|
|
log.Warn(fmt.Sprintf("Using an encrypted regex does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name()))
|
|
}
|
|
if unencryptedRegex != "" {
|
|
log.Warn(fmt.Sprintf("Using an unencrypted regex does not make sense with the input store (the %s store produces one key that should always be encrypted) and will be ignored.", inputStore.Name()))
|
|
}
|
|
if encryptedCommentRegex != "" {
|
|
log.Warn(fmt.Sprintf("Using an encrypted comment regex does not make sense with the input store (the %s store never produces comments) and will be ignored.", inputStore.Name()))
|
|
}
|
|
// Do not warn about unencryptedCommentRegex and macOnlyEncrypted since they cannot have any effect.
|
|
unencryptedSuffix = ""
|
|
encryptedSuffix = ""
|
|
encryptedRegex = ""
|
|
unencryptedRegex = ""
|
|
encryptedCommentRegex = ""
|
|
unencryptedCommentRegex = ""
|
|
macOnlyEncrypted = false
|
|
}
|
|
|
|
cryptRuleCount := 0
|
|
if unencryptedSuffix != "" {
|
|
cryptRuleCount++
|
|
}
|
|
if encryptedSuffix != "" {
|
|
cryptRuleCount++
|
|
}
|
|
if encryptedRegex != "" {
|
|
cryptRuleCount++
|
|
}
|
|
if unencryptedRegex != "" {
|
|
cryptRuleCount++
|
|
}
|
|
if encryptedCommentRegex != "" {
|
|
cryptRuleCount++
|
|
}
|
|
if unencryptedCommentRegex != "" {
|
|
cryptRuleCount++
|
|
}
|
|
|
|
if cryptRuleCount > 1 {
|
|
return encryptConfig{}, common.NewExitError("Error: cannot use more than one of encrypted_suffix, unencrypted_suffix, encrypted_regex, unencrypted_regex, encrypted_comment_regex, or unencrypted_comment_regex in the same file", codes.ErrorConflictingParameters)
|
|
}
|
|
|
|
// only supply the default UnencryptedSuffix when EncryptedSuffix, EncryptedRegex, and others are not provided
|
|
if cryptRuleCount == 0 && !isSingleValueStore {
|
|
unencryptedSuffix = sops.DefaultUnencryptedSuffix
|
|
}
|
|
|
|
var groups []sops.KeyGroup
|
|
groups, err = keyGroups(c, fileName, optionalConfig)
|
|
if err != nil {
|
|
return encryptConfig{}, err
|
|
}
|
|
|
|
var threshold int
|
|
threshold, err = shamirThreshold(c, fileName, optionalConfig)
|
|
if err != nil {
|
|
return encryptConfig{}, err
|
|
}
|
|
|
|
return encryptConfig{
|
|
UnencryptedSuffix: unencryptedSuffix,
|
|
EncryptedSuffix: encryptedSuffix,
|
|
UnencryptedRegex: unencryptedRegex,
|
|
EncryptedRegex: encryptedRegex,
|
|
UnencryptedCommentRegex: unencryptedCommentRegex,
|
|
EncryptedCommentRegex: encryptedCommentRegex,
|
|
MACOnlyEncrypted: macOnlyEncrypted,
|
|
KeyGroups: groups,
|
|
GroupThreshold: threshold,
|
|
}, nil
|
|
}
|
|
|
|
func getMasterKeys(c *cli.Context, kmsEncryptionContext map[string]*string, kmsOptionName string, pgpOptionName string, gcpKmsOptionName string, azureKvOptionName string, hcVaultTransitOptionName string, ageOptionName string) ([]keys.MasterKey, error) {
|
|
var masterKeys []keys.MasterKey
|
|
for _, k := range kms.MasterKeysFromArnString(c.String(kmsOptionName), kmsEncryptionContext, c.String("aws-profile")) {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
for _, k := range pgp.MasterKeysFromFingerprintString(c.String(pgpOptionName)) {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
for _, k := range gcpkms.MasterKeysFromResourceIDString(c.String(gcpKmsOptionName)) {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
azureKeys, err := azkv.MasterKeysFromURLs(c.String(azureKvOptionName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range azureKeys {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
hcVaultKeys, err := hcvault.NewMasterKeysFromURIs(c.String(hcVaultTransitOptionName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range hcVaultKeys {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
ageKeys, err := age.MasterKeysFromRecipients(c.String(ageOptionName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range ageKeys {
|
|
masterKeys = append(masterKeys, k)
|
|
}
|
|
return masterKeys, nil
|
|
}
|
|
|
|
func getRotateOpts(c *cli.Context, fileName string, inputStore common.Store, outputStore common.Store, svcs []keyservice.KeyServiceClient, decryptionOrder []string) (rotateOpts, error) {
|
|
kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
|
|
addMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "add-kms", "add-pgp", "add-gcp-kms", "add-azure-kv", "add-hc-vault-transit", "add-age")
|
|
if err != nil {
|
|
return rotateOpts{}, err
|
|
}
|
|
rmMasterKeys, err := getMasterKeys(c, kmsEncryptionContext, "rm-kms", "rm-pgp", "rm-gcp-kms", "rm-azure-kv", "rm-hc-vault-transit", "rm-age")
|
|
if err != nil {
|
|
return rotateOpts{}, err
|
|
}
|
|
return rotateOpts{
|
|
OutputStore: outputStore,
|
|
InputStore: inputStore,
|
|
InputPath: fileName,
|
|
Cipher: aes.NewCipher(),
|
|
KeyServices: svcs,
|
|
DecryptionOrder: decryptionOrder,
|
|
IgnoreMAC: c.Bool("ignore-mac"),
|
|
AddMasterKeys: addMasterKeys,
|
|
RemoveMasterKeys: rmMasterKeys,
|
|
}, nil
|
|
}
|
|
|
|
func toExitError(err error) error {
|
|
if cliErr, ok := err.(*cli.ExitError); ok && cliErr != nil {
|
|
return cliErr
|
|
} else if execErr, ok := err.(*osExec.ExitError); ok && execErr != nil {
|
|
return cli.NewExitError(err, execErr.ExitCode())
|
|
} else if err != nil {
|
|
return cli.NewExitError(err, codes.ErrorGeneric)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) {
|
|
if c.Bool("enable-local-keyservice") {
|
|
svcs = append(svcs, keyservice.NewLocalClient())
|
|
}
|
|
uris := c.StringSlice("keyservice")
|
|
for _, uri := range uris {
|
|
url, err := url.Parse(uri)
|
|
if err != nil {
|
|
log.WithField("uri", uri).
|
|
Warnf("Error parsing URI for keyservice, skipping")
|
|
continue
|
|
}
|
|
addr := url.Host
|
|
addrToUse := addr
|
|
opts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
}
|
|
if url.Scheme == "unix" {
|
|
addr = url.Path
|
|
addrToUse = uri
|
|
} else {
|
|
opts = append(opts,
|
|
grpc.WithContextDialer(
|
|
func(ctx context.Context, addr string) (net.Conn, error) {
|
|
return (&net.Dialer{}).DialContext(ctx, url.Scheme, addr)
|
|
},
|
|
))
|
|
}
|
|
log.WithField(
|
|
"address",
|
|
fmt.Sprintf("%s://%s", url.Scheme, addr),
|
|
).Infof("Connecting to key service")
|
|
conn, err := grpc.NewClient(addrToUse, opts...)
|
|
if err != nil {
|
|
log.Fatalf("failed to listen: %v", err)
|
|
}
|
|
svcs = append(svcs, keyservice.NewKeyServiceClient(conn))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Wrapper of config.LookupConfigFile that takes care of handling the returned warning.
|
|
func findConfigFile() (string, error) {
|
|
result, err := config.LookupConfigFile(".")
|
|
if len(result.Warning) > 0 && !showedConfigFileWarning {
|
|
showedConfigFileWarning = true
|
|
log.Warn(result.Warning)
|
|
}
|
|
return result.Path, err
|
|
}
|
|
|
|
func loadStoresConfig(context *cli.Context, path string) (*config.StoresConfig, error) {
|
|
configPath := context.GlobalString("config")
|
|
if configPath == "" {
|
|
// Ignore config not found errors returned from findConfigFile since the config file is not mandatory
|
|
foundPath, err := findConfigFile()
|
|
if err != nil {
|
|
return config.NewStoresConfig(), nil
|
|
}
|
|
configPath = foundPath
|
|
}
|
|
return config.LoadStoresConfig(configPath)
|
|
}
|
|
|
|
func inputStore(context *cli.Context, path string) (common.Store, error) {
|
|
storesConf, err := loadStoresConfig(context, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("input-type")), nil
|
|
}
|
|
|
|
func outputStore(context *cli.Context, path string) (common.Store, error) {
|
|
storesConf, err := loadStoresConfig(context, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if context.IsSet("indent") {
|
|
indent := context.Int("indent")
|
|
storesConf.YAML.Indent = indent
|
|
storesConf.JSON.Indent = indent
|
|
storesConf.JSONBinary.Indent = indent
|
|
}
|
|
|
|
return common.DefaultStoreForPathOrFormat(storesConf, path, context.String("output-type")), nil
|
|
}
|
|
|
|
func parseTreePath(arg string) ([]interface{}, error) {
|
|
var path []interface{}
|
|
components := strings.Split(arg, "[")
|
|
for _, component := range components {
|
|
if component == "" {
|
|
continue
|
|
}
|
|
if component[len(component)-1] != ']' {
|
|
return nil, fmt.Errorf("component %s doesn't end with ]", component)
|
|
}
|
|
component = component[:len(component)-1]
|
|
if component[0] == byte('"') || component[0] == byte('\'') {
|
|
// The component is a string
|
|
component = component[1 : len(component)-1]
|
|
path = append(path, component)
|
|
} else {
|
|
// The component must be a number
|
|
i, err := strconv.Atoi(component)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path = append(path, i)
|
|
}
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func keyGroups(c *cli.Context, file string, optionalConfig *config.Config) ([]sops.KeyGroup, error) {
|
|
var kmsKeys []keys.MasterKey
|
|
var pgpKeys []keys.MasterKey
|
|
var cloudKmsKeys []keys.MasterKey
|
|
var azkvKeys []keys.MasterKey
|
|
var hcVaultMkKeys []keys.MasterKey
|
|
var ageMasterKeys []keys.MasterKey
|
|
kmsEncryptionContext := kms.ParseKMSContext(c.String("encryption-context"))
|
|
if c.String("encryption-context") != "" && kmsEncryptionContext == nil {
|
|
return nil, common.NewExitError("Invalid KMS encryption context format", codes.ErrorInvalidKMSEncryptionContextFormat)
|
|
}
|
|
if c.String("kms") != "" {
|
|
for _, k := range kms.MasterKeysFromArnString(c.String("kms"), kmsEncryptionContext, c.String("aws-profile")) {
|
|
kmsKeys = append(kmsKeys, k)
|
|
}
|
|
}
|
|
if c.String("gcp-kms") != "" {
|
|
for _, k := range gcpkms.MasterKeysFromResourceIDString(c.String("gcp-kms")) {
|
|
cloudKmsKeys = append(cloudKmsKeys, k)
|
|
}
|
|
}
|
|
if c.String("azure-kv") != "" {
|
|
azureKeys, err := azkv.MasterKeysFromURLs(c.String("azure-kv"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range azureKeys {
|
|
azkvKeys = append(azkvKeys, k)
|
|
}
|
|
}
|
|
if c.String("hc-vault-transit") != "" {
|
|
hcVaultKeys, err := hcvault.NewMasterKeysFromURIs(c.String("hc-vault-transit"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range hcVaultKeys {
|
|
hcVaultMkKeys = append(hcVaultMkKeys, k)
|
|
}
|
|
}
|
|
if c.String("pgp") != "" {
|
|
for _, k := range pgp.MasterKeysFromFingerprintString(c.String("pgp")) {
|
|
pgpKeys = append(pgpKeys, k)
|
|
}
|
|
}
|
|
if c.String("age") != "" {
|
|
ageKeys, err := age.MasterKeysFromRecipients(c.String("age"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, k := range ageKeys {
|
|
ageMasterKeys = append(ageMasterKeys, k)
|
|
}
|
|
}
|
|
if c.String("kms") == "" && c.String("pgp") == "" && c.String("gcp-kms") == "" && c.String("azure-kv") == "" && c.String("hc-vault-transit") == "" && c.String("age") == "" {
|
|
conf := optionalConfig
|
|
var err error
|
|
if conf == nil {
|
|
conf, err = loadConfig(c, file, kmsEncryptionContext)
|
|
}
|
|
// config file might just not be supplied, without any error
|
|
if conf == nil {
|
|
errMsg := "config file not found, or has no creation rules, and no keys provided through command line options"
|
|
if err != nil {
|
|
errMsg = fmt.Sprintf("%s: %s", errMsg, err)
|
|
}
|
|
return nil, fmt.Errorf("%s", errMsg)
|
|
}
|
|
return conf.KeyGroups, err
|
|
}
|
|
var group sops.KeyGroup
|
|
group = append(group, kmsKeys...)
|
|
group = append(group, cloudKmsKeys...)
|
|
group = append(group, azkvKeys...)
|
|
group = append(group, pgpKeys...)
|
|
group = append(group, hcVaultMkKeys...)
|
|
group = append(group, ageMasterKeys...)
|
|
log.Debugf("Master keys available: %+v", group)
|
|
return []sops.KeyGroup{group}, nil
|
|
}
|
|
|
|
// loadConfig will look for an existing config file, either provided through the command line, or using findConfigFile
|
|
// Since a config file is not required, this function does not error when one is not found, and instead returns a nil config pointer
|
|
func loadConfig(c *cli.Context, file string, kmsEncryptionContext map[string]*string) (*config.Config, error) {
|
|
var err error
|
|
configPath := c.GlobalString("config")
|
|
if configPath == "" {
|
|
// Ignore config not found errors returned from findConfigFile since the config file is not mandatory
|
|
configPath, err = findConfigFile()
|
|
if err != nil {
|
|
// If we can't find a config file, but we were not explicitly requested to, assume it does not exist
|
|
return nil, nil
|
|
}
|
|
}
|
|
conf, err := config.LoadCreationRuleForFile(configPath, file, kmsEncryptionContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return conf, nil
|
|
}
|
|
|
|
func shamirThreshold(c *cli.Context, file string, optionalConfig *config.Config) (int, error) {
|
|
if c.Int("shamir-secret-sharing-threshold") != 0 {
|
|
return c.Int("shamir-secret-sharing-threshold"), nil
|
|
}
|
|
var err error
|
|
conf := optionalConfig
|
|
if conf == nil {
|
|
conf, err = loadConfig(c, file, nil)
|
|
}
|
|
if conf == nil {
|
|
// This takes care of the following two case:
|
|
// 1. No config was provided, or contains no creation rules. Err will be nil and ShamirThreshold will be the default value of 0.
|
|
// 2. We did find a config file, but failed to load it. In that case the calling function will print the error and exit.
|
|
return 0, err
|
|
}
|
|
return conf.ShamirThreshold, nil
|
|
}
|
|
|
|
func jsonValueToTreeInsertableValue(jsonValue string) (interface{}, error) {
|
|
var valueToInsert interface{}
|
|
err := encodingjson.Unmarshal([]byte(jsonValue), &valueToInsert)
|
|
if err != nil {
|
|
return nil, common.NewExitError("Value for --set is not valid JSON", codes.ErrorInvalidSetFormat)
|
|
}
|
|
// Check if decoding it as json we find a single value
|
|
// and not a map or slice, in which case we can't marshal
|
|
// it to a sops.TreeBranch
|
|
kind := reflect.ValueOf(valueToInsert).Kind()
|
|
if kind == reflect.Map || kind == reflect.Slice {
|
|
var err error
|
|
valueToInsert, err = (&json.Store{}).LoadPlainFile([]byte(jsonValue))
|
|
if err != nil {
|
|
return nil, common.NewExitError("Invalid --set value format", codes.ErrorInvalidSetFormat)
|
|
}
|
|
}
|
|
// Fix for #461
|
|
// Attempt conversion to TreeBranches to handle yaml multidoc. If conversion fails it's
|
|
// most likely a string value, so just return it as-is.
|
|
values, ok := valueToInsert.(sops.TreeBranches)
|
|
if !ok {
|
|
return valueToInsert, nil
|
|
}
|
|
return values[0], nil
|
|
}
|
|
|
|
func extractSetArguments(set string) (path []interface{}, valueToInsert interface{}, err error) {
|
|
// Set is a string with the format "python-dict-index json-value"
|
|
// Since python-dict-index has to end with ], we split at "] " to get the two parts
|
|
pathValuePair := strings.SplitAfterN(set, "] ", 2)
|
|
if len(pathValuePair) < 2 {
|
|
return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat)
|
|
}
|
|
fullPath := strings.TrimRight(pathValuePair[0], " ")
|
|
jsonValue := pathValuePair[1]
|
|
valueToInsert, err = jsonValueToTreeInsertableValue(jsonValue)
|
|
if err != nil {
|
|
// All errors returned by jsonValueToTreeInsertableValue are created by common.NewExitError(),
|
|
// so we can simply pass them on
|
|
return nil, nil, err
|
|
}
|
|
|
|
path, err = parseTreePath(fullPath)
|
|
if err != nil {
|
|
return nil, nil, common.NewExitError("Invalid --set format", codes.ErrorInvalidSetFormat)
|
|
}
|
|
return path, valueToInsert, nil
|
|
}
|
|
|
|
func decryptionOrder(decryptionOrder string) ([]string, error) {
|
|
if decryptionOrder == "" {
|
|
return sops.DefaultDecryptionOrder, nil
|
|
}
|
|
orderList := strings.Split(decryptionOrder, ",")
|
|
unique := make(map[string]struct{})
|
|
for _, v := range orderList {
|
|
if _, ok := unique[v]; ok {
|
|
return nil, common.NewExitError(fmt.Sprintf("Duplicate decryption key type: %s", v), codes.DuplicateDecryptionKeyType)
|
|
}
|
|
unique[v] = struct{}{}
|
|
}
|
|
return orderList, nil
|
|
}
|