Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/metrics_exporter/app/metrics_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func Run() {
log.Infof("Starting metrics exporter. Version:%s GitSHA:%s BuiltAt:%s\n", version.Version, version.GitSHA, version.BuiltAt)

// Initialize k8s API clients
kubeClient, _, chopClient, _ := chop.GetClientset(kubeConfigFile, masterURL)
kubeClient, _, chopClient, _ := chop.GetClientset(kubeConfigFile, masterURL, chopConfigFile)

// Create operator instance
chop.New(kubeClient, chopClient, chopConfigFile)
Expand Down
2 changes: 1 addition & 1 deletion cmd/operator/app/thread_chi.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func initClickHouse(ctx context.Context) {
}

// Initialize k8s API clients
kubeClient, extClient, chopClient, dynamicClient := chop.GetClientset(kubeConfigFile, masterURL)
kubeClient, extClient, chopClient, dynamicClient := chop.GetClientset(kubeConfigFile, masterURL, chopConfigFile)

// Create operator instance. The chopconf load inside chop.New gates on
// clickhouse.security.kubernetes.allowInsecure BEFORE the first network call,
Expand Down
2 changes: 1 addition & 1 deletion cmd/operator/app/thread_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func initKeeper(ctx context.Context) error {

// Build the apiextensions client for CRD deletion checks during CHK cleanup.
// Uses the same kubeConfigFile/masterURL package vars as the CHI thread.
_, extClient, _, _ := chop.GetClientset(kubeConfigFile, masterURL)
_, extClient, _, _ := chop.GetClientset(kubeConfigFile, masterURL, chopConfigFile)

err = ctrlRuntime.
NewControllerManagedBy(manager).
Expand Down
14 changes: 14 additions & 0 deletions pkg/apis/clickhouse.altinity.com/v1/type_configuration_chop.go
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,20 @@ func (c *OperatorConfig) RequiresStrictK8sTLS() bool {
c.Security.GetKubernetes().GetTLS().GetVerify() == TLSVerifyStrict
}

// ResolveK8sTLSMinVersion returns the K8s-API TLS floor for GetClientset. Under
// hardened posture (policy=Enforced or fips.enforced) returns TLSMinVersion13;
// otherwise security.kubernetes.tls.minVersion. Callable on raw file-based config
// before applyEnforcedHardening runs. Empty = Go stdlib default.
func (c *OperatorConfig) ResolveK8sTLSMinVersion() TLSMinVersion {
if c == nil {
return TLSMinVersion("")
}
if c.Security.RequiresHardening() {
return TLSMinVersion13
}
return c.Security.GetKubernetes().GetTLS().GetMinVersion()
}

// coerceTypedString one-way coerces a *types.String-valued config field (TLSVerify,
// TLSMinVersion, IPCMode — all type aliases of types.String) to the FIPS-strict
// target value and logs the change. Caller passes the address of the struct field
Expand Down
17 changes: 5 additions & 12 deletions pkg/apis/clickhouse.altinity.com/v1/type_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,8 @@ type ClusterSecurityKubernetes struct {
}

// ClusterSecurityKubernetesTLS holds knobs for the operator's outbound
// Kubernetes API client. The k8s client-go respects whatever's in the
// kubeconfig; the operator never builds the kubeconfig's tls.Config itself,
// so these knobs are evaluated as a LOAD-TIME GATE — the operator refuses
// to start with a kubeconfig that doesn't meet the requested posture.
// Kubernetes API client. Verify is a load-time gate against the kubeconfig's
// Insecure flag; MinVersion is applied to the client transport by GetClientset.
type ClusterSecurityKubernetesTLS struct {
// Verify gates startup against the kubeconfig's TLSClientConfig.Insecure
// field. Valid values are TLSVerifyStrict and TLSVerifyNone.
Expand All @@ -77,12 +75,8 @@ type ClusterSecurityKubernetesTLS struct {
// override the kubeconfig; it only refuses to load an insecure one.
Verify *types.String `json:"verify,omitempty" yaml:"verify,omitempty"`
// MinVersion floors TLS at the named protocol version. Valid values are
// TLSMinVersion12 and TLSMinVersion13. Nil = Go stdlib default.
//
// Declared for shape symmetry with ClickHouse/Zookeeper and coerced under
// FIPS, but not yet enforced on the operator's K8s API transport — a future
// enhancement will wire it into rest.Config when the operator wraps the
// kubeconfig with stricter TLS settings.
// TLSMinVersion12 and TLSMinVersion13. Nil = Go stdlib default. Coerced to
// 1.3 under FIPS/Enforced; applied on the K8s API transport by GetClientset.
MinVersion *types.String `json:"minVersion,omitempty" yaml:"minVersion,omitempty"`
}

Expand Down Expand Up @@ -420,8 +414,7 @@ func (t *ClusterSecurityKubernetesTLS) GetVerify() TLSVerify {
}

// GetMinVersion returns the resolved TLSMinVersion for the operator's K8s client.
// Nil-safe; returns empty value when unset. Declared for shape consistency and
// FIPS coercion uniformity — not yet wired into the K8s API transport.
// Nil-safe; returns empty value when unset.
func (t *ClusterSecurityKubernetesTLS) GetMinVersion() TLSMinVersion {
if (t == nil) || (t.MinVersion == nil) || !t.MinVersion.HasValue() {
return TLSMinVersion("")
Expand Down
30 changes: 30 additions & 0 deletions pkg/apis/clickhouse.altinity.com/v1/type_security_fips_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ func TestSecurity_GetFIPS_IsEnforced_NilSafe(t *testing.T) {
require.True(t, (&OperatorConfigSecurity{FIPS: &OperatorConfigSecurityFIPS{Enforced: types.NewStringBool(true)}}).GetFIPS().IsEnforced())
}

// TestResolveK8sTLSMinVersion verifies hardened posture forces TLS 1.3 and that
// explicit security.kubernetes.tls.minVersion is honored when not hardened.
func TestResolveK8sTLSMinVersion(t *testing.T) {
require.Equal(t, TLSMinVersion(""), (*OperatorConfig)(nil).ResolveK8sTLSMinVersion())
require.Equal(t, TLSMinVersion(""), (&OperatorConfig{}).ResolveK8sTLSMinVersion())

explicit12 := &OperatorConfig{}
explicit12.Security.Kubernetes = &ClusterSecurityKubernetes{
TLS: &ClusterSecurityKubernetesTLS{MinVersion: types.NewString(string(TLSMinVersion12))},
}
require.Equal(t, TLSMinVersion12, explicit12.ResolveK8sTLSMinVersion())

explicit13 := &OperatorConfig{}
explicit13.Security.Kubernetes = &ClusterSecurityKubernetes{
TLS: &ClusterSecurityKubernetesTLS{MinVersion: types.NewString(string(TLSMinVersion13))},
}
require.Equal(t, TLSMinVersion13, explicit13.ResolveK8sTLSMinVersion())

enforcedOver12 := &OperatorConfig{}
enforcedOver12.Security.Policy = types.NewString(string(SecurityPolicyEnforced))
enforcedOver12.Security.Kubernetes = &ClusterSecurityKubernetes{
TLS: &ClusterSecurityKubernetesTLS{MinVersion: types.NewString(string(TLSMinVersion12))},
}
require.Equal(t, TLSMinVersion13, enforcedOver12.ResolveK8sTLSMinVersion())

fipsForced := &OperatorConfig{}
fipsForced.Security.FIPS = &OperatorConfigSecurityFIPS{Enforced: types.NewStringBool(true)}
require.Equal(t, TLSMinVersion13, fipsForced.ResolveK8sTLSMinVersion())
}

// TestSecurity_RequiresHardening_NilSafe verifies the union accessor used to
// gate per-CR security checks (plain-text ZK rejection, FIPS-bypass rejection,
// ZK digest-auth rejection). Fires when EITHER security.policy=Enforced OR
Expand Down
48 changes: 46 additions & 2 deletions pkg/chop/kube_machinery.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package chop

import (
"crypto/tls"
"fmt"
"net/http"
"os"
"os/user"
"path/filepath"
Expand All @@ -30,6 +32,7 @@ import (
log "github.com/altinity/clickhouse-operator/pkg/announcer"
"github.com/altinity/clickhouse-operator/pkg/apis/deployment"
chopclientset "github.com/altinity/clickhouse-operator/pkg/client/clientset/versioned"
"github.com/altinity/clickhouse-operator/pkg/util/tlsutil"
)

// lastKubeConfigInsecure records whether the most recently loaded kubeconfig
Expand Down Expand Up @@ -80,8 +83,11 @@ func getKubeConfig(kubeConfigFile, masterURL string) (*kuberest.Config, error) {
return captureInsecure(conf, nil)
}

// GetClientset gets k8s API clients - both kube native client and our custom client
func GetClientset(kubeConfigFile, masterURL string) (
// GetClientset gets k8s API clients - both kube native client and our custom client.
// chopConfigFile supplies the file-based chopconf path used to resolve the K8s-API
// TLS minVersion floor before the first network call (same timing as the
// insecure-kubeconfig gate in ConfigManager.Init).
func GetClientset(kubeConfigFile, masterURL, chopConfigFile string) (
*kube.Clientset,
*apiextensions.Clientset,
*chopclientset.Clientset,
Expand All @@ -93,6 +99,10 @@ func GetClientset(kubeConfigFile, masterURL string) (
os.Exit(1)
}

if minVer := tlsutil.VersionUint16(string(resolveK8sTLSMinVersion(chopConfigFile))); minVer != 0 {
applyK8sClientTLSMinVersion(kubeConfig, minVer)
}

// Layer on k8s client rate limiting overrides if specified in CHOP config.
if maybeQps := os.Getenv(deployment.OPERATOR_K8S_CLIENT_QPS_LIMIT); maybeQps != "" {
parsedQps, err := strconv.ParseFloat(maybeQps, 32)
Expand Down Expand Up @@ -139,3 +149,37 @@ func GetClientset(kubeConfigFile, masterURL string) (

return kubeClientset, apiextensionsClientset, chopClientset, dynamicClientset
}

// resolveK8sTLSMinVersion reads the file-based chopconf and returns the effective
// K8s-API TLS floor ("1.2"|"1.3"|""). Uses a nil-client ConfigManager because
// file loading never touches the API. Errors yield "" (no floor).
func resolveK8sTLSMinVersion(chopConfigFile string) string {
cm := newConfigManager(nil, nil, chopConfigFile)
fileConfig, err := cm.getFileBasedConfig(chopConfigFile)
if err != nil || fileConfig == nil {
return ""
}
return string(fileConfig.ResolveK8sTLSMinVersion())
}

// applyK8sClientTLSMinVersion stamps MinVersion onto the rest.Config transport
// via rest.Config.Wrap, preserving client-go's TLS/proxy/HTTP2 setup. Warns if
// the built RoundTripper is not *http.Transport.
func applyK8sClientTLSMinVersion(cfg *kuberest.Config, minVer uint16) {
cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
t, ok := rt.(*http.Transport)
if !ok {
log.F().Warning(
"k8s client TLS minVersion floor requested (0x%04x) but transport is %T, not *http.Transport — floor NOT applied",
minVer, rt,
)
return rt
}
if t.TLSClientConfig == nil {
t.TLSClientConfig = &tls.Config{}
}
t.TLSClientConfig.MinVersion = minVer
log.F().Info("k8s client TLS minVersion floor applied: 0x%04x", minVer)
return rt
})
}
Loading