Skip to content

Commit

Permalink
Auto unseal with Yandex Key Management Service
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreyZamyslov committed May 14, 2024
1 parent c6e4c2d commit 1c251b1
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ hooks:

# bootstrap the build by generating any necessary code and downloading additional tools that may
# be used by devs.
bootstrap: prep tools
bootstrap: tools prep

# Note: if you have plugins in GOPATH you can update all of them via something like:
# for i in $(ls | grep vault-plugin-); do cd $i; git remote update; git reset --hard origin/master; dep ensure -update; git add .; git commit; git push; cd ..; done
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,8 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240513082302-2e0a3cd8443b // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240513082658-e33b8a503812 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytecodealliance/wasmtime-go v0.36.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
Expand Down Expand Up @@ -2103,6 +2104,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
Expand Down Expand Up @@ -3454,6 +3456,10 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yandex-cloud/go-genproto v0.0.0-20240513082302-2e0a3cd8443b h1:dVGX0V6GkBxfYgq3F4LB+k8QW9U+OdpaEdfd4ztzKeo=
github.com/yandex-cloud/go-genproto v0.0.0-20240513082302-2e0a3cd8443b/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-sdk v0.0.0-20240513082658-e33b8a503812 h1:gLo7wF5FNdnTf5HT70eqgYwU/eqRr3jLVftrw7LKlx0=
github.com/yandex-cloud/go-sdk v0.0.0-20240513082658-e33b8a503812/go.mod h1:1VId8ra1WVRwxujGrJea5CAGa38TG65hjlP9SfFkPN0=
github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
Expand Down Expand Up @@ -4393,6 +4399,7 @@ google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
Expand Down
19 changes: 19 additions & 0 deletions internalshared/configutil/kms.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ func configureWrapper(configKMS *KMS, infoKeys *[]string, info *map[string]strin
case wrapping.WrapperTypePkcs11:
return nil, fmt.Errorf("KMS type 'pkcs11' requires the Vault Enterprise HSM binary")

case WrapperTypeYandexCloudKms:
wrapper, kmsInfo, err = GetYandexCloudKMSFunc(configKMS, opts...)

default:
return nil, fmt.Errorf("Unknown KMS type %q", configKMS.Type)
}
Expand Down Expand Up @@ -421,6 +424,22 @@ var GetTransitKMSFunc = func(kms *KMS, opts ...wrapping.Option) (wrapping.Wrappe
return wrapper, info, nil
}

func GetYandexCloudKMSFunc(kms *KMS, opts ...wrapping.Option) (wrapping.Wrapper, map[string]string, error) {
wrapper := NewWrapper()
wrapperInfo, err := wrapper.SetConfig(context.Background(), append(opts, wrapping.WithConfigMap(kms.Config))...)
if err != nil {
// If the error is any other than logical.KeyNotFoundError, return the error
if !errwrap.ContainsType(err, new(logical.KeyNotFoundError)) {
return nil, nil, err
}
}
info := make(map[string]string)
if wrapperInfo != nil {
info["Yandex.Cloud KMS KeyID"] = wrapperInfo.Metadata["kms_key_id"]
}
return wrapper, info, nil
}

func createSecureRandomReader(_ *SharedConfig, _ []*EntropySourcerInfo, _ hclog.Logger) (io.Reader, error) {
return rand.Reader, nil
}
Expand Down
250 changes: 250 additions & 0 deletions internalshared/configutil/yandexcloudkms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package configutil

import (
"context"
"fmt"
"os"
"sync/atomic"

"github.com/yandex-cloud/go-genproto/yandex/cloud/kms/v1"
"github.com/yandex-cloud/go-sdk/iamkey"

wrapping "github.com/hashicorp/go-kms-wrapping/v2"
ycsdk "github.com/yandex-cloud/go-sdk"
)

