Skip to content

Commit

Permalink
vault: add support for transit-encrypted K/V (#404)
Browse files Browse the repository at this point in the history
* vault: add support for transit-encrypted K/V

This commit adds support for encrypting K/V entries with a
specific transit engine key.

**Transit Engine**

The transit engine is Hashicorp Vault's en/decryption engine.
Among others, it allows to send a plaintext to an encrypt API
endpoint and receive a ciphertext and vice versa.
Ref: https://developer.hashicorp.com/vault/api-docs/secret/transit

Now, users can specify a transit key name in the KES config
file. KES will use this key to en/decrypt its key values
before storing them on the K/V backend.
However, this does, in general, not improve security since
Vault encrypts all data stored on the K/V engine with internally
managed keys. Users may specify a transit key if the want/have to
control which key is used to encrypt the K/V data.

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead authored Oct 24, 2023
1 parent 0d11e46 commit 0244caf
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 27 deletions.
17 changes: 17 additions & 0 deletions edge/server-config-yml.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ type yml struct {
Namespace env[string] `yaml:"namespace"`
Prefix env[string] `yaml:"prefix"`

Transit *struct {
Engine env[string] `yaml:"engine"`
KeyName env[string] `yaml:"key"`
}

AppRole *struct {
Engine env[string] `yaml:"engine"`
ID env[string] `yaml:"id"`
Expand Down Expand Up @@ -448,6 +453,12 @@ func ymlToKeyStore(y *yml) (KeyStore, error) {
y.KeyStore.Vault.Kubernetes.JWT.Value = string(b)
}
}
if y.KeyStore.Vault.Transit != nil {
if y.KeyStore.Vault.Transit.KeyName.Value == "" {
return nil, errors.New("edge: invalid vault keystore: invalid transit config: no key name specified")
}
}

if y.KeyStore.Vault.TLS.PrivateKey.Value != "" && y.KeyStore.Vault.TLS.Certificate.Value == "" {
return nil, errors.New("edge: invalid vault keystore: invalid tls config: no TLS certificate provided")
}
Expand Down Expand Up @@ -479,6 +490,12 @@ func ymlToKeyStore(y *yml) (KeyStore, error) {
Role: y.KeyStore.Vault.Kubernetes.Role.Value,
}
}
if y.KeyStore.Vault.Transit != nil {
s.Transit = &VaultTransit{
Engine: y.KeyStore.Vault.Transit.Engine.Value,
KeyName: y.KeyStore.Vault.Transit.KeyName.Value,
}
}
keystore = s
}

Expand Down
34 changes: 30 additions & 4 deletions edge/server-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ type VaultKeyStore struct {
// method credentials.
Kubernetes *VaultKubernetesAuth

// Transit contains the Vault transit encryption engine
// configuration for en/decrypting K/V entries with a
// key managed by Vault.
//
// This is an optional and additional layer of encryption.
// Since Vault manages and encrypts K/V values in any case,
// using the transit engine is usually not necessary.
Transit *VaultTransit

// PrivateKey is an optional path to a
// TLS private key file containing a
// TLS private key for mTLS authentication.
Expand Down Expand Up @@ -357,7 +366,7 @@ type VaultKeyStore struct {
// VaultAppRoleAuth is a structure containing the configuration
// for the Hashicorp Vault AppRole authentication method.
type VaultAppRoleAuth struct {
// AppRoleEngine is the AppRole authentication engine path.
// Engine is the AppRole authentication engine path.
// If empty, defaults to "approle".
Engine string

Expand All @@ -373,7 +382,7 @@ type VaultAppRoleAuth struct {
// VaultKubernetesAuth is a structure containing the configuration
// for the Hashicorp Vault Kubernetes authentication method.
type VaultKubernetesAuth struct {
// KubernetesEngine is the Kubernetes authentication engine path.
// Engine is the Kubernetes authentication engine path.
// If empty, defaults to "kubernetes".
Engine string

Expand All @@ -387,6 +396,17 @@ type VaultKubernetesAuth struct {
JWT string
}

// VaultTransit is a structure containing the configuration
// for the Hashicorp Vault transit encryption engine.
type VaultTransit struct {
// Engine is the Transit encryption engine path.
// If empty, defaults to "transit".
Engine string

// KeyName is the name of the key used for en/decryption.
KeyName string
}

// Connect returns a kv.Store that stores key-value pairs on a Hashicorp Vault server.
func (s *VaultKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte], error) {
if s.AppRole == nil && s.Kubernetes == nil {
Expand All @@ -407,19 +427,25 @@ func (s *VaultKeyStore) Connect(ctx context.Context) (kv.Store[string, []byte],
StatusPingAfter: s.StatusPing,
}
if s.AppRole != nil {
c.AppRole = vault.AppRole{
c.AppRole = &vault.AppRole{
Engine: s.AppRole.Engine,
ID: s.AppRole.ID,
Secret: s.AppRole.Secret,
}
}
if s.Kubernetes != nil {
c.K8S = vault.Kubernetes{
c.K8S = &vault.Kubernetes{
Engine: s.Kubernetes.Engine,
Role: s.Kubernetes.Role,
JWT: s.Kubernetes.JWT,
}
}
if s.Transit != nil {
c.Transit = &vault.Transit{
Engine: s.Transit.Engine,
KeyName: s.Transit.KeyName,
}
}
return vault.Connect(ctx, c)
}

Expand Down
4 changes: 2 additions & 2 deletions internal/keystore/vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (c *client) CheckStatus(ctx context.Context, delay time.Duration) {
// from the vault server by using the login AppRole credentials.
//
// To renew the auth. token see: client.RenewToken(...).
func (c *client) AuthenticateWithAppRole(login AppRole) authFunc {
func (c *client) AuthenticateWithAppRole(login *AppRole) authFunc {
return func() (token string, ttl time.Duration, err error) {
secret, err := c.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{
"role_id": login.ID,
Expand Down Expand Up @@ -99,7 +99,7 @@ func (c *client) AuthenticateWithAppRole(login AppRole) authFunc {
}
}

func (c *client) AuthenticateWithK8S(login Kubernetes) authFunc {
func (c *client) AuthenticateWithK8S(login *Kubernetes) authFunc {
return func() (token string, ttl time.Duration, err error) {
secret, err := c.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{
"role": login.Role,
Expand Down
68 changes: 64 additions & 4 deletions internal/keystore/vault/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const (
// EngineKV is the Hashicorp Vault default KV secret engine path.
EngineKV = "kv"

// EngineTransit is the Hashicorp Vault default transit secret engine path.
EngineTransit = "transit"

// EngineAppRole is the Hashicorp Vault default AppRole authentication
// engine path.
EngineAppRole = "approle"
Expand Down Expand Up @@ -58,6 +61,19 @@ type AppRole struct {
Retry time.Duration
}

// Clone returns a copy of the AppRole auth.
func (a *AppRole) Clone() *AppRole {
if a == nil {
return nil
}
return &AppRole{
Engine: a.Engine,
ID: a.ID,
Secret: a.Secret,
Retry: a.Retry,
}
}

// Kubernetes contains authentication information
// for the Hashicorp Vault Kubernetes authentication
// API.
Expand All @@ -83,6 +99,44 @@ type Kubernetes struct {
Retry time.Duration
}

// Clone returns a copy of the Kubernetes auth.
func (k *Kubernetes) Clone() *Kubernetes {
if k == nil {
return nil
}
return &Kubernetes{
Engine: k.Engine,
Role: k.Role,
JWT: k.JWT,
Retry: k.Retry,
}
}

// Transit contains information for using the
// Hashicorp Vault transit encryption engine.
//
// Ref: https://developer.hashicorp.com/vault/api-docs/secret/transit
type Transit struct {
// Engine is the transit engine path.
// If empty, defaults to EngineTransit.
Engine string

// KeyName is the name of the transit key
// used for en/decrypting K/V entries.
KeyName string
}

// Clone returns a copy of the Transit.
func (t *Transit) Clone() *Transit {
if t == nil {
return nil
}
return &Transit{
Engine: t.Engine,
KeyName: t.KeyName,
}
}

// Config is a structure containing configuration
// options for connecting to a Hashicorp Vault server.
type Config struct {
Expand Down Expand Up @@ -119,11 +173,16 @@ type Config struct {

// AppRole contains the Vault AppRole authentication
// credentials.
AppRole AppRole
AppRole *AppRole

// K8S contains the Vault Kubernetes authentication
// credentials.
K8S Kubernetes
K8S *Kubernetes

// Transit contains an optional Vault transit engine
// configuration for en/decrypting keys at the K/V
// engine. It adds an additional layer of encryption.
Transit *Transit

// StatusPingAfter is the duration after which
// the KeyStore will check the status of the Vault
Expand Down Expand Up @@ -164,8 +223,9 @@ func (c *Config) Clone() *Config {
APIVersion: c.APIVersion,
Namespace: c.Namespace,
Prefix: c.Prefix,
AppRole: c.AppRole,
K8S: c.K8S,
AppRole: c.AppRole.Clone(),
K8S: c.K8S.Clone(),
Transit: c.Transit.Clone(),
StatusPingAfter: c.StatusPingAfter,
PrivateKey: c.PrivateKey,
Certificate: c.Certificate,
Expand Down
11 changes: 8 additions & 3 deletions internal/keystore/vault/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
package vault

import (
"reflect"
"testing"
"time"
)

func TestCloneConfig(t *testing.T) {
for i, a := range cloneConfigTests {
if b := a.Clone(); *a != *b {
if b := a.Clone(); !reflect.DeepEqual(a, b) {
t.Fatalf("Test %d: cloned config does not match original", i)
}
}
Expand All @@ -24,17 +25,21 @@ var cloneConfigTests = []*Config{
APIVersion: APIv2,
Namespace: "ns-1",
Prefix: "my-prefix",
AppRole: AppRole{
AppRole: &AppRole{
Engine: "auth",
ID: "be7f3c83-9733-4d65-adaa-7eeb6e14e922",
Secret: "ba8d68af-23c4-4199-a516-e37cebdaab48",
Retry: 30 * time.Second,
},
K8S: Kubernetes{
K8S: &Kubernetes{
Engine: "auth",
Role: "kes",
JWT: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
},
Transit: &Transit{
Engine: "transit",
KeyName: "my-key",
},
StatusPingAfter: 15 * time.Second,
PrivateKey: "/tmp/kes/vault.key",
Certificate: "/tmp/kes/vault.crt",
Expand Down
Loading

0 comments on commit 0244caf

Please sign in to comment.