diff --git a/README.md b/README.md index a70d2dc..28b5156 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ The following environment variables are **required** for configuring the Sysdig You can also set the following variables to override the default configuration: - `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`. +- `SYSDIG_MCP_API_SKIP_TLS_VERIFICATION`: Whether to skip TLS verification for the Sysdig API connection (useful for self-signed certificates). Defaults to: `false`. - `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server` - `SYSDIG_MCP_LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO` - `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`streamable-http`, `sse`). Defaults to: `8080` diff --git a/cmd/server/main.go b/cmd/server/main.go index 0657db3..1ba8c12 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "fmt" "log/slog" "net/http" @@ -82,13 +83,26 @@ func setupLogger(logLevel string) { } func setupSysdigClient(cfg *config.Config) (sysdig.ExtendedClientWithResponsesInterface, error) { - sysdigClient, err := sysdig.NewSysdigClient( + sysdigClientOptions := []sysdig.IntoClientOption{ sysdig.WithVersion(Version), sysdig.WithFallbackAuthentication( sysdig.WithHostAndTokenFromContext(), sysdig.WithFixedHostAndToken(cfg.APIHost, cfg.APIToken), ), - ) + } + + if cfg.SkipTLSVerification { + transport := http.DefaultTransport.(*http.Transport).Clone() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true + httpClient := &http.Client{Transport: transport} + + sysdigClientOptions = append(sysdigClientOptions, sysdig.WithHTTPClient(httpClient)) + } + + sysdigClient, err := sysdig.NewSysdigClient(sysdigClientOptions...) if err != nil { return nil, fmt.Errorf("error creating sysdig client: %w", err) } diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 2da345a..05f97a8 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -6,6 +6,9 @@ **Problem**: "unable to authenticate with any method" - **Solution**: For `stdio`, verify `SYSDIG_MCP_API_HOST` and `SYSDIG_MCP_API_TOKEN` env vars are set correctly. For remote transports, check `Authorization: Bearer ` header format. +**Problem**: Connection failing with "certificate signed by unknown authority" +- **Solution**: If using a self-signed certificate (e.g. on-prem), set `SYSDIG_MCP_API_SKIP_TLS_VERIFICATION=true`. + **Problem**: Tests failing with "command not found" - **Solution**: Enter Nix shell with `nix develop` or `direnv allow`. All dev tools are provided by the flake. diff --git a/internal/config/config.go b/internal/config/config.go index 7a14991..ad1f76e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,16 +3,19 @@ package config import ( "fmt" "os" + "strconv" + "strings" ) type Config struct { - APIHost string - APIToken string - Transport string - ListeningHost string - ListeningPort string - MountPath string - LogLevel string + APIHost string + APIToken string + SkipTLSVerification bool + Transport string + ListeningHost string + ListeningPort string + MountPath string + LogLevel string } func (c *Config) Validate() error { @@ -27,13 +30,14 @@ func (c *Config) Validate() error { func Load() (*Config, error) { cfg := &Config{ - APIHost: getEnv("SYSDIG_MCP_API_HOST", ""), - APIToken: getEnv("SYSDIG_MCP_API_TOKEN", ""), - Transport: getEnv("SYSDIG_MCP_TRANSPORT", "stdio"), - ListeningHost: getEnv("SYSDIG_MCP_LISTENING_HOST", "localhost"), - ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"), - MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"), - LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"), + APIHost: getEnv("SYSDIG_MCP_API_HOST", ""), + APIToken: getEnv("SYSDIG_MCP_API_TOKEN", ""), + SkipTLSVerification: getEnv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", false), + Transport: getEnv("SYSDIG_MCP_TRANSPORT", "stdio"), + ListeningHost: getEnv("SYSDIG_MCP_LISTENING_HOST", "localhost"), + ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"), + MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"), + LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"), } if err := cfg.Validate(); err != nil { @@ -43,9 +47,32 @@ func Load() (*Config, error) { return cfg, nil } -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value +type envType interface { + ~string | ~bool +} + +func getEnv[T envType](key string, fallback T) T { + value, ok := os.LookupEnv(key) + if !ok { + return fallback } + + switch any(fallback).(type) { + case string: + return any(value).(T) + + case bool: + value = strings.TrimSpace(value) + if value == "" { + return fallback + } + + b, err := strconv.ParseBool(value) + if err != nil { + return fallback + } + return any(b).(T) + } + return fallback } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a6d47ea..2787a02 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -82,6 +82,7 @@ var _ = Describe("Config", func() { Expect(cfg.ListeningPort).To(Equal("8080")) Expect(cfg.MountPath).To(Equal("/sysdig-mcp-server")) Expect(cfg.LogLevel).To(Equal("INFO")) + Expect(cfg.SkipTLSVerification).To(BeFalse()) }) }) @@ -106,6 +107,7 @@ var _ = Describe("Config", func() { BeforeEach(func() { _ = os.Setenv("SYSDIG_MCP_API_HOST", "env-host") _ = os.Setenv("SYSDIG_MCP_API_TOKEN", "env-token") + _ = os.Setenv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", "true") _ = os.Setenv("SYSDIG_MCP_TRANSPORT", "http") _ = os.Setenv("SYSDIG_MCP_LISTENING_HOST", "0.0.0.0") _ = os.Setenv("SYSDIG_MCP_LISTENING_PORT", "9090") @@ -118,6 +120,7 @@ var _ = Describe("Config", func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg.APIHost).To(Equal("env-host")) Expect(cfg.APIToken).To(Equal("env-token")) + Expect(cfg.SkipTLSVerification).To(BeTrue()) Expect(cfg.Transport).To(Equal("http")) Expect(cfg.ListeningHost).To(Equal("0.0.0.0")) Expect(cfg.ListeningPort).To(Equal("9090")) @@ -126,6 +129,20 @@ var _ = Describe("Config", func() { }) }) + Context("with invalid boolean env var", func() { + BeforeEach(func() { + _ = os.Setenv("SYSDIG_MCP_API_HOST", "host") + _ = os.Setenv("SYSDIG_MCP_API_TOKEN", "token") + _ = os.Setenv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", "invalid-bool") + }) + + It("should fall back to default value", func() { + cfg, err := config.Load() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.SkipTLSVerification).To(BeFalse()) + }) + }) + Context("without required env vars", func() { It("should return an error", func() { _, err := config.Load() diff --git a/internal/infra/sysdig/client.go b/internal/infra/sysdig/client.go index fc638e6..c7d802a 100644 --- a/internal/infra/sysdig/client.go +++ b/internal/infra/sysdig/client.go @@ -88,10 +88,22 @@ func WithVersion(version string) RequestEditorFn { } } -func NewSysdigClient(requestEditors ...RequestEditorFn) (ExtendedClientWithResponsesInterface, error) { +type IntoClientOption interface { + AsClientOption() ClientOption +} + +func (r RequestEditorFn) AsClientOption() ClientOption { + return WithRequestEditorFn(r) +} + +func (c ClientOption) AsClientOption() ClientOption { + return c +} + +func NewSysdigClient(requestEditors ...IntoClientOption) (ExtendedClientWithResponsesInterface, error) { editors := make([]ClientOption, len(requestEditors)) for i, e := range requestEditors { - editors[i] = WithRequestEditorFn(e) + editors[i] = e.AsClientOption() } return NewClientWithResponses("", editors...) diff --git a/internal/infra/sysdig/client_test.go b/internal/infra/sysdig/client_test.go new file mode 100644 index 0000000..d0c1848 --- /dev/null +++ b/internal/infra/sysdig/client_test.go @@ -0,0 +1,69 @@ +package sysdig_test + +import ( + "context" + "crypto/tls" + "log" + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig" +) + +var _ = Describe("Client TLS", func() { + var ts *httptest.Server + + BeforeEach(func() { + // Start a TLS server with a self-signed certificate + ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/users/me/permissions" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"permissions":[]}`)) + return + } + w.WriteHeader(http.StatusNotFound) + })) + // Redirect server logs to GinkgoWriter to avoid noise in test output + ts.Config.ErrorLog = log.New(GinkgoWriter, "", 0) + }) + + AfterEach(func() { + ts.Close() + }) + + It("should fail request with self-signed cert by default", func() { + // Create client pointing to the TLS server without custom transport + client, err := sysdig.NewSysdigClient( + sysdig.WithFixedHostAndToken(ts.URL, "dummy-token"), + ) + Expect(err).NotTo(HaveOccurred()) + + // Attempt request + _, err = client.GetMyPermissionsWithResponse(context.Background()) + Expect(err).To(HaveOccurred()) + // Verification that it failed due to certificate issues + Expect(err.Error()).To(Or(ContainSubstring("certificate"), ContainSubstring("unknown authority"))) + }) + + It("should succeed request when using custom HTTP client with InsecureSkipVerify", func() { + // Create custom HTTP client that skips verification + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + httpClient := &http.Client{Transport: transport} + + // Create client using the custom HTTP client + client, err := sysdig.NewSysdigClient( + sysdig.WithFixedHostAndToken(ts.URL, "dummy-token"), + sysdig.WithHTTPClient(httpClient), + ) + Expect(err).NotTo(HaveOccurred()) + + // Attempt request + resp, err := client.GetMyPermissionsWithResponse(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.HTTPResponse.StatusCode).To(Equal(http.StatusOK)) + }) +}) diff --git a/package.nix b/package.nix index 158ce18..b79dcd5 100644 --- a/package.nix +++ b/package.nix @@ -1,7 +1,7 @@ { buildGoModule, versionCheckHook }: buildGoModule (finalAttrs: { pname = "sysdig-mcp-server"; - version = "0.5.4"; + version = "0.6.0"; src = ./.; # This hash is automatically re-calculated with `just rehash-package-nix`. This is automatically called as well by `just bump`. vendorHash = "sha256-jf/px0p88XbfuSPMry/qZcfR0QPTF9IrPegg2CwAd6M=";