util: add support for vault certificates

Added a option to pass the client certificate
and the client certificate key for the vault token
based encryption.

Signed-off-by: Madhu Rajanna <madhupr007@gmail.com>
This commit is contained in:
Madhu Rajanna 2020-12-15 12:41:28 +05:30 committed by mergify[bot]
parent 3109160fa0
commit 81061e9f68
5 changed files with 232 additions and 30 deletions

View File

@ -180,8 +180,73 @@ data:
vaultBackendPath: "secret/ceph-csi-encryption/" vaultBackendPath: "secret/ceph-csi-encryption/"
vaultTLSServerName: "vault.infosec.example.org" vaultTLSServerName: "vault.infosec.example.org"
vaultCAFromSecret: "vault-infosec-ca" vaultCAFromSecret: "vault-infosec-ca"
vaultClientCertFromSecret: "vault-client-cert"
vaultClientCertKeyFromSecret: "vault-client-cert-key"
vaultCAVerify: "true" vaultCAVerify: "true"
``` ```
Only parameters with the `vault`-prefix may be changed in the Kubernetes Only parameters with the `vault`-prefix may be changed in the Kubernetes
ConfigMap of the Tenant. ConfigMap of the Tenant.
### Certificates stored in the Tenants Kubernetes Namespace
The `vaultCAFromSecret` , `vaultClientCertFromSecret` and
`vaultClientCertKeyFromSecret` secrets should be created in the namespace where
Ceph-CSI is deployed. The sample of secrets for the CA and client Certificate.
#### CA Certificate to verify Vault server TLS certificate
```yaml
---
apiVersion: v1
kind: secret
metadata:
name: vault-infosec-ca
stringData:
ca.cert: |
MIIC2DCCAcCgAwIBAgIBATANBgkqh...
```
#### Client Certificate for Vault connection
```yaml
---
apiVersion: v1
kind: secret
metadata:
name: vault-client-cert
stringData:
tls.cert: |
BATANBgkqcCgAwIBAgIBATANBAwI...
```
#### Client Certificate key for Vault connection
```yaml
---
apiVersion: v1
kind: secret
metadata:
name: vault-client-cert-key
stringData:
tls.key: |
KNSC2DVVXcCgkqcCgAwIBAgIwewrvx...
```
Its also possible that a user can create a single secret for the certificates
and update the configuration to fetch certificates from a secret.
```yaml
---
apiVersion: v1
kind: secret
metadata:
name: vault-certificates
stringData:
ca.cert: |
MIIC2DCCAcCgAwIBAgIBATANBgkqh...
tls.cert: |
BATANBgkqcCgAwIBAgIBATANBAwI...
tls.key: |
KNSC2DVVXcCgkqcCgAwIBAgIwewrvx...
```

View File

@ -141,7 +141,7 @@ func GetKMS(tenant, kmsID string, secrets map[string]string) (EncryptionKMS, err
case kmsTypeVault: case kmsTypeVault:
return InitVaultKMS(kmsID, kmsConfig, secrets) return InitVaultKMS(kmsID, kmsConfig, secrets)
case kmsTypeVaultTokens: case kmsTypeVaultTokens:
return InitVaultTokensKMS(tenant, kmsID, kmsConfig, secrets) return InitVaultTokensKMS(tenant, kmsID, kmsConfig)
} }
return nil, fmt.Errorf("unknown encryption KMS type %s", kmsType) return nil, fmt.Errorf("unknown encryption KMS type %s", kmsType)
} }

View File

@ -113,7 +113,7 @@ func setConfigString(option *string, config map[string]interface{}, key string)
// vc.connectVault(). // vc.connectVault().
// //
// nolint:gocyclo // iterating through many config options, not complex at all. // nolint:gocyclo // iterating through many config options, not complex at all.
func (vc *vaultConnection) initConnection(kmsID string, config map[string]interface{}, secrets map[string]string) error { func (vc *vaultConnection) initConnection(kmsID string, config map[string]interface{}) error {
vaultConfig := make(map[string]interface{}) vaultConfig := make(map[string]interface{})
keyContext := make(map[string]string) keyContext := make(map[string]string)
@ -183,18 +183,6 @@ func (vc *vaultConnection) initConnection(kmsID string, config map[string]interf
if errors.Is(err, errConfigOptionInvalid) { if errors.Is(err, errConfigOptionInvalid) {
return err return err
} }
// ignore errConfigOptionMissing, no default was set
if vaultCAFromSecret != "" {
caPEM, ok := secrets[vaultCAFromSecret]
if !ok {
return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
}
vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
}
}
// update the existing config only if no config is available yet // update the existing config only if no config is available yet
if vc.keyContext != nil { if vc.keyContext != nil {
@ -215,6 +203,38 @@ func (vc *vaultConnection) initConnection(kmsID string, config map[string]interf
return nil return nil
} }
// initCertificates sets VAULT_* environment variables in the vc.vaultConfig map,
// these settings will be used when connecting to the Vault service with
// vc.connectVault().
//
func (vc *vaultConnection) initCertificates(config map[string]interface{}, secrets map[string]string) error {
vaultConfig := make(map[string]interface{})
vaultCAFromSecret := "" // optional
err := setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultCAFromSecret != "" {
caPEM, ok := secrets[vaultCAFromSecret]
if !ok {
return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
}
vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
}
// update the existing config
for key, value := range vaultConfig {
vc.vaultConfig[key] = value
}
}
return nil
}
// connectVault creates a new connection to Vault. This should be called after // connectVault creates a new connection to Vault. This should be called after
// filling vc.vaultConfig. // filling vc.vaultConfig.
func (vc *vaultConnection) connectVault() error { func (vc *vaultConnection) connectVault() error {
@ -242,11 +262,16 @@ func (vc *vaultConnection) Destroy() {
// InitVaultKMS returns an interface to HashiCorp Vault KMS. // InitVaultKMS returns an interface to HashiCorp Vault KMS.
func InitVaultKMS(kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) { func InitVaultKMS(kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) {
kms := &VaultKMS{} kms := &VaultKMS{}
err := kms.initConnection(kmsID, config, secrets) err := kms.initConnection(kmsID, config)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize Vault connection: %w", err) return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
} }
err = kms.initCertificates(config, secrets)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault certificates: %w", err)
}
vaultAuthPath := vaultDefaultAuthPath vaultAuthPath := vaultDefaultAuthPath
err = setConfigString(&vaultAuthPath, config, "vaultAuthPath") err = setConfigString(&vaultAuthPath, config, "vaultAuthPath")
if err != nil { if err != nil {

View File

@ -20,9 +20,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
loss "github.com/libopenstorage/secrets" loss "github.com/libopenstorage/secrets"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -62,6 +64,8 @@ Example JSON structure in the KMS config is,
"vaultBackendPath": "secret/", "vaultBackendPath": "secret/",
"vaultTLSServerName": "vault.default.svc.cluster.local", "vaultTLSServerName": "vault.default.svc.cluster.local",
"vaultCAFromSecret": "vault-ca", "vaultCAFromSecret": "vault-ca",
"vaultClientCertFromSecret": "vault-client-cert",
"vaultClientCertKeyFromSecret": "vault-client-cert-key",
"vaultCAVerify": "false", "vaultCAVerify": "false",
"tenantConfigName": "ceph-csi-kms-config", "tenantConfigName": "ceph-csi-kms-config",
"tenantTokenName": "ceph-csi-kms-token", "tenantTokenName": "ceph-csi-kms-token",
@ -89,9 +93,9 @@ type VaultTokensKMS struct {
} }
// InitVaultTokensKMS returns an interface to HashiCorp Vault KMS. // InitVaultTokensKMS returns an interface to HashiCorp Vault KMS.
func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) { func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}) (EncryptionKMS, error) {
kms := &VaultTokensKMS{} kms := &VaultTokensKMS{}
err := kms.initConnection(kmsID, config, secrets) err := kms.initConnection(kmsID, config)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize Vault connection: %w", err) return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
} }
@ -100,7 +104,7 @@ func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, sec
kms.ConfigName = vaultTokensDefaultConfigName kms.ConfigName = vaultTokensDefaultConfigName
kms.TokenName = vaultTokensDefaultTokenName kms.TokenName = vaultTokensDefaultTokenName
err = kms.parseConfig(config, secrets) err = kms.parseConfig(config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -116,7 +120,7 @@ func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, sec
tenantConfig, ok := tenants[tenant] tenantConfig, ok := tenants[tenant]
if ok { if ok {
// override connection details from the tenant // override connection details from the tenant
err = kms.parseConfig(tenantConfig, secrets) err = kms.parseConfig(tenantConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,6 +136,10 @@ func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, sec
return nil, fmt.Errorf("failed fetching token from %s/%s: %w", tenant, kms.TokenName, err) return nil, fmt.Errorf("failed fetching token from %s/%s: %w", tenant, kms.TokenName, err)
} }
err = kms.initCertificates(config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault certificates: %w", err)
}
// connect to the Vault service // connect to the Vault service
err = kms.connectVault() err = kms.connectVault()
if err != nil { if err != nil {
@ -144,8 +152,8 @@ func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, sec
// parseConfig updates the kms.vaultConfig with the options from config and // parseConfig updates the kms.vaultConfig with the options from config and
// secrets. This method can be called multiple times, i.e. to override // secrets. This method can be called multiple times, i.e. to override
// configuration options from tenants. // configuration options from tenants.
func (kms *VaultTokensKMS) parseConfig(config map[string]interface{}, secrets map[string]string) error { func (kms *VaultTokensKMS) parseConfig(config map[string]interface{}) error {
err := kms.initConnection(kms.EncryptionKMSID, config, secrets) err := kms.initConnection(kms.EncryptionKMSID, config)
if err != nil { if err != nil {
return err return err
} }
@ -163,6 +171,97 @@ func (kms *VaultTokensKMS) parseConfig(config map[string]interface{}, secrets ma
return nil return nil
} }
// initCertificates updates the kms.vaultConfig with the options from config
// it calls the kubernetes secrets and get the required data.
// nolint:gocyclo // iterating through many config options, not complex at all.
func (kms *VaultTokensKMS) initCertificates(config map[string]interface{}) error {
vaultConfig := make(map[string]interface{})
csiNamespace := os.Getenv("POD_NAMESPACE")
vaultCAFromSecret := "" // optional
err := setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultCAFromSecret != "" {
cert, cErr := getCertificate(kms.Tenant, vaultCAFromSecret, "ca.cert")
if cErr != nil && !apierrs.IsNotFound(err) {
return fmt.Errorf("failed to get CA certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(cErr) {
cert, cErr = getCertificate(csiNamespace, vaultCAFromSecret, "ca.cert")
if cErr != nil {
return fmt.Errorf("failed to get CA certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
}
vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(cert))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
}
}
vaultClientCertFromSecret := "" // optional
err = setConfigString(&vaultClientCertFromSecret, config, "vaultClientCertFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultClientCertFromSecret != "" {
cert, cErr := getCertificate(kms.Tenant, vaultClientCertFromSecret, "tls.cert")
if cErr != nil && !apierrs.IsNotFound(cErr) {
return fmt.Errorf("failed to get client certificate from secret %s: %w", vaultClientCertFromSecret, cErr)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(cErr) {
cert, cErr = getCertificate(csiNamespace, vaultClientCertFromSecret, "tls.cert")
if cErr != nil {
return fmt.Errorf("failed to get client certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
}
vaultConfig[api.EnvVaultClientCert], err = createTempFile("vault-ca-cert", []byte(cert))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault client certificate: %w", err)
}
}
vaultClientCertKeyFromSecret := "" // optional
err = setConfigString(&vaultClientCertKeyFromSecret, config, "vaultClientCertKeyFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultClientCertKeyFromSecret != "" {
certKey, err := getCertificate(kms.Tenant, vaultClientCertKeyFromSecret, "tls.key")
if err != nil && !apierrs.IsNotFound(err) {
return fmt.Errorf("failed to get client certificate key from secret %s: %w", vaultClientCertKeyFromSecret, err)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(err) {
certKey, err = getCertificate(csiNamespace, vaultClientCertFromSecret, "tls.key")
if err != nil {
return fmt.Errorf("failed to get client certificate key from secret %s: %w", vaultCAFromSecret, err)
}
}
vaultConfig[api.EnvVaultClientKey], err = createTempFile("vault-client-cert-key", []byte(certKey))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault client cert key: %w", err)
}
}
for key, value := range vaultConfig {
kms.vaultConfig[key] = value
}
return nil
}
// GetPassphrase returns passphrase from Vault. The passphrase is stored in a // GetPassphrase returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure. // data.data.passphrase structure.
func (kms *VaultTokensKMS) GetPassphrase(key string) (string, error) { func (kms *VaultTokensKMS) GetPassphrase(key string) (string, error) {
@ -225,3 +324,18 @@ func getToken(tenant, tokenName string) (string, error) {
return string(token), nil return string(token), nil
} }
func getCertificate(tenant, secretName, key string) (string, error) {
c := NewK8sClient()
secret, err := c.CoreV1().Secrets(tenant).Get(context.TODO(), secretName, metav1.GetOptions{})
if err != nil {
return "", err
}
cert, ok := secret.Data[key]
if !ok {
return "", errors.New("failed to parse certificates")
}
return string(cert), nil
}

View File

@ -26,10 +26,9 @@ func TestParseConfig(t *testing.T) {
kms := VaultTokensKMS{} kms := VaultTokensKMS{}
config := make(map[string]interface{}) config := make(map[string]interface{})
secrets := make(map[string]string)
// empty config map // empty config map
err := kms.parseConfig(config, secrets) err := kms.parseConfig(config)
if !errors.Is(err, errConfigOptionMissing) { if !errors.Is(err, errConfigOptionMissing) {
t.Errorf("unexpected error (%T): %s", err, err) t.Errorf("unexpected error (%T): %s", err, err)
} }
@ -40,7 +39,7 @@ func TestParseConfig(t *testing.T) {
config["tenantTokenName"] = vaultTokensDefaultTokenName config["tenantTokenName"] = vaultTokensDefaultTokenName
// parsing with all required options // parsing with all required options
err = kms.parseConfig(config, secrets) err = kms.parseConfig(config)
switch { switch {
case err != nil: case err != nil:
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
@ -53,7 +52,7 @@ func TestParseConfig(t *testing.T) {
// tenant "bob" uses a different kms.ConfigName // tenant "bob" uses a different kms.ConfigName
bob := make(map[string]interface{}) bob := make(map[string]interface{})
bob["tenantConfigName"] = "the-config-from-bob" bob["tenantConfigName"] = "the-config-from-bob"
err = kms.parseConfig(bob, secrets) err = kms.parseConfig(bob)
switch { switch {
case err != nil: case err != nil:
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
@ -75,10 +74,9 @@ func TestInitVaultTokensKMS(t *testing.T) {
} }
config := make(map[string]interface{}) config := make(map[string]interface{})
secrets := make(map[string]string)
// empty config map // empty config map
_, err := InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) _, err := InitVaultTokensKMS("bob", "vault-tokens-config", config)
if !errors.Is(err, errConfigOptionMissing) { if !errors.Is(err, errConfigOptionMissing) {
t.Errorf("unexpected error (%T): %s", err, err) t.Errorf("unexpected error (%T): %s", err, err)
} }
@ -87,7 +85,7 @@ func TestInitVaultTokensKMS(t *testing.T) {
config["vaultAddress"] = "https://vault.default.cluster.svc" config["vaultAddress"] = "https://vault.default.cluster.svc"
// parsing with all required options // parsing with all required options
_, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }
@ -97,7 +95,7 @@ func TestInitVaultTokensKMS(t *testing.T) {
config["tenants"] = tenants config["tenants"] = tenants
// empty tenants list // empty tenants list
_, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }
@ -107,7 +105,7 @@ func TestInitVaultTokensKMS(t *testing.T) {
config["tenants"].(map[string]interface{})["bob"] = bob config["tenants"].(map[string]interface{})["bob"] = bob
bob["vaultAddress"] = "https://vault.bob.example.org" bob["vaultAddress"] = "https://vault.bob.example.org"
_, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }