Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ceph: fix kms auto-detection when full TLS #8867

Merged
merged 1 commit into from Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/daemon/ceph/osd/kms/kms.go
Expand Up @@ -202,7 +202,7 @@ func ValidateConnectionDetails(clusterdContext *clusterd.Context, securitySpec *
case VaultKVSecretEngineKey:
// Append Backend Version if not already present
if GetParam(securitySpec.KeyManagementService.ConnectionDetails, vault.VaultBackendKey) == "" {
backendVersion, err := BackendVersion(securitySpec.KeyManagementService.ConnectionDetails)
backendVersion, err := BackendVersion(clusterdContext, ns, securitySpec.KeyManagementService.ConnectionDetails)
if err != nil {
return errors.Wrap(err, "failed to get backend version")
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/daemon/ceph/osd/kms/kms_test.go
Expand Up @@ -91,7 +91,7 @@ func TestValidateConnectionDetails(t *testing.T) {
securitySpec.KeyManagementService.ConnectionDetails["VAULT_CACERT"] = "vault-ca-secret"
err = ValidateConnectionDetails(context, securitySpec, ns)
assert.Error(t, err, "")
assert.EqualError(t, err, "failed to validate vault connection details: failed to find TLS connection details k8s secret \"VAULT_CACERT\"")
assert.EqualError(t, err, "failed to validate vault connection details: failed to find TLS connection details k8s secret \"vault-ca-secret\"")

// Error: TLS secret exists but empty key
tlsSecret := &v1.Secret{
Expand Down Expand Up @@ -122,7 +122,9 @@ func TestValidateConnectionDetails(t *testing.T) {
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client
// Mock the client here
vaultClient = func(secretConfig map[string]string) (*api.Client, error) { return client, nil }
vaultClient = func(clusterdContext *clusterd.Context, namespace string, secretConfig map[string]string) (*api.Client, error) {
return client, nil
}
if err := client.Sys().Mount("rook/", &api.MountInput{
Type: "kv-v2",
Options: map[string]string{"version": "2"},
Expand Down
74 changes: 64 additions & 10 deletions pkg/daemon/ceph/osd/kms/vault.go
Expand Up @@ -19,6 +19,7 @@ package kms
import (
"context"
"io/ioutil"
"os"
"strings"

"github.com/hashicorp/vault/api"
Expand All @@ -45,6 +46,14 @@ var (
vaultMandatoryConnectionDetails = []string{api.EnvVaultAddress}
)

// Used for unit tests mocking too as well as production code
var (
createTmpFile = ioutil.TempFile
getRemoveCertFiles = getRemoveCertFilesFunc
)

type removeCertFilesFunction func()

/* VAULT API INTERNAL VALUES
// Refer to https://pkg.golangclub.com/github.com/hashicorp/vault/api?tab=doc#pkg-constants
const EnvVaultAddress = "VAULT_ADDR"
Expand Down Expand Up @@ -77,10 +86,11 @@ func InitVault(context *clusterd.Context, namespace string, config map[string]st
}

// Populate TLS config
newConfigWithTLS, err := configTLS(context, namespace, oriConfig)
newConfigWithTLS, removeCertFiles, err := configTLS(context, namespace, oriConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize vault tls configuration")
}
defer removeCertFiles()

// Populate TLS config
for key, value := range newConfigWithTLS {
Expand All @@ -96,8 +106,31 @@ func InitVault(context *clusterd.Context, namespace string, config map[string]st
return v, nil
}

func configTLS(clusterdContext *clusterd.Context, namespace string, config map[string]string) (map[string]string, error) {
// configTLS returns a map of TLS config that map physical files for the TLS library to load
// Also it returns a function to remove the temporary files (certs, keys)
// The signature has named result parameters to help building 'defer' statements especially for the
// content of removeCertFiles which needs to be populated by the files to remove if no errors and be
// nil on errors
func configTLS(clusterdContext *clusterd.Context, namespace string, config map[string]string) (newConfig map[string]string, removeCertFiles removeCertFilesFunction, retErr error) {
ctx := context.TODO()
var filesToRemove []*os.File

defer func() {
// Build the function that the caller should use to remove the temp files here
// create it when this function is returning based on the currently-recorded files
removeCertFiles = getRemoveCertFiles(filesToRemove)
if retErr != nil {
// If we encountered an error, remove the temp files
removeCertFiles()

// Also return an empty function to remove the temp files
// It's fine to use nil here since the defer from the calling functions is only
// triggered after evaluating any error, if on error the defer is not triggered since we
// have returned already
removeCertFiles = nil
}
}()

for _, tlsOption := range cephv1.VaultTLSConnectionDetails {
tlsSecretName := GetParam(config, tlsOption)
if tlsSecretName == "" {
Expand All @@ -107,31 +140,52 @@ func configTLS(clusterdContext *clusterd.Context, namespace string, config map[s
if !strings.Contains(tlsSecretName, EtcVaultDir) {
secret, err := clusterdContext.Clientset.CoreV1().Secrets(namespace).Get(ctx, tlsSecretName, v1.GetOptions{})
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch tls k8s secret %q", tlsSecretName)
return nil, removeCertFiles, errors.Wrapf(err, "failed to fetch tls k8s secret %q", tlsSecretName)
}

// Generate a temp file
file, err := ioutil.TempFile("", "")
file, err := createTmpFile("", "")
if err != nil {
return nil, errors.Wrapf(err, "failed to generate temp file for k8s secret %q content", tlsSecretName)
return nil, removeCertFiles, errors.Wrapf(err, "failed to generate temp file for k8s secret %q content", tlsSecretName)
}

// Write into a file
err = ioutil.WriteFile(file.Name(), secret.Data[tlsSecretKeyToCheck(tlsOption)], 0444)
if err != nil {
return nil, errors.Wrapf(err, "failed to write k8s secret %q content to a file", tlsSecretName)
return nil, removeCertFiles, errors.Wrapf(err, "failed to write k8s secret %q content to a file", tlsSecretName)
}

leseb marked this conversation as resolved.
Show resolved Hide resolved
logger.Debugf("replacing %q current content %q with %q", tlsOption, config[tlsOption], file.Name())

// update the env var with the path
// Update the env var with the path
config[tlsOption] = file.Name()

// Add the file to the list of files to remove
filesToRemove = append(filesToRemove, file)
} else {
logger.Debugf("value of tlsOption %q tlsSecretName is already correct %q", tlsOption, tlsSecretName)
}
}

return config, nil
return config, removeCertFiles, nil
}

func getRemoveCertFilesFunc(filesToRemove []*os.File) removeCertFilesFunction {
return removeCertFilesFunction(func() {
for _, file := range filesToRemove {
logger.Debugf("closing %q", file.Name())
err := file.Close()
if err != nil {
logger.Errorf("failed to close file %q. %v", file.Name(), err)
}
Comment on lines +176 to +179
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the files are definitely still open when this is called, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we don't close them anywhere else.

logger.Debugf("closed %q", file.Name())
logger.Debugf("removing %q", file.Name())
err = os.Remove(file.Name())
if err != nil {
logger.Errorf("failed to remove file %q. %v", file.Name(), err)
}
logger.Debugf("removed %q", file.Name())
}
})
}

func put(v secrets.Secrets, secretName, secretValue string, keyContext map[string]string) error {
Expand Down Expand Up @@ -215,7 +269,7 @@ func validateVaultConnectionDetails(clusterdContext *clusterd.Context, ns string
// Fetch the secret
s, err := clusterdContext.Clientset.CoreV1().Secrets(ns).Get(ctx, tlsSecretName, v1.GetOptions{})
if err != nil {
return errors.Errorf("failed to find TLS connection details k8s secret %q", tlsOption)
return errors.Errorf("failed to find TLS connection details k8s secret %q", tlsSecretName)
}

// Check the Secret key and its content
Expand Down
30 changes: 25 additions & 5 deletions pkg/daemon/ceph/osd/kms/vault_api.go
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/libopenstorage/secrets/vault"
"github.com/libopenstorage/secrets/vault/utils"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"

"github.com/hashicorp/vault/api"
)
Expand All @@ -38,16 +39,35 @@ var vaultClient = newVaultClient

// newVaultClient returns a vault client, there is no need for any secretConfig validation
// Since this is called after an already validated call InitVault()
func newVaultClient(secretConfig map[string]string) (*api.Client, error) {
func newVaultClient(clusterdContext *clusterd.Context, namespace string, secretConfig map[string]string) (*api.Client, error) {
// DefaultConfig uses the environment variables if present.
config := api.DefaultConfig()

// Always use a new map otherwise the map will mutate and subsequent calls will fail since the
// TLS content has been altered by the TLS config in vaultClient()
localSecretConfig := make(map[string]string)
for k, v := range secretConfig {
localSecretConfig[k] = v
}

// Convert map string to map interface
c := make(map[string]interface{})
for k, v := range secretConfig {
for k, v := range localSecretConfig {
c[k] = v
}

// Populate TLS config
newConfigWithTLS, removeCertFiles, err := configTLS(clusterdContext, namespace, localSecretConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize vault tls configuration")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be simpler to remove cert files here in the error condition, instead of inside configTLS()?

Suggested change
return nil, errors.Wrap(err, "failed to initialize vault tls configuration")
removeCertFiles()
return nil, errors.Wrap(err, "failed to initialize vault tls configuration")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think removing the files from this function is the better design so that the calling function doesn't have to worry about handling special behavior. It seems like it's normal practice to me for go functions to clean up after themselves.

But I think what you're seeing is that modifying the function in the future, we could forget to keep removeCertFiles() before returning an error in configTLS()...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller already owns calling removeCertFiles() on success, so it seems reasonable to cleanup here on failure as well. Like you said, cleaning up could be missed in an individual error condition inside the method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup is not missed anymore since we use defer in configTLS() now.

}
defer removeCertFiles()

// Populate TLS config
for key, value := range newConfigWithTLS {
c[key] = string(value)
}

// Configure TLS
if err := utils.ConfigureTLS(config, c); err != nil {
return nil, err
Expand All @@ -64,15 +84,15 @@ func newVaultClient(secretConfig map[string]string) (*api.Client, error) {
client.SetToken(strings.TrimSuffix(os.Getenv(api.EnvVaultToken), "\n"))

// Set Vault address, was validated by ValidateConnectionDetails()
err = client.SetAddress(strings.TrimSuffix(secretConfig[api.EnvVaultAddress], "\n"))
err = client.SetAddress(strings.TrimSuffix(localSecretConfig[api.EnvVaultAddress], "\n"))
if err != nil {
return nil, err
}

return client, nil
}

func BackendVersion(secretConfig map[string]string) (string, error) {
func BackendVersion(clusterdContext *clusterd.Context, namespace string, secretConfig map[string]string) (string, error) {
v1 := "v1"
v2 := "v2"

Expand All @@ -91,7 +111,7 @@ func BackendVersion(secretConfig map[string]string) (string, error) {
return v2, nil
default:
// Initialize Vault client
vaultClient, err := vaultClient(secretConfig)
vaultClient, err := vaultClient(clusterdContext, namespace, secretConfig)
if err != nil {
return "", errors.Wrap(err, "failed to initialize vault client")
}
Expand Down
112 changes: 110 additions & 2 deletions pkg/daemon/ceph/osd/kms/vault_api_test.go
Expand Up @@ -17,13 +17,20 @@ limitations under the License.
package kms

import (
"context"
"testing"

kv "github.com/hashicorp/vault-plugin-secrets-kv"
"github.com/hashicorp/vault/api"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/libopenstorage/secrets/vault/utils"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/operator/test"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestBackendVersion(t *testing.T) {
Expand All @@ -35,7 +42,9 @@ func TestBackendVersion(t *testing.T) {
client := cluster.Cores[0].Client

// Mock the client here
vaultClient = func(secretConfig map[string]string) (*api.Client, error) { return client, nil }
vaultClient = func(clusterdContext *clusterd.Context, namespace string, secretConfig map[string]string) (*api.Client, error) {
return client, nil
}

// Set up the kv store
if err := client.Sys().Mount("rook/", &api.MountInput{
Expand Down Expand Up @@ -67,7 +76,7 @@ func TestBackendVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := BackendVersion(tt.args.secretConfig)
got, err := BackendVersion(&clusterd.Context{}, "ns", tt.args.secretConfig)
if (err != nil) != tt.wantErr {
t.Errorf("BackendVersion() error = %v, wantErr %v", err, tt.wantErr)
return
Expand All @@ -91,3 +100,102 @@ func fakeVaultServer(t *testing.T) *vault.TestCluster {

return cluster
}

func TestTLSConfig(t *testing.T) {
ns := "rook-ceph"
ctx := context.TODO()
context := &clusterd.Context{Clientset: test.New(t, 3)}
secretConfig := map[string]string{
"foo": "bar",
"KMS_PROVIDER": "vault",
"VAULT_ADDR": "1.1.1.1",
"VAULT_BACKEND_PATH": "vault",
"VAULT_CACERT": "vault-ca-cert",
"VAULT_CLIENT_CERT": "vault-client-cert",
"VAULT_CLIENT_KEY": "vault-client-key",
}

// DefaultConfig uses the environment variables if present.
config := api.DefaultConfig()

// Convert map string to map interface
c := make(map[string]interface{})
for k, v := range secretConfig {
c[k] = v
}

sCa := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "vault-ca-cert",
Namespace: ns,
},
Data: map[string][]byte{"cert": []byte(`-----BEGIN CERTIFICATE-----
MIIBJTCB0AIJAPNFNz1CNlDOMA0GCSqGSIb3DQEBCwUAMBoxCzAJBgNVBAYTAkZS
MQswCQYDVQQIDAJGUjAeFw0yMTA5MzAwODAzNDBaFw0yNDA2MjYwODAzNDBaMBox
CzAJBgNVBAYTAkZSMQswCQYDVQQIDAJGUjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
QQDHeZ47hVBcryl6SCghM8Zj3Q6DQzJzno1J7EjPXef5m+pIVAEylS9sQuwKtFZc
vv3qS/OVFExmMdbrvfKEIfbBAgMBAAEwDQYJKoZIhvcNAQELBQADQQAAnflLuUM3
4Dq0v7If4cgae2mr7jj3U/lIpHVtFbF7kVjC/eqmeN1a9u0UbRHKkUr+X1mVX3rJ
BvjQDN6didwQ
-----END CERTIFICATE-----`)},
}

sClCert := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "vault-client-cert",
Namespace: ns,
},
Data: map[string][]byte{"cert": []byte(`-----BEGIN CERTIFICATE-----
MIIBEDCBuwIBATANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJGUjELMAkGA1UE
CAwCRlIwHhcNMjEwOTMwMDgwNDA1WhcNMjQwNjI2MDgwNDA1WjANMQswCQYDVQQG
EwJGUjBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCpWJqKhSES3BiFkt2M82xy3tkB
plDS8DM0s/+VkqfZlVG18KbbIVDHi1lsPjjs/Aja7lWymw0ycV4KGEcqxdmNAgMB
AAEwDQYJKoZIhvcNAQEFBQADQQC5esmoTqp4uEWyC+GKbTTFp8ngMUywAtZJs4nS
wdoF3ZJJzo4ps0saP1ww5LBdeeXUURscxyaFfCFmGODaHJJn
-----END CERTIFICATE-----`)},
}

sClKey := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "vault-client-key",
Namespace: ns,
},
Data: map[string][]byte{"key": []byte(`-----BEGIN PRIVATE KEY-----
MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEAqViaioUhEtwYhZLd
jPNsct7ZAaZQ0vAzNLP/lZKn2ZVRtfCm2yFQx4tZbD447PwI2u5VspsNMnFeChhH
KsXZjQIDAQABAkARlCv+oxEq1wQIoZUz83TXe8CFBlGvg9Wc6+5lBWM9F7K4by7i
IB5hQ2oaTNN+1Kxzf+XRM9R7sMPP9qFEp0LhAiEA0PzsQqbvNUVEx8X16Hed6V/Z
yvL1iZeHvc2QIbGjZGkCIQDPcM7U0frsFIPuMY4zpX2b6w4rpxZN7Kybp9/3l0tX
hQIhAJVWVsGeJksLr4WNuRYf+9BbNPdoO/rRNCd2L+tT060ZAiEAl0uontITl9IS
s0yTcZm29lxG9pGkE+uVrOWQ1W0Ud10CIQDJ/L+VCQgjO+SviUECc/nMwhWDMT+V
cjLxGL8tcZjHKg==
-----END PRIVATE KEY-----`)},
}

for _, s := range []*v1.Secret{sCa, sClCert, sClKey} {
if secret, err := context.Clientset.CoreV1().Secrets(ns).Create(ctx, s, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
} else {
defer func() {
err := context.Clientset.CoreV1().Secrets(ns).Delete(ctx, secret.Name, metav1.DeleteOptions{})
if err != nil {
logger.Errorf("failed to delete secret %s: %v", secret.Name, err)
}
}()
}
}

// Populate TLS config
newConfigWithTLS, removeCertFiles, err := configTLS(context, ns, secretConfig)
assert.NoError(t, err)
defer removeCertFiles()

// Populate TLS config
for key, value := range newConfigWithTLS {
c[key] = string(value)
}

// Configure TLS
err = utils.ConfigureTLS(config, c)
assert.NoError(t, err)
}