diff --git a/pkg/client/client.go b/pkg/client/client.go index e44f9e0..77d01b8 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -26,6 +26,11 @@ const ( VaultToken = "VAULT_TOKEN" VaultNamespace = "VAULT_NAMESPACE" VaultSkipTLSVerify = "VAULT_SKIP_VERIFY" + VaultCACert = "VAULT_CACERT" + VaultCAPath = "VAULT_CAPATH" + VaultClientCert = "VAULT_CLIENT_CERT" + VaultClientKey = "VAULT_CLIENT_KEY" + VaultTLSServerName = "VAULT_TLS_SERVER_NAME" VaultHeaderToken = "X-Vault-Token" VaultHeaderNamespace = "X-Vault-Namespace" ) @@ -49,10 +54,32 @@ func NewVaultClient(sessionId string, vaultAddress string, vaultSkipTLSVerify bo config := api.DefaultConfig() config.Address = vaultAddress - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: vaultSkipTLSVerify}, + // Honor the standard Vault TLS environment variables so a private or + // self-signed CA can be trusted without modifying the host trust store. + // ConfigureTLS updates the existing transport's TLSClientConfig in place + // (RootCAs, client certificate, ServerName) instead of replacing it with a + // bare tls.Config, which previously discarded the configured RootCAs. + if err := config.ConfigureTLS(&api.TLSConfig{ + CACert: getEnv(VaultCACert, ""), + CAPath: getEnv(VaultCAPath, ""), + ClientCert: getEnv(VaultClientCert, ""), + ClientKey: getEnv(VaultClientKey, ""), + TLSServerName: getEnv(VaultTLSServerName, ""), + }); err != nil { + return nil, fmt.Errorf("failed to configure Vault TLS: %w", err) + } + + // Apply the caller-resolved skip-verify decision explicitly. ConfigureTLS + // and ReadEnvironment (invoked by DefaultConfig) only ever set + // InsecureSkipVerify to true and never clear it, so set it directly here to + // preserve the context/env precedence resolved by the caller in both + // directions. + if tr, ok := config.HttpClient.Transport.(*http.Transport); ok { + if tr.TLSClientConfig == nil { + tr.TLSClientConfig = &tls.Config{} + } + tr.TLSClientConfig.InsecureSkipVerify = vaultSkipTLSVerify } - config.HttpClient = &http.Client{Transport: tr} client, err := api.NewClient(config) if err != nil { @@ -151,7 +178,7 @@ func CreateVaultClientForSession(ctx context.Context, session server.ClientSessi logger.WithFields(log.Fields{ "session_id": session.SessionID(), "value": envVal, - }).Warn("Invalid boolean value for VAULT_SKIP_VERIFY; using default value false") + }).Warn("Invalid boolean value for VAULT_SKIP_VERIFY; using default value false") } else { vaultSkipTLSVerify = parsed } diff --git a/pkg/client/client_cacert_test.go b/pkg/client/client_cacert_test.go new file mode 100644 index 0000000..7f0c948 --- /dev/null +++ b/pkg/client/client_cacert_test.go @@ -0,0 +1,68 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package client + +import ( + "context" + "encoding/pem" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" +) + +// writeServerCACert PEM-encodes the test server's certificate to a temp file so +// it can be used as a CA bundle via VAULT_CACERT. +func writeServerCACert(t *testing.T, srv *httptest.Server) string { + t.Helper() + caFile := filepath.Join(t.TempDir(), "ca.pem") + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw}) + if err := os.WriteFile(caFile, pemBytes, 0o600); err != nil { + t.Fatalf("write CA cert: %v", err) + } + return caFile +} + +// TestNewVaultClientHonorsCACert verifies that VAULT_CACERT lets the client +// verify a Vault listener presenting a private/self-signed certificate, and +// that without it verification still fails (i.e. we did not silently disable it). +func TestNewVaultClientHonorsCACert(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + caFile := writeServerCACert(t, srv) + + // Keep the test fast: no retry backoff on the expected TLS failure. + t.Setenv("VAULT_MAX_RETRIES", "0") + + doRequest := func(sessionId string) error { + c, err := NewVaultClient(sessionId, srv.URL, false, "test-token", "") + if err != nil { + t.Fatalf("NewVaultClient: %v", err) + } + req := c.NewRequest(http.MethodGet, "/v1/sys/health") + resp, err := c.RawRequestWithContext(context.Background(), req) + if resp != nil { + _ = resp.Body.Close() + } + return err + } + + t.Run("without CA cert verification fails", func(t *testing.T) { + t.Setenv(VaultCACert, "") + if err := doRequest("no-ca"); err == nil { + t.Fatal("expected TLS verification error against self-signed cert, got nil") + } + }) + + t.Run("with CA cert verification succeeds", func(t *testing.T) { + t.Setenv(VaultCACert, caFile) + if err := doRequest("with-ca"); err != nil { + t.Fatalf("expected success with VAULT_CACERT set, got: %v", err) + } + }) +}