const (
// Accepted env vars
EnvYandexCloudOAuthToken = "YANDEXCLOUD_OAUTH_TOKEN"
EnvYandexCloudServiceAccountKeyFile = "YANDEXCLOUD_SERVICE_ACCOUNT_KEY_FILE"
EnvYandexCloudKMSKeyID = "YANDEXCLOUD_KMS_KEY_ID"
EnvYandexCloudEndpoint = "YANDEXCLOUD_ENDPOINT"

// Accepted config parameters
CfgYandexCloudOAuthToken = "oauth_token"
CfgYandexCloudServiceAccountKeyFile = "service_account_key_file"
CfgYandexCloudKMSKeyID = "kms_key_id"
CfgYandexCloudEndpoint = "endpoint"

WrapperTypeYandexCloudKms wrapping.WrapperType = "yandexcloudkms"
)

// Wrapper represents credentials and key information for the KMS Key used to
// encryption and decryption
type Wrapper struct {
client kms.SymmetricCryptoServiceClient
keyID string
currentVersionID *atomic.Value
}

// Ensure that we are implementing Wrapper
var _ wrapping.Wrapper = (*Wrapper)(nil)

// NewWrapper creates a new Yandex.Cloud wrapper
func NewWrapper() *Wrapper {
k := &Wrapper{
currentVersionID: new(atomic.Value),
}
k.currentVersionID.Store("")
return k
}

// SetConfig sets the fields on the Wrapper object based on
// values from the config parameter.
//
// Order of precedence Yandex.Cloud values:
// * Environment variable
// * Value from Vault configuration file
// * Compute Instance metadata
// func (k *Wrapper) SetConfig(config map[string]string) (map[string]string, error) {
func (wrapper *Wrapper) SetConfig(ctx context.Context, options ...wrapping.Option) (*wrapping.WrapperConfig, error) {
opts, err := wrapping.GetOpts(options...)
if err != nil {
return nil, err
}

configMap := opts.WithConfigMap
if configMap == nil {
configMap = map[string]string{}
}

// Check and set versionId
wrapper.keyID = coalesce(os.Getenv(EnvYandexCloudKMSKeyID), configMap[CfgYandexCloudKMSKeyID])
if wrapper.keyID == "" {
return nil, fmt.Errorf(
"neither '%s' environment variable nor '%s' config parameter is set",
EnvYandexCloudKMSKeyID, CfgYandexCloudKMSKeyID,
)
}

// Check and set wrapper.client
if wrapper.client == nil {
client, err := getYandexCloudKMSClient(
coalesce(os.Getenv(EnvYandexCloudOAuthToken), configMap[CfgYandexCloudOAuthToken]),
coalesce(os.Getenv(EnvYandexCloudServiceAccountKeyFile), configMap[CfgYandexCloudServiceAccountKeyFile]),
coalesce(os.Getenv(EnvYandexCloudEndpoint), configMap[CfgYandexCloudEndpoint]),
)
if err != nil {
return nil, fmt.Errorf("error initializing Yandex.Cloud KMS client: %w", err)
}

if err := wrapper.setClient(client); err != nil {
return nil, fmt.Errorf("error setting Yandex.Cloud KMS client: %w", err)
}
}

wrapConfig := new(wrapping.WrapperConfig)
wrapConfig.Metadata = make(map[string]string)
wrapConfig.Metadata["kms_key_id"] = wrapper.keyID

return wrapConfig, nil
}

func (wrapper *Wrapper) setClient(client kms.SymmetricCryptoServiceClient) error {
wrapper.client = client

// Make sure all the required permissions are granted (also checks if key exists)
_, err := wrapper.Encrypt(context.Background(), []byte("go-kms-wrapping-test"), nil)
if err != nil {
return fmt.Errorf(
"failed to encrypt with Yandex.Cloud KMS key - ensure the key exists and permission to encrypt the key is granted: %w", err)
}

return nil
}

func (wrapper *Wrapper) KeyId(_ context.Context) (string, error) {
return wrapper.keyID, nil
}

func (wrapper *Wrapper) Type(ctx context.Context) (wrapping.WrapperType, error) {
return WrapperTypeYandexCloudKms, nil
}

func (wrapper *Wrapper) versionId() string {
return wrapper.currentVersionID.Load().(string)
}

func (wrapper *Wrapper) Encrypt(ctx context.Context, plaintext []byte, options ...wrapping.Option) (*wrapping.BlobInfo, error) {
if plaintext == nil {
return nil, fmt.Errorf("given plaintext for encryption is nil")
}

env, err := wrapping.EnvelopeEncrypt(plaintext, options...)
if err != nil {
return nil, fmt.Errorf("error wrapping data: %w", err)
}

if wrapper.client == nil {
return nil, fmt.Errorf("nil client")
}

encryptResponse, err := wrapper.client.Encrypt(
ctx,
&kms.SymmetricEncryptRequest{
KeyId: wrapper.keyID,
Plaintext: env.Key,
},
)
if err != nil {
return nil, fmt.Errorf("error encrypting data: %w", err)
}

// Store the current version id
wrapper.currentVersionID.Store(encryptResponse.VersionId)

ret := &wrapping.BlobInfo{
Ciphertext: env.Ciphertext,
Iv: env.Iv,
KeyInfo: &wrapping.KeyInfo{
// Even though we do not use the version id during decryption, store it
// to know exactly the specific key version used in encryption in case we
// want to rewrap older entries
KeyId: encryptResponse.VersionId,
WrappedKey: encryptResponse.Ciphertext,
},
}

return ret, nil
}

func (wrapper *Wrapper) Decrypt(ctx context.Context, ciphertext *wrapping.BlobInfo, options ...wrapping.Option) ([]byte, error) {
if ciphertext == nil {
return nil, fmt.Errorf("given input for decryption is nil")
}

decryptResponse, err := wrapper.client.Decrypt(
ctx,
&kms.SymmetricDecryptRequest{
KeyId: wrapper.keyID,
Ciphertext: ciphertext.KeyInfo.WrappedKey,
},
)
if err != nil {
return nil, fmt.Errorf("error decrypting data encryption key: %w", err)
}

envInfo := &wrapping.EnvelopeInfo{
Key: decryptResponse.Plaintext,
Iv: ciphertext.Iv,
Ciphertext: ciphertext.Ciphertext,
}
plaintext, err := wrapping.EnvelopeDecrypt(envInfo, options...)
if err != nil {
return nil, fmt.Errorf("error decrypting data: %w", err)
}

return plaintext, nil
}

func getYandexCloudKMSClient(oauthToken string, serviceAccountKeyFile string, endpoint string) (kms.SymmetricCryptoServiceClient, error) {
credentials, err := getCredentials(oauthToken, serviceAccountKeyFile)
if err != nil {
return nil, fmt.Errorf("error getting credentials: %w", err)
}

sdk, err := ycsdk.Build(
context.Background(),
ycsdk.Config{
Credentials: credentials,
Endpoint: endpoint,
},
)
if err != nil {
return nil, fmt.Errorf("error building Yandex.Cloud SDK instance: %w", err)
}

return sdk.KMSCrypto().SymmetricCrypto(), nil
}

func getCredentials(oauthToken string, serviceAccountKeyFile string) (ycsdk.Credentials, error) {
if oauthToken != "" && serviceAccountKeyFile != "" {
return nil, fmt.Errorf("error configuring authentication: both OAuth token and service account key file are specified")
}

// Yandex account authentication (via Oauth token)
if oauthToken != "" {
return ycsdk.OAuthToken(oauthToken), nil
}

// Service account authentication (via authorized key)
if serviceAccountKeyFile != "" {
key, err := iamkey.ReadFromJSONFile(serviceAccountKeyFile)
if err != nil {
return nil, fmt.Errorf("error reading service account key file: %w", err)
}
return ycsdk.ServiceAccountKey(key)
}

// Compute Instance Service Account authentication
return ycsdk.InstanceServiceAccount(), nil
}

func coalesce(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}
2 changes: 1 addition & 1 deletion version/version_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ var (
//go:embed VERSION
fullVersion string
Version, VersionPrerelease, _ = strings.Cut(strings.TrimSpace(fullVersion), "-")
VersionMetadata = ""
VersionMetadata = "yckms"
)

0 comments on commit 1c251b1

Please sign in to comment.