ClickHouse with the default user wide open — fixing the four classic mistakes

Four checks and fixes for the misconfigurations ClickHouse self-installs ship with: default user with empty password, the XML-vs-SQL access-management gotcha, sustained authentication failures, and an unprotected :9363 Prometheus endpoint.

Published 2026-05-2610 min readClickHouseSecurity 101Tested on CH 24.3
On this page
  1. Why this post exists
  2. Can the default user log in from anywhere with no password?
  3. Make a read-only monitoring user (not default)
  4. Are authentication failures piling up?
  5. Is the :9363 Prometheus endpoint open?

Why this post exists

ClickHouse's defaults privilege getting-started speed over security: the `default` user has an empty password, listens on every interface, and has `access_management = 1` permission to create more users. That is fantastic for local development; it is a 4 a.m. pager incident in production. Our agent watches the operational state (merge backlog, replica lag, mark cache, ZooKeeper health) but cannot see the auth posture. This post covers what only you can fix.

Every command below was run on a fresh `clickhouse/clickhouse-server:24.3` container on 2026-05-26 (full traces in [the spec](https://github.com/elk-utilities/homelab-apps/blob/main/.ai/specs/security-101-best-practices-blog.md)). The single most important fix — rotating the `default` user — has a SQL path and an XML path; we cover both and explain when each one applies.

Can the default user log in from anywhere with no password?

This is the classic ClickHouse footgun. The Docker image and most self-install paths leave the `default` user with `auth_type = no_password` (or `plaintext_password = ''`) and `host_ip = ['::/0']`. Combined, that's "unauthenticated read/write to anyone with port 8123 or 9000 reachable".

Check

Check: enumerate users and their auth posture

The `system.users` view (CH 20.4+) has everything you need in one query. The verdict column is the one-liner that flags the danger pattern.

SQL✓ ClickHouse 24.3.18.7. `system.users` requires 20.4+; before that the only place to inspect users is the live `users.xml`. Column name became `host_ip` (singular) in 24.x — before 23.4 it was `host_ips`.

What to look for: Any `DANGER` or `WARNING` row is a finding. On a fresh Docker container, `default` produces a `DANGER` row.

Fix

Fix A (XML-defined default user): rotate password via users.d

If `default` was created by `users.xml` (the standard Docker / apt path), it is **immutable from SQL** — `ALTER USER default IDENTIFIED…` fails with an access-manager error. Edit the XML instead.

XML✓ ClickHouse 24.3. `password_sha256_hex` element is stable since 19.x.

Verify it worked: # Reload happens on file write (CH watches /etc/clickhouse-server/users.d/). # Confirm with: clickhouse-client --user default --password '' --query "SELECT 1" # expect: "Authentication failed" clickhouse-client --user default --password 'your-strong-password' --query "SELECT 1" # expect: 1

Note: CH polls `users.d/` every few seconds — no restart needed. If the change doesn't take effect within ~30s, check `tail /var/log/clickhouse-server/clickhouse-server.err.log` for an XML parse error.

Fix

Fix B (SQL-managed users): rotate via SQL

Users created via `CREATE USER` (SQL-managed) can be altered the same way. This is the right path for any non-default user, and for `default` ONLY if it was originally created by SQL (rare).

SQL✓ ClickHouse 24.3. `CREATE/ALTER USER` SQL grammar is 20.4+. `IDENTIFIED WITH sha256_password` is the recommended hashing form; `plaintext_password` is supported but never on production.
ALTER USER my_sql_user
  IDENTIFIED WITH sha256_password BY 'a-strong-rotated-password'
  HOST IP '10.0.0.0/8';

Verify it worked: SELECT name, auth_type, host_ip FROM system.users WHERE name = 'my_sql_user'; -- expect: my_sql_user | sha256_password | ['10.0.0.0/8']

Make a read-only monitoring user (not default)

When you point a Prometheus exporter or our monitor-agent at ClickHouse, the connection should not use `default`. Create a dedicated user that has `readonly = 2` and `SELECT` on `system.*` only.

Fix

Fix: scoped read-only user

SQL✓ ClickHouse 24.3.18.7. `readonly = 2` (read-only, no SET allowed) is 20.4+. `host_ip` (singular column) is 24.x; earlier was `host_ips`.

Verify it worked: -- Login as prom_user and confirm it can read but cannot mutate. -- (Run from a separate session.) clickhouse-client --user prom_user --password 'a-strong-rotated-password' \ --query "SELECT count() FROM system.metrics" # expect: a number clickhouse-client --user prom_user --password 'a-strong-rotated-password' \ --query "CREATE TABLE t (x Int32) ENGINE=Memory" # expect: ACCESS_DENIED

Note: `readonly = 2` is the strict mode: queries cannot change session settings either. `readonly = 1` lets the client set per-query settings (timeouts, max memory) which is fine for most exporters; pick `1` if your exporter complains about "setting cannot be changed in readonly mode".

Are authentication failures piling up?

Sustained `AUTHENTICATION_FAILED` rate is one of three things, in order of likelihood: (a) a misconfigured exporter sending wrong credentials, (b) a credential rotation that missed some consumers, (c) an actual brute-force probe. Our `ClickHouseAuthenticationFailures` alert (PR #242) catches this — here's the manual version when you don't have Prometheus.

Check

Check: recent auth failures from the server log

Auth failures happen before the session is established, so they don't land in `system.query_log`. The server log file is the source of truth.

bash✓ ClickHouse 24.3. The log message text (`Code: 516. … AUTHENTICATION_FAILED`) is stable since 21.x.
grep AUTHENTICATION_FAILED /var/log/clickhouse-server/clickhouse-server.err.log \
  | tail -20

What to look for: Any recent line is a finding; >1/s is the threshold our alert fires on. Look at the username and source IP in each line — they are usually decisive.

Fix

Fix: identify the source, then rotate or restrict

If the source IP is a known internal service, the fix is almost always a credential rotation that missed somebody. If the source is external and unfamiliar, restrict reachability at the network layer.

bash✓ ClickHouse 24.3.
# Group the most recent auth failures by source IP + username:
grep AUTHENTICATION_FAILED /var/log/clickhouse-server/clickhouse-server.err.log \
  | sed -E 's/.*\{[^}]*\} <Error> ServerErrorHandler: Code: 516\. DB::Exception: ([^:]+): Authentication failed.*from ([0-9.]+).*/\1 \2/' \
  | sort | uniq -c | sort -rn | head -10

Verify it worked: # After fixing the misconfigured client (or blocking the bad IP): grep AUTHENTICATION_FAILED /var/log/clickhouse-server/clickhouse-server.err.log \ | awk -F'[][]' '{print $2}' \ | awk '$0 > strftime("%Y.%m.%d %H:%M:%S", systime()-300)' \ | wc -l # expect: a number trending toward zero

Is the :9363 Prometheus endpoint open?

ClickHouse's built-in `/metrics` endpoint (port 9363 by default) serves Prometheus metrics with NO authentication. That's intentional — Prometheus exporters don't typically auth — but means anyone with a route to that port can read your operational telemetry (query rates, error counts, table sizes). Restrict it at the network layer.

Check

Check: who can reach :9363?

bash✓ ClickHouse 24.3.
# From outside the cluster — should be UNREACHABLE.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' --max-time 5 \
  http://CLICKHOUSE_HOST:9363/metrics

What to look for: Anything other than a timeout (000) or connection refused (000) means the endpoint is reachable from the box you ran the check on. From outside your trusted network the answer must be "unreachable".

Fix

Fix: NetworkPolicy / firewall, not application auth

There is no built-in CH way to put a password on the Prometheus endpoint. The right answer is network-layer isolation: a Kubernetes NetworkPolicy in cluster, a security group in cloud, or a `bind_host: 127.0.0.1` if the exporter runs on the same box.

YAML✓ Kubernetes NetworkPolicy v1, Calico/Cilium-compatible. The same shape works on any CNI that implements NetworkPolicy.

Verify it worked: # From a pod OUTSIDE monitor-agent namespace — expect timeout: kubectl run -it --rm probe --image=curlimages/curl --restart=Never -- \ curl --max-time 5 http://clickhouse.<your-ns>.svc:9363/metrics # expect: connection timed out # From a pod INSIDE monitor-agent namespace — expect metrics: kubectl -n monitor-agent run -it --rm probe --image=curlimages/curl --restart=Never -- \ curl --max-time 5 http://clickhouse.<your-ns>.svc:9363/metrics | head -5

Note: This is the same NetworkPolicy our [/exporters/clickhouse/scoped-user](/exporters/clickhouse/scoped-user) page recommends — repeated here so the post is self-contained.