mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-06-17 05:37:33 +00:00
cli: authenticate Hub HTTP requests with License-Key (gated-auth phase 1)
Add a License-Key RoundTripper plus NewHubHTTPClient / HubAuthTransport helpers in utils, and route the MCP runner's Hub clients (connect, --url validate, file download, tools list) through them so the CLI keeps working once the Hub enforces auth. License-Key is a custom header, so it survives the kube API-server service proxy (a bearer token would not). Surface ErrHubAuthRequired on a 401/302 from the Hub. console already sends License-Key on its ws connection. pprof is not covered here: it shells out to 'go tool pprof <hubUrl>' / opens a browser, external fetches that can't carry a custom header (follow-up).
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/kubeshark/kubeshark/internal/connect"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -167,7 +168,7 @@ func runMCPWithConfig(setFlags []string, directURL string, allowDestructive bool
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
server := &mcpServer{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
httpClient: utils.NewHubHTTPClient(30*time.Second, config.Config.License),
|
||||
stdin: os.Stdin,
|
||||
stdout: os.Stdout,
|
||||
setFlags: setFlags,
|
||||
@@ -195,7 +196,7 @@ func (s *mcpServer) validateDirectURL() error {
|
||||
s.directURL = urlStr
|
||||
|
||||
// Use a short timeout for validation
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := utils.NewHubHTTPClient(10*time.Second, config.Config.License)
|
||||
|
||||
// Try to reach the MCP API base endpoint which returns tool definitions
|
||||
testURL := fmt.Sprintf("%s/api/mcp", urlStr)
|
||||
@@ -205,6 +206,10 @@ func (s *mcpServer) validateDirectURL() error {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if utils.IsAuthRequired(resp) {
|
||||
return utils.ErrHubAuthRequired
|
||||
}
|
||||
|
||||
// Try to parse the MCP response to validate it's a valid Kubeshark endpoint
|
||||
var mcpInfo hubMCPResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mcpInfo); err != nil {
|
||||
@@ -820,10 +825,10 @@ func (s *mcpServer) callDownloadFile(args map[string]any) (string, bool) {
|
||||
// The default s.httpClient has a 30s total timeout which would fail for large files (up to 10GB).
|
||||
// This client sets only connection-level timeouts and lets the body stream without a deadline.
|
||||
downloadClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Transport: utils.HubAuthTransport(config.Config.License, &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
resp, err := downloadClient.Get(fullURL)
|
||||
@@ -1145,7 +1150,7 @@ func establishProxyConnection(timeout time.Duration) (string, error) {
|
||||
|
||||
// fetchAndDisplayTools fetches tools from the Kubeshark API and displays them
|
||||
func fetchAndDisplayTools(hubURL string, timeout time.Duration) {
|
||||
client := &http.Client{Timeout: timeout}
|
||||
client := utils.NewHubHTTPClient(timeout, config.Config.License)
|
||||
|
||||
// Fetch tools list from /api/mcp endpoint
|
||||
resp, err := client.Get(strings.TrimSuffix(hubURL, "/mcp") + "/mcp")
|
||||
|
||||
@@ -2,17 +2,67 @@ package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
X_KUBESHARK_CAPTURE_HEADER_KEY = "X-Kubeshark-Capture"
|
||||
X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE = "ignore"
|
||||
LICENSE_KEY_HEADER = "License-Key"
|
||||
)
|
||||
|
||||
// ErrHubAuthRequired indicates the Hub rejected the request because auth is
|
||||
// enabled but no valid credential was presented (missing/expired license).
|
||||
var ErrHubAuthRequired = errors.New("hub requires authentication: set a valid license (config 'license') or credentials")
|
||||
|
||||
// licenseKeyRoundTripper attaches the License-Key header to every request so
|
||||
// any client built with it authenticates to a gated Hub. License-Key is a
|
||||
// custom (non-Authorization) header so it survives the kube API-server
|
||||
// service proxy, unlike a bearer token.
|
||||
type licenseKeyRoundTripper struct {
|
||||
licenseKey string
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt *licenseKeyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
base := rt.base
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
if rt.licenseKey != "" && req.Header.Get(LICENSE_KEY_HEADER) == "" {
|
||||
req = req.Clone(req.Context())
|
||||
req.Header.Set(LICENSE_KEY_HEADER, rt.licenseKey)
|
||||
}
|
||||
return base.RoundTrip(req)
|
||||
}
|
||||
|
||||
// NewHubHTTPClient returns an *http.Client that authenticates to the Hub by
|
||||
// attaching the License-Key header to every request.
|
||||
func NewHubHTTPClient(timeout time.Duration, licenseKey string) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &licenseKeyRoundTripper{licenseKey: licenseKey},
|
||||
}
|
||||
}
|
||||
|
||||
// HubAuthTransport wraps base so requests carry the License-Key header. Use
|
||||
// when a client needs custom transport settings (e.g. streaming downloads)
|
||||
// but must still authenticate to the Hub.
|
||||
func HubAuthTransport(licenseKey string, base http.RoundTripper) http.RoundTripper {
|
||||
return &licenseKeyRoundTripper{licenseKey: licenseKey, base: base}
|
||||
}
|
||||
|
||||
// IsAuthRequired reports whether the response indicates the Hub demanded
|
||||
// authentication — a 401, or a 302 redirect to an SSO login page.
|
||||
func IsAuthRequired(resp *http.Response) bool {
|
||||
return resp != nil && (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusFound)
|
||||
}
|
||||
|
||||
// Get - When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
func Get(url string, client *http.Client) (*http.Response, error) {
|
||||
|
||||
Reference in New Issue
Block a user