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
3 changes: 2 additions & 1 deletion charts/clickhouse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,11 @@ EOSQL
|-----|------|---------|-------------|
| clickhouse.antiAffinity | bool | `false` | |
| clickhouse.antiAffinityScope | string | ClickHouseInstallation | Scope for anti-affinity policy when antiAffinity is enabled. Determines the level at which pod distribution is enforced. Available scopes: - ClickHouseInstallation: Pods from the same installation won't run on the same node (default) - Shard: Pods from the same shard won't run on the same node - Replica: Pods from the same replica won't run on the same node - Cluster: Pods from the same cluster won't run on the same node - Namespace: Pods from the same namespace won't run on the same node |
| clickhouse.clusterSecret | object | `{"auto":true,"enabled":false,"secure":false,"value":"","valueFrom":{"secretKeyRef":{"key":"secret","name":""}}}` | Cluster secret configuration for secure inter-node communication |
| clickhouse.clusterSecret | object | `{"auto":true,"enabled":false,"secure":false,"disableInsecure":false,"value":"","valueFrom":{"secretKeyRef":{"key":"secret","name":""}}}` | Cluster secret configuration for secure inter-node communication |
| clickhouse.clusterSecret.auto | bool | `true` | Auto-generate cluster secret (recommended for security) |
| clickhouse.clusterSecret.enabled | bool | `false` | Whether to enable secret-based cluster communication |
| clickhouse.clusterSecret.secure | bool | `false` | Whether to secure this behind the SSL port |
| clickhouse.clusterSecret.disableInsecure | bool | `false` | Disable insecure ports for the cluster via `insecure: "no"` in the CHI spec. If set, then `clusterSecret.secure` or else the template will error. Before disabling, you MUST ensure your Operator is using HTTPS to interact with the installation ([see here](https://github.com/Altinity/clickhouse-operator/blob/release-0.25.6/docs/security_hardening.md#forcing-https-for-operator-connections) for more details). |
| clickhouse.clusterSecret.value | string | `""` | Plaintext cluster secret value (not recommended for production) |
| clickhouse.clusterSecret.valueFrom | object | `{"secretKeyRef":{"key":"secret","name":""}}` | Reference to an existing Kubernetes secret containing the cluster secret |
| clickhouse.clusterSecret.valueFrom.secretKeyRef.key | string | `"secret"` | Key in the secret that contains the cluster secret value |
Expand Down
9 changes: 9 additions & 0 deletions charts/clickhouse/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ Extra Users
{{- else -}}
{{- end -}}
{{- end -}}
{{/*
Cluster Secret Configuration
*/}}
{{- define "validate.clickhouse.clusterSecretDisableInsecure" -}}
{{- if and .Values.clickhouse.clusterSecret.disableInsecure (not .Values.clickhouse.clusterSecret.secure) }}
{{- fail "clusterSecret.disableInsecure requires `clusterSecret.secure: true`." }}
{{- end -}}
{{- end -}}

{{/*
Common labels
*/}}
Expand Down
8 changes: 8 additions & 0 deletions charts/clickhouse/templates/chi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{{- range .Values.clickhouse.extraPorts }}
{{- $extraPortNames = append $extraPortNames .name }}
{{- end }}
{{- $disableInsecure := and .Values.clickhouse.clusterSecret .Values.clickhouse.clusterSecret.disableInsecure -}}
{{- $serviceHttpsPort := (((.Values.clickhouse).settings).https_port) | default "" -}}
{{- $serviceSecureTcpPort := (((.Values.clickhouse).settings).tcp_port_secure) | default "" -}}
---
Expand Down Expand Up @@ -55,12 +56,14 @@ spec:
spec:
type: {{ .Values.clickhouse.service.type }}
ports:
{{- if not $disableInsecure }}
- name: http
port: 8123
targetPort: 8123
- name: tcp
port: 9000
targetPort: 9000
{{- end }}
{{- if .Values.clickhouse.extraPorts }}
{{- range .Values.clickhouse.extraPorts }}
- name: {{ .name }}
Expand Down Expand Up @@ -99,12 +102,14 @@ spec:
{{- toYaml .Values.clickhouse.lbService.loadBalancerSourceRanges | nindent 12 }}
{{- end }}
ports:
{{- if not $disableInsecure }}
- name: http
port: 8123
targetPort: 8123
- name: tcp
port: 9000
targetPort: 9000
{{- end }}
{{- if .Values.clickhouse.extraPorts }}
{{- range .Values.clickhouse.extraPorts }}
- name: {{ .name }}
Expand Down Expand Up @@ -226,6 +231,9 @@ spec:
{{- if .Values.clickhouse.clusterSecret.secure }}
secure: "yes"
{{- end }}
{{- if $disableInsecure }}
insecure: "no"
{{- end }}
secret:
{{- if .Values.clickhouse.clusterSecret.auto }}
auto: "true"
Expand Down
2 changes: 2 additions & 0 deletions charts/clickhouse/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ clickhouse:
enabled: false
# -- Whether to secure this behind the SSL port
secure: false
# -- Disable insecure ports
disableInsecure: false
# -- Auto-generate cluster secret (recommended for security)
auto: true
# -- Plaintext cluster secret value (not recommended for production)
Expand Down
9 changes: 8 additions & 1 deletion tests/fixtures/10-tls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ clickhouse:

settings:
https_port: 8444
tcp_port_secure: 9440

clusterSecret:
enabled: true
secure: true
disableInsecure: true
auto: true

configurationFiles:
# To regenerate the public certificate:
Expand Down Expand Up @@ -80,7 +87,7 @@ clickhouse:
<certificateFile>/etc/clickhouse-server/config.d/foo.crt</certificateFile>
<privateKeyFile>/etc/clickhouse-server/secrets.d/bar.key/clickhouse-certs/server.key</privateKeyFile>
<dhParamsFile>/etc/clickhouse-server/secrets.d/dhparam.pem/clickhouse-certs/dhparam.pem</dhParamsFile>
<verificationMode>relaxed</verificationMode>
<verificationMode>none</verificationMode>
<loadDefaultCAFile>true</loadDefaultCAFile>
<cacheSessions>true</cacheSessions>
<disableProtocols>sslv2,sslv3</disableProtocols>
Expand Down
9 changes: 9 additions & 0 deletions tests/scenarios/smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ def check_deployment(self, fixture_file, skip_external_keeper=True):
tls.verify_settings_ports_in_chi(
namespace=namespace,
expected_https_port=https_port,
expected_tcp_port_secure=9440,
)

tls.verify_insecure_disabled_in_chi(
namespace=namespace,
)

tls.verify_service_ports_secure_only(
namespace=namespace,
)

with And("verify HTTPS endpoint certificate"):
Expand Down
22 changes: 19 additions & 3 deletions tests/steps/clickhouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ def wait_until(check_fn, timeout=60, interval=5, timeout_msg="Operation timed ou
@TestStep(When)
def get_version(self, namespace, pod_name, user="default", password=""):
"""Get ClickHouse version from the specified pod."""
secure_flag = "--secure --accept-invalid-certificate" if get_chi_secure_flag(namespace=namespace) else ""
auth_args = f"-u {user}" if user else ""
if password:
auth_args += f" --password {password}"

version = run(
cmd=f"kubectl exec -n {namespace} {pod_name} "
f"-- clickhouse-client {auth_args} -q 'SELECT version()'"
f"-- clickhouse-client {secure_flag} {auth_args} -q 'SELECT version()'"
)
return version.stdout.strip()

Expand All @@ -55,6 +56,7 @@ def execute_clickhouse_query(
self, namespace, pod_name, query, user="default", password="", check=True
):
"""Execute a ClickHouse query on a specific pod."""
secure_flag = "--secure --accept-invalid-certificate" if get_chi_secure_flag(namespace=namespace) else ""
auth_args = f"-u {user}" if user else ""
if password:
auth_args += f" --password {password}"
Expand All @@ -63,7 +65,7 @@ def execute_clickhouse_query(

result = run(
cmd=f"kubectl exec -n {namespace} {pod_name} "
f"-- clickhouse-client {auth_args} -q '{escaped_query}'",
f"-- clickhouse-client {secure_flag} {auth_args} -q '{escaped_query}'",
check=check,
)
return result
Expand All @@ -73,9 +75,10 @@ def execute_clickhouse_query(
def test_clickhouse_connection(self, namespace, pod_name, user, password):
"""Test ClickHouse connection with given credentials."""
try:
secure_flag = "--secure --accept-invalid-certificate" if get_chi_secure_flag(namespace=namespace) else ""
result = run(
cmd=f"kubectl exec -n {namespace} {pod_name} "
f"-- clickhouse-client -u {user} --password {password} "
f"-- clickhouse-client {secure_flag} -u {user} --password {password} "
f"-q 'SELECT 1'",
check=False,
)
Expand Down Expand Up @@ -106,6 +109,19 @@ def get_chi_info(self, namespace):
return None


_secure_flag_cache = {}

@TestStep(When)
def get_chi_secure_flag(self, namespace):
"""Check if the CHI cluster has secure: 'yes' enabled. Result is cached per namespace."""
if namespace not in _secure_flag_cache:
try:
_secure_flag_cache[namespace] = get_chi_info(namespace=namespace)["spec"]["configuration"]["clusters"][0]["secure"] == "yes"
except (TypeError, KeyError, IndexError):
_secure_flag_cache[namespace] = False
return _secure_flag_cache[namespace]


@TestStep(When)
def get_clickhouse_pods(self, namespace):
"""Get ClickHouse pods (excluding operator pods)."""
Expand Down
48 changes: 44 additions & 4 deletions tests/steps/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@ def verify_tls_secret_references_in_chi(self, namespace):


@TestStep(Then)
def verify_settings_ports_in_chi(self, namespace, expected_https_port):
def verify_settings_ports_in_chi(self, namespace, expected_https_port, expected_tcp_port_secure):
"""Verify settings block has correct port configuration in CHI spec."""
chi_data = clickhouse.get_chi_info(namespace=namespace)

settings = chi_data.get("spec", {}).get("configuration", {}).get("settings", {})
assert settings.get("https_port") == expected_https_port, \
f"Expected https_port: {expected_https_port}, got: {settings.get('https_port')!r}"
assert "tcp_port_secure" not in settings, \
f"Did not expect 'tcp_port_secure' in settings, but found: {settings.get('tcp_port_secure')!r}"
note(f"✓ Settings block only has https_port as explicitly set: {expected_https_port}")
note(f"✓ https_port: {expected_https_port}")

assert settings.get("tcp_port_secure") == expected_tcp_port_secure, \
f"Expected tcp_port_secure: {expected_tcp_port_secure}, got: {settings.get('tcp_port_secure')!r}"
note(f"✓ tcp_port_secure: {expected_tcp_port_secure}")


@TestStep(Then)
Expand Down Expand Up @@ -125,6 +127,44 @@ def verify_tls_files_on_pod(self, namespace):
note("✓ DH params valid (g=2)")


@TestStep(Then)
def verify_insecure_disabled_in_chi(self, namespace):
"""Verify insecure: 'no' and secure: 'yes' are set on the cluster in CHI spec."""
chi_data = clickhouse.get_chi_info(namespace=namespace)

clusters = chi_data.get("spec", {}).get("configuration", {}).get("clusters", [])
assert len(clusters) > 0, "No clusters found in CHI spec"

cluster = clusters[0]
assert cluster.get("secure") == "yes", \
f"Expected secure: 'yes', got: {cluster.get('secure')!r}"
assert cluster.get("insecure") == "no", \
f"Expected insecure: 'no', got: {cluster.get('insecure')!r}"

note("✓ Cluster has secure: 'yes' and insecure: 'no'")


@TestStep(Then)
def verify_service_ports_secure_only(self, namespace):
"""Verify service templates only expose secure ports (no 8123/9000)."""
chi_data = clickhouse.get_chi_info(namespace=namespace)

service_templates = chi_data.get("spec", {}).get("templates", {}).get("serviceTemplates", [])
assert len(service_templates) > 0, "No service templates found in CHI spec"

for svc in service_templates:
svc_name = svc.get("name", "unknown")
ports = svc.get("spec", {}).get("ports", [])
port_numbers = [p.get("port") for p in ports]

assert 8123 not in port_numbers, \
f"Service '{svc_name}' still has insecure HTTP port 8123"
assert 9000 not in port_numbers, \
f"Service '{svc_name}' still has insecure TCP port 9000"

note(f"✓ Service '{svc_name}' has only secure ports: {port_numbers}")


@TestStep(Then)
def verify_https_certificate(self, namespace, https_port):
"""Verify the HTTPS endpoint serves TLS with the correct certificate.
Expand Down