diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 82d614bc47..a28812bc19 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "context" "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/json" "errors" @@ -54,6 +55,8 @@ type HeadlampConfig struct { oidcClientID string oidcClientSecret string oidcIdpIssuerURL string + oidcSkipTLSVerify bool + oidcCACert string baseURL string oidcScopes []string proxyURLs []string @@ -366,7 +369,7 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { if config.useInCluster { context, err := kubeconfig.GetInClusterContext(config.oidcIdpIssuerURL, config.oidcClientID, config.oidcClientSecret, - strings.Join(config.oidcScopes, ",")) + strings.Join(config.oidcScopes, ","), config.oidcSkipTLSVerify, config.oidcCACert) if err != nil { logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") } @@ -545,6 +548,32 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { http.Error(w, err.Error(), http.StatusInternalServerError) return } + + if oidcAuthConfig.SkipTLSVerify { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + } + insecureClient := &http.Client{Transport: tr} + ctx = oidc.ClientContext(ctx, insecureClient) + } + + if oidcAuthConfig.CACert != "" { + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM([]byte(oidcAuthConfig.CACert)) { + logger.Log(logger.LevelError, map[string]string{"oidc-ca-file": oidcAuthConfig.CACert}, + errors.New("failed to append oidc-ca-file to cert pool"), "failed to append oidc-ca-file to cert pool") + http.Error(w, "invalid oidc-ca-file", http.StatusBadRequest) + return + } + + secureTLSClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: caCertPool}, //nolint:gosec + }, + } + ctx = oidc.ClientContext(ctx, secureTLSClient) + } + provider, err := oidc.NewProvider(ctx, oidcAuthConfig.IdpIssuerURL) if err != nil { logger.Log(logger.LevelError, map[string]string{"idpIssuerURL": oidcAuthConfig.IdpIssuerURL}, diff --git a/backend/cmd/server.go b/backend/cmd/server.go index bca276986a..5b91183595 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -36,7 +36,7 @@ func main() { kubeConfigStore := kubeconfig.NewContextStore() multiplexer := NewMultiplexer(kubeConfigStore) - StartHeadlampServer(&HeadlampConfig{ + headlampConfig := &HeadlampConfig{ useInCluster: conf.InCluster, kubeConfigPath: conf.KubeConfigPath, port: conf.Port, @@ -48,6 +48,7 @@ func main() { oidcClientSecret: conf.OidcClientSecret, oidcIdpIssuerURL: conf.OidcIdpIssuerURL, oidcScopes: strings.Split(conf.OidcScopes, ","), + oidcSkipTLSVerify: conf.OidcSkipTLSVerify, baseURL: conf.BaseURL, proxyURLs: strings.Split(conf.ProxyURLs, ","), enableHelm: conf.EnableHelm, @@ -55,5 +56,17 @@ func main() { cache: cache, kubeConfigStore: kubeConfigStore, multiplexer: multiplexer, - }) + } + + if conf.OidcCAFile != "" { + caFile, err := os.ReadFile(conf.OidcCAFile) + if err != nil { + logger.Log(logger.LevelError, nil, err, "reading oidc-ca-file") + os.Exit(1) + } + + headlampConfig.oidcCACert = string(caFile) + } + + StartHeadlampServer(headlampConfig) } diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index bb110b897e..444bde1e76 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "crypto/x509" "errors" "flag" "fmt" @@ -35,6 +36,8 @@ type Config struct { OidcClientSecret string `koanf:"oidc-client-secret"` OidcIdpIssuerURL string `koanf:"oidc-idp-issuer-url"` OidcScopes string `koanf:"oidc-scopes"` + OidcSkipTLSVerify bool `koanf:"oidc-skip-tls-verify"` + OidcCAFile string `koanf:"oidc-ca-file"` } func (c *Config) Validate() error { @@ -141,6 +144,22 @@ func Parse(args []string) (*Config, error) { } } + if config.OidcSkipTLSVerify { + logger.Log(logger.LevelWarn, nil, nil, "oidc-skip-tls-verify is set, this is not safe for production") + } + + if config.OidcCAFile != "" { + caFile, err := os.ReadFile(config.OidcCAFile) + if err != nil { + return nil, fmt.Errorf("error reading oidc-ca-file: %w", err) + } + // check if the file is a valid PEM file + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caFile) { + return nil, errors.New("invalid oidc-ca-file") + } + } + config.KubeConfigPath = kubeConfigPath return &config, nil @@ -166,6 +185,8 @@ func flagset() *flag.FlagSet { f.String("oidc-idp-issuer-url", "", "Identity provider issuer URL for OIDC") f.String("oidc-scopes", "profile,email", "A comma separated list of scopes needed from the OIDC provider") + f.Bool("oidc-skip-tls-verify", false, "Skip TLS verification for OIDC") + f.String("oidc-ca-file", "", "CA file for OIDC") return f } diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 99aa3aa4a1..48ac35065b 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -48,10 +48,12 @@ type Context struct { } type OidcConfig struct { - ClientID string - ClientSecret string - IdpIssuerURL string - Scopes []string + ClientID string + ClientSecret string + IdpIssuerURL string + Scopes []string + SkipTLSVerify bool + CACert string } // CustomObject represents the custom object that holds the HeadlampInfo regarding custom name. @@ -243,11 +245,25 @@ func (c *Context) OidcConfig() (*OidcConfig, error) { return nil, errors.New("authProvider is nil") } + var caCert string + + caFilePath, ok := c.AuthInfo.AuthProvider.Config["idp-certificate-authority"] + if ok { + caFile, err := os.ReadFile(caFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read idp-ca-file: %w", err) + } + + caCert = string(caFile) + } + return &OidcConfig{ - ClientID: c.AuthInfo.AuthProvider.Config["client-id"], - ClientSecret: c.AuthInfo.AuthProvider.Config["client-secret"], - Scopes: strings.Split(c.AuthInfo.AuthProvider.Config["scope"], ","), - IdpIssuerURL: c.AuthInfo.AuthProvider.Config["idp-issuer-url"], + ClientID: c.AuthInfo.AuthProvider.Config["client-id"], + ClientSecret: c.AuthInfo.AuthProvider.Config["client-secret"], + Scopes: strings.Split(c.AuthInfo.AuthProvider.Config["scope"], ","), + IdpIssuerURL: c.AuthInfo.AuthProvider.Config["idp-issuer-url"], + CACert: caCert, + SkipTLSVerify: c.AuthInfo.AuthProvider.Config["insecure-skip-tls-verify"] == "true", }, nil } @@ -868,6 +884,8 @@ func splitKubeConfigPath(path string) []string { func GetInClusterContext(oidcIssuerURL string, oidcClientID string, oidcClientSecret string, oidcScopes string, + oidcSkipTLSVerify bool, + oidcCACert string, ) (*Context, error) { clusterConfig, err := rest.InClusterConfig() if err != nil { @@ -891,10 +909,12 @@ func GetInClusterContext(oidcIssuerURL string, if oidcClientID != "" && oidcClientSecret != "" && oidcIssuerURL != "" && oidcScopes != "" { oidcConf = &OidcConfig{ - ClientID: oidcClientID, - ClientSecret: oidcClientSecret, - IdpIssuerURL: oidcIssuerURL, - Scopes: strings.Split(oidcScopes, ","), + ClientID: oidcClientID, + ClientSecret: oidcClientSecret, + IdpIssuerURL: oidcIssuerURL, + Scopes: strings.Split(oidcScopes, ","), + SkipTLSVerify: oidcSkipTLSVerify, + CACert: oidcCACert, } }