зеркало из https://github.com/getsops/sops.git
275 строки
7.8 KiB
Go
275 строки
7.8 KiB
Go
package hcvault
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
homedir "github.com/mitchellh/go-homedir"
|
|
"github.com/sirupsen/logrus"
|
|
"go.mozilla.org/sops/v3/logging"
|
|
)
|
|
|
|
var log *logrus.Logger
|
|
|
|
func init() {
|
|
log = logging.NewLogger("VAULT_TRANSIT")
|
|
}
|
|
|
|
// MasterKey is a Vault Transit backend path used to encrypt and decrypt sops' data key.
|
|
type MasterKey struct {
|
|
EncryptedKey string
|
|
KeyName string
|
|
EnginePath string
|
|
VaultAddress string
|
|
CreationDate time.Time
|
|
}
|
|
|
|
// NewMasterKeysFromURIs gets lots of keys from lots of URIs
|
|
func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) {
|
|
var keys []*MasterKey
|
|
if uris == "" {
|
|
return keys, nil
|
|
}
|
|
uriList := strings.Split(uris, ",")
|
|
for _, uri := range uriList {
|
|
if uri == "" {
|
|
continue
|
|
}
|
|
key, err := NewMasterKeyFromURI(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// NewMasterKeyFromURI obtains the vaultAddress the transit backend path and the key name from the full URI of the key
|
|
func NewMasterKeyFromURI(uri string) (*MasterKey, error) {
|
|
log.Debugln("Called NewMasterKeyFromURI with uri: ", uri)
|
|
var key *MasterKey
|
|
if uri == "" {
|
|
return key, nil
|
|
}
|
|
u, err := url.Parse(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if u.Scheme == "" {
|
|
return nil, fmt.Errorf("missing scheme in vault URL (should be like this: https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri)
|
|
}
|
|
enginePath, keyName, err := getBackendAndKeyFromPath(u.RequestURI())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Path = ""
|
|
return NewMasterKey(u.String(), enginePath, keyName), nil
|
|
|
|
}
|
|
|
|
func getBackendAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) {
|
|
// Running vault behind a reverse proxy with longer urls seems not to be supported
|
|
// by the vault client api so we have a separate Error for that here.
|
|
if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) {
|
|
return "", "", fmt.Errorf("running Vault with a prefixed url is not supported! (Format has to be like https://vault.example.com:8200/v1/transit/keys/keyName)")
|
|
} else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) == false {
|
|
return "", "", fmt.Errorf("vault path does not seem to be formatted correctly: (eg. https://vault.example.com:8200/v1/transit/keys/keyName)")
|
|
}
|
|
fullPath = strings.TrimPrefix(fullPath, "/")
|
|
fullPath = strings.TrimSuffix(fullPath, "/")
|
|
|
|
dirs := strings.Split(fullPath, "/")
|
|
|
|
keyName = dirs[len(dirs)-1]
|
|
enginePath = path.Join(dirs[1 : len(dirs)-2]...)
|
|
err = nil
|
|
return
|
|
}
|
|
|
|
// NewMasterKey creates a new MasterKey from a vault address, transit backend path and a key name and setting the creation date to the current date
|
|
func NewMasterKey(addess, enginePath, keyName string) *MasterKey {
|
|
mk := &MasterKey{
|
|
VaultAddress: addess,
|
|
EnginePath: enginePath,
|
|
KeyName: keyName,
|
|
CreationDate: time.Now().UTC(),
|
|
}
|
|
log.Debugln("Created Vault Master Key: ", mk)
|
|
return mk
|
|
}
|
|
|
|
// EncryptedDataKey returns the encrypted data key this master key holds
|
|
func (key *MasterKey) EncryptedDataKey() []byte {
|
|
return []byte(key.EncryptedKey)
|
|
}
|
|
|
|
// SetEncryptedDataKey sets the encrypted data key for this master key
|
|
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
|
|
key.EncryptedKey = string(enc)
|
|
}
|
|
|
|
func vaultClient(address string) (*api.Client, error) {
|
|
cfg := api.DefaultConfig()
|
|
cfg.Address = address
|
|
cli, err := api.NewClient(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot create Vault Client: %v", err)
|
|
}
|
|
if cli.Token() != "" {
|
|
return cli, nil
|
|
}
|
|
homePath, err := homedir.Dir()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("error getting user's home directory: %v", err))
|
|
}
|
|
tokenPath := filepath.Join(homePath, ".vault-token")
|
|
f, err := os.Open(tokenPath)
|
|
if os.IsNotExist(err) {
|
|
return cli, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
if _, err := io.Copy(buf, f); err != nil {
|
|
return nil, err
|
|
}
|
|
cli.SetToken(strings.TrimSpace(buf.String()))
|
|
return cli, nil
|
|
}
|
|
|
|
// Encrypt takes a sops data key, encrypts it with Vault Transit and stores the result in the EncryptedKey field
|
|
func (key *MasterKey) Encrypt(dataKey []byte) error {
|
|
fullPath := path.Join(key.EnginePath, "encrypt", key.KeyName)
|
|
cli, err := vaultClient(key.VaultAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encoded := base64.StdEncoding.EncodeToString(dataKey)
|
|
payload := make(map[string]interface{})
|
|
payload["plaintext"] = encoded
|
|
raw, err := cli.Logical().Write(fullPath, payload)
|
|
if err != nil {
|
|
log.WithField("Path", fullPath).Info("Encryption failed")
|
|
return err
|
|
}
|
|
if raw == nil || raw.Data == nil {
|
|
return fmt.Errorf("The transit backend %s is empty", fullPath)
|
|
}
|
|
encrypted, ok := raw.Data["ciphertext"]
|
|
if !ok {
|
|
return fmt.Errorf("there's not encrypted data")
|
|
}
|
|
encryptedKey, ok := encrypted.(string)
|
|
if !ok {
|
|
return fmt.Errorf("the ciphertext cannot be casted to string")
|
|
}
|
|
key.EncryptedKey = encryptedKey
|
|
return nil
|
|
}
|
|
|
|
// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet
|
|
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
|
|
if key.EncryptedKey == "" {
|
|
return key.Encrypt(dataKey)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result.
|
|
func (key *MasterKey) Decrypt() ([]byte, error) {
|
|
fullPath := path.Join(key.EnginePath, "decrypt", key.KeyName)
|
|
cli, err := vaultClient(key.VaultAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload := make(map[string]interface{})
|
|
payload["ciphertext"] = key.EncryptedKey
|
|
raw, err := cli.Logical().Write(fullPath, payload)
|
|
if err != nil {
|
|
log.WithField("Path", fullPath).Info("Encryption failed")
|
|
return nil, err
|
|
}
|
|
if raw == nil || raw.Data == nil {
|
|
return nil, fmt.Errorf("The transit backend %s is empty", fullPath)
|
|
}
|
|
decrypted, ok := raw.Data["plaintext"]
|
|
if ok != true {
|
|
return nil, fmt.Errorf("there's no decrypted data")
|
|
}
|
|
dataKey, ok := decrypted.(string)
|
|
if ok != true {
|
|
return nil, fmt.Errorf("the plaintest cannot be casted to string")
|
|
}
|
|
result, err := base64.StdEncoding.DecodeString(dataKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Couldn't decode base64 plaintext")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// NeedsRotation returns whether the data key needs to be rotated or not.
|
|
// This is simply copied from GCPKMS
|
|
// TODO: handle key rotation on vault side
|
|
func (key *MasterKey) NeedsRotation() bool {
|
|
//TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data
|
|
return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6)
|
|
}
|
|
|
|
// ToString converts the key to a string representation
|
|
func (key *MasterKey) ToString() string {
|
|
return fmt.Sprintf("%s/v1/%s/keys/%s", key.VaultAddress, key.EnginePath, key.KeyName)
|
|
}
|
|
|
|
func (key *MasterKey) createVaultTransitAndKey() error {
|
|
cli, err := vaultClient(key.VaultAddress)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot create Vault Client: %v", err)
|
|
}
|
|
err = cli.Sys().Mount(key.EnginePath, &api.MountInput{
|
|
Type: "transit",
|
|
Description: "backend transit used by SOPS",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := path.Join(key.EnginePath, "keys", key.KeyName)
|
|
payload := make(map[string]interface{})
|
|
payload["type"] = "rsa-4096"
|
|
_, err = cli.Logical().Write(path, payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = cli.Logical().Read(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToMap converts the MasterKey to a map for serialization purposes
|
|
func (key MasterKey) ToMap() map[string]interface{} {
|
|
out := make(map[string]interface{})
|
|
out["vault_address"] = key.VaultAddress
|
|
out["key_name"] = key.KeyName
|
|
out["engine_path"] = key.EnginePath
|
|
out["enc"] = key.EncryptedKey
|
|
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
|
|
return out
|
|
}
|