On this page
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: 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.
What to look for: Any `DANGER` or `WARNING` row is a finding. On a fresh Docker container, `default` produces a `DANGER` row.
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.
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 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).
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: scoped read-only user
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: 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.
grep AUTHENTICATION_FAILED /var/log/clickhouse-server/clickhouse-server.err.log \
| tail -20What 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: 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.
# 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 -10Verify 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: who can reach :9363?
# From outside the cluster — should be UNREACHABLE.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' --max-time 5 \
http://CLICKHOUSE_HOST:9363/metricsWhat 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: 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.
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.