mirror of
https://github.com/Luzifer/yaml-vault.git
synced 2024-12-20 12:01:18 +00:00
250 lines
6.1 KiB
Go
250 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
korvike "github.com/Luzifer/korvike/functions"
|
|
"github.com/Luzifer/rconfig/v2"
|
|
)
|
|
|
|
const filePermissionUserWrite = 0o600
|
|
|
|
var (
|
|
cfg = struct {
|
|
File string `flag:"file,f" default:"vault.yaml" description:"File to import from / export to" validate:"nonzero"`
|
|
Import bool `flag:"import" default:"false" description:"Enable importing data into Vault"`
|
|
Export bool `flag:"export" default:"false" description:"Enable exporting data from Vault"`
|
|
ExportPaths []string `flag:"export-paths" default:"secret" description:"Which paths to export"`
|
|
IgnoreErrors bool `flag:"ignore-errors" default:"false" description:"Do not exit on read/write errors"`
|
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
|
VaultAddress string `flag:"vault-addr" env:"VAULT_ADDR" default:"https://127.0.0.1:8200" description:"Vault API address"`
|
|
VaultToken string `flag:"vault-token" env:"VAULT_TOKEN" vardefault:"vault-token" description:"Specify a token to use instead of app-id auth" validate:"nonzero"`
|
|
VersionAndExit bool `flag:"version" default:"false" description:"Print program version and exit"`
|
|
}{}
|
|
|
|
version = "dev"
|
|
)
|
|
|
|
type importFile struct {
|
|
Keys []importField
|
|
}
|
|
|
|
type importField struct {
|
|
Key string
|
|
State string
|
|
Values map[string]interface{}
|
|
}
|
|
|
|
type execFunction func(*api.Client) error
|
|
|
|
func vaultTokenFromDisk() string {
|
|
vf, err := homedir.Expand("~/.vault-token")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
data, err := os.ReadFile(vf) //#nosec G304 // Intended to read file from disk
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return string(data)
|
|
}
|
|
|
|
func initApp() error {
|
|
rconfig.AutoEnv(true)
|
|
rconfig.SetVariableDefaults(map[string]string{
|
|
"vault-token": vaultTokenFromDisk(),
|
|
})
|
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
|
return fmt.Errorf("parsing CLI options: %w", err)
|
|
}
|
|
|
|
l, err := logrus.ParseLevel(cfg.LogLevel)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing log-level: %w", err)
|
|
}
|
|
logrus.SetLevel(l)
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
var err error
|
|
if err = initApp(); err != nil {
|
|
logrus.WithError(err).Fatal("initializing app")
|
|
}
|
|
|
|
if cfg.VersionAndExit {
|
|
fmt.Printf("yaml-vault %s\n", version) //nolint:forbidigo
|
|
os.Exit(0)
|
|
}
|
|
|
|
if cfg.Export == cfg.Import {
|
|
logrus.Fatal("either import or export must be set")
|
|
}
|
|
|
|
if _, err := os.Stat(cfg.File); (err == nil && cfg.Export) || (err != nil && cfg.Import) {
|
|
if cfg.Export {
|
|
logrus.Fatal("output file exists, stopping now.")
|
|
}
|
|
logrus.Fatal("input file does not exist, stopping now.")
|
|
}
|
|
|
|
client, err := api.NewClient(&api.Config{
|
|
Address: cfg.VaultAddress,
|
|
})
|
|
if err != nil {
|
|
logrus.WithError(err).Fatal("creating Vault client")
|
|
}
|
|
|
|
client.SetToken(cfg.VaultToken)
|
|
|
|
var ex execFunction
|
|
if cfg.Export {
|
|
ex = exportFromVault
|
|
} else {
|
|
ex = importToVault
|
|
}
|
|
|
|
if err = ex(client); err != nil {
|
|
logrus.WithError(err).Fatal("executing requested action")
|
|
}
|
|
}
|
|
|
|
func exportFromVault(client *api.Client) error {
|
|
out := importFile{}
|
|
|
|
for _, path := range cfg.ExportPaths {
|
|
if path[0] == '/' {
|
|
path = path[1:]
|
|
}
|
|
|
|
if !strings.HasSuffix(path, "/") {
|
|
path += "/"
|
|
}
|
|
|
|
if err := readRecurse(client, path, &out); err != nil {
|
|
return fmt.Errorf("reading from Vault: %w", err)
|
|
}
|
|
}
|
|
|
|
data, err := yaml.Marshal(out)
|
|
if err != nil {
|
|
return fmt.Errorf("marshalling YAML: %w", err)
|
|
}
|
|
|
|
if err = os.WriteFile(cfg.File, data, filePermissionUserWrite); err != nil {
|
|
return fmt.Errorf("writing file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func importToVault(client *api.Client) error {
|
|
keysRaw, err := os.ReadFile(cfg.File)
|
|
if err != nil {
|
|
return fmt.Errorf("reading input-file: %w", err)
|
|
}
|
|
|
|
keysRaw, err = parseImportFile(keysRaw)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing input file: %w", err)
|
|
}
|
|
|
|
var keys importFile
|
|
if err := yaml.Unmarshal(keysRaw, &keys); err != nil {
|
|
return fmt.Errorf("unmarshalling input file: %w", err)
|
|
}
|
|
|
|
for _, field := range keys.Keys {
|
|
logger := logrus.WithField("path", field.Key)
|
|
|
|
if field.State == "absent" {
|
|
if _, err := client.Logical().Delete(field.Key); err != nil {
|
|
if cfg.IgnoreErrors {
|
|
logger.WithError(err).Error("deleting key")
|
|
continue
|
|
}
|
|
return fmt.Errorf("deleting path %q: %w", field.Key, err)
|
|
}
|
|
logger.Debug("deleted key")
|
|
} else {
|
|
if _, err := client.Logical().Write(field.Key, field.Values); err != nil {
|
|
if cfg.IgnoreErrors {
|
|
logger.WithError(err).Error("writing data to key")
|
|
continue
|
|
}
|
|
return fmt.Errorf("writing path %q: %w", field.Key, err)
|
|
}
|
|
logger.Debug("wrote data to key")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseImportFile(in []byte) (out []byte, err error) {
|
|
t, err := template.New("input file").Funcs(korvike.GetFunctionMap()).Parse(string(in))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing template: %w", err)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if err = t.Execute(buf, nil); err != nil {
|
|
return nil, fmt.Errorf("executing template: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func readRecurse(client *api.Client, path string, out *importFile) error {
|
|
if !strings.HasSuffix(path, "/") {
|
|
secret, err := client.Logical().Read(path)
|
|
if err != nil {
|
|
return fmt.Errorf("reading path %q: %w", path, err)
|
|
}
|
|
|
|
if secret == nil {
|
|
if cfg.IgnoreErrors {
|
|
logrus.WithField("path", path).Info("read nil secret")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read non-existent path %q", path)
|
|
}
|
|
|
|
out.Keys = append(out.Keys, importField{Key: path, Values: secret.Data})
|
|
logrus.WithField("path", path).Debug("read data from key")
|
|
return nil
|
|
}
|
|
|
|
secret, err := client.Logical().List(path)
|
|
if err != nil {
|
|
if cfg.IgnoreErrors {
|
|
logrus.WithError(err).WithField("path", path).Error("reading secret")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("reading path %q: %w", path, err)
|
|
}
|
|
|
|
if secret != nil && secret.Data["keys"] != nil {
|
|
for _, k := range secret.Data["keys"].([]interface{}) {
|
|
if err := readRecurse(client, path+k.(string), out); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|