Skip to content
Merged
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
9 changes: 5 additions & 4 deletions .wiki/Deployment-and-Security.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ Re-check replica count (`kubectl get pods -n demo -l clickhouse.altinity.com/chi
before every otel deploy — a ClickHouse Operator `chi` can add replicas
independent of any SPA-deploy action. See [[Operations-Memory]].

## Standalone nginx image + Helm chart (sql.demo.altinity.cloud)
## Standalone Caddy image + Helm chart (sql.demo.altinity.cloud)

Distinct from the three ClickHouse-hosted deploys above: the SPA also ships as a
production **nginx** container image (`ghcr.io/altinity/altinity-sql-browser`,
production **Caddy** container image (`ghcr.io/altinity/altinity-sql-browser`,
public, multi-arch) built by `.github/workflows/docker.yml` (`edge`+`sha-<c>` on
every push to `main`; `X.Y.Z`+`latest` on a `vX.Y.Z` tag push — the release
image) and a **Helm chart** (`helm/altinity-sql-browser/`, published to
`oci://ghcr.io/altinity/altinity-sql-browser/helm/altinity-sql-browser` by
`release.yml` on `v*`).
The container is a standalone static server (nginx-unprivileged, uid 101, port
8080); it never proxies — the browser POSTs queries cross-origin to whatever
The container is a standalone static server (`caddy:2.8.4-alpine`, uid 101, port
8080, `deploy/caddy/Caddyfile`; serves precompressed br/zstd/gzip sidecars, no
request-time compression; CSP `connect-src` from the `CONNECT_SRC` env var); it never proxies — the browser POSTs queries cross-origin to whatever
cluster the login picker selects.

Live at **https://sql.demo.altinity.cloud/sql** (ns `demo`, Helm release
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Fixed
- **#692 (part 1): deployment docs and installer output corrections.**
`deploy/install.sh` printed `https://` even without `--secure` and omitted the
HTTP port; the next-steps text now comes from a `print_next_steps` function
(also shown by `--dry-run`, covered by `tests/unit/install-script.test.js`) and
prints the right scheme and the default HTTP(S) port (8123/8443). Docs: removed
frozen artifact-size figures (`dist/sql.html` is about 2 MB raw; measure your
build), stated the real installer prerequisites (npm, node 22+, curl,
clickhouse-client; prebuilt release asset noted), clarified that
ClickHouse-native hosting needs `http_handlers` access and that the
container/Helm runtime is the alternative, corrected the option B/C caveats
(replication scope, ConfigMap 1 MiB cap), corrected the auth wording
(Bearer id_token by default, access_token with an audience, JWT as Basic
password with `ch_auth: "basic"`), and updated the wiki from nginx to Caddy.
Docs also no longer claim `install.sh --cluster` fans out to every replica:
testing (ClickHouse 25.8 OSS, 26.3 OSS, Antalya 25.8.33) showed it fails on any
multi-replica cluster (`INSERT INTO FUNCTION clusterAllReplicas` ->
`STORAGE_REQUIRES_PARAMETER`) and, since the script exits (`set -e`) before its
`DROP`, may leave a temp `_asb_*` table behind (cleanup `DROP ... ON CLUSTER`
statements are documented). `install.sh --cluster` now fails closed (exit 2,
before building) unless the `system.clusters` count query succeeds and returns
exactly 1: query failure, an unknown cluster (0), malformed output and more
than one replica each get a distinct message (the last points at the manual
procedure). Added a tested interim manual `ON CLUSTER ... ENGINE = File ... AS
SELECT FROM url()` procedure (including the `sql-config.json` literal, tested on single-node 26.3
only) to `docs/ASSET-DISTRIBUTION.md`, structured as an idempotent
pre-clean/create/drop/verify sequence for retrying after a partial failure
(the retry flow itself is untested); the installer fix is tracked in #692.

### Added
- **#592: lock in the #586/#587 shell primitive guardrails mechanically.**
`check:arch` now rejects three regrowth shapes the six-copy-pasted-overlays
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ variable across every tile.

## How it works

![Auth & data flow: the browser fetches the single-file SPA and its config.json from ClickHouse, signs in to your OAuth IdP with OAuth2 Authorization-Code + PKCE (id_token kept in sessionStorage), then POSTs every query to ClickHouse with an Authorization: Bearer id_token that ClickHouse validates against the IdP's JWKS via its token_processor (or a delegated verifier). There is no app-specific backend.](docs/assets/img/how-it-works.svg)
![Auth & data flow: the browser fetches the single-file SPA and its config.json from ClickHouse, signs in to your OAuth IdP with OAuth2 Authorization-Code + PKCE (id_token kept in sessionStorage), then POSTs every query to ClickHouse with an Authorization: Bearer id_token (or access_token with an audience; the JWT as the Basic password with ch_auth basic) that ClickHouse validates against the IdP's JWKS via its token_processor (or a delegated verifier). There is no app-specific backend.](docs/assets/img/how-it-works.svg)

The browser never holds a static credential — each user authenticates with your
IdP and ClickHouse sees their JWT. There is **no app-specific backend**: the
Expand Down Expand Up @@ -653,9 +653,13 @@ CLICKHOUSE_PASSWORD=… ./deploy/install.sh \
[--issuer https://accounts.google.com] \
[--audience <api-audience>] \ # audience-gated CH → also sends the access_token
[--ch-auth basic] \ # OSS CH + ch-jwt-verify → JWT as Basic password
[--cluster <cluster-name>] # single-shard multi-replica only (else per-node)
[--cluster <cluster-name>] # BROKEN on multi-replica clusters (#692); omit
```

`--cluster` currently fails on multi-replica clusters (tracked in #692); for a
single node run the installer without it, and for multi-node clusters follow
[the interim manual procedure](docs/ASSET-DISTRIBUTION.md#multi-node-clusters-interim-manual).

With **no** `--audience`, the IdP returns an **id_token** (its `aud` is the
client_id) and the browser sends that as the bearer — so ClickHouse's
`expected_audience` must be the **client_id**, not an API audience. Passing
Expand All @@ -670,7 +674,7 @@ see "Security headers" below), and uploads the SPA + config into ClickHouse
it as an ACM cluster setting `config.d/sql-browser.xml`) and reload ClickHouse.
The SPA handler serves `/sql`; Workbench and Dashboard bookmarks differ only
by the `ws`, `surface`, and optional `mode` query parameters.
2. Register the single redirect URI `https://<ch-host>/sql` with your OAuth IdP.
2. Register the single redirect URI `https://<ch-host>:8443/sql` with your OAuth IdP (the URI must match the origin exactly: include the HTTP(S) port unless it is the scheme default, 443/80, or use the port/ingress host actually exposed).
3. Make sure ClickHouse accepts the bearer JWT — either a CH
`<token_processors>` entry validating your IdP's JWKS, or a delegated
`<http_authentication_servers>` verifier. See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
Expand Down
4 changes: 3 additions & 1 deletion deploy/http_handlers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
GET /sql -> the SPA (dist/sql.html in user_files)
GET /sql/config.json -> the OAuth config (issuer + client_id [+ audience])

The SPA POSTs queries to "/" with `Authorization: Bearer <id_token>`; that is
The SPA POSTs queries to "/" with the IdP JWT in `Authorization: Bearer <jwt>`
(id_token by default, access_token when an audience is set; with
`ch_auth: "basic"` the JWT is the Basic password instead); that is
handled by ClickHouse's built-in dynamic query handler (the <defaults/> rule),
so no extra route is needed for query execution. ClickHouse must be configured
to accept the JWT (token_processor / JWKS, or a delegated http_authentication
Expand Down
90 changes: 66 additions & 24 deletions deploy/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
# The password is read from the CLICKHOUSE_PASSWORD env var or prompted — never
# passed on the command line (it would leak via `ps`/shell history).
#
# Prerequisites: bash, node 22+ and npm (the installer builds the SPA from source),
# curl (only for OIDC discovery; it falls back to the Google connect-src without
# it), and clickhouse-client (not needed for --dry-run). To skip the build, take
# the prebuilt sql.html from the GitHub release and upload it yourself; see
# docs/ASSET-DISTRIBUTION.md.
#
# Usage:
# CLICKHOUSE_PASSWORD=... ./deploy/install.sh \
# --ch-host clickhouse.example.com \
Expand All @@ -17,7 +23,7 @@
# [--issuer https://accounts.google.com] \
# [--audience <aud>] \ # audience-gated CH → also sends access_token
# [--ch-auth basic] \ # OSS CH + ch-jwt-verify → JWT as Basic password
# [--cluster my_cluster] \ # single-shard multi-replica only
# [--cluster my_cluster] \ # BROKEN on multi-replica clusters (#692); omit
# [--secure] \
# [--dry-run] # build + render config.json + http_handlers.xml, print, no CH contact
set -euo pipefail
Expand Down Expand Up @@ -54,15 +60,34 @@ export CLICKHOUSE_PASSWORD
CH=(clickhouse-client --host "$CH_HOST" --user "$CH_USER")
[[ "$SECURE" == 1 ]] && CH+=(--secure)

# user_files is node-local, and clusterAllReplicas cannot write to a multi-shard
# Distributed target, so a --cluster install only works on a single shard.
# user_files is node-local. NOTE: --cluster is currently broken (#692): the
# clusterAllReplicas INSERT below fails on any cluster with more than one
# replica (STORAGE_REQUIRES_PARAMETER). The guard fails CLOSED, before building:
# proceed only when the count query succeeds and prints exactly 1.
if [[ "$DRY_RUN" != 1 && -n "$CLUSTER" ]]; then
SHARDS=$("${CH[@]}" --query "SELECT max(shard_num) FROM system.clusters WHERE cluster = '${CLUSTER}'" 2>/dev/null || true)
if [[ "$SHARDS" =~ ^[0-9]+$ ]] && (( SHARDS > 1 )); then
echo "ERROR: cluster '${CLUSTER}' has ${SHARDS} shards. clusterAllReplicas can't" >&2
echo "write across shards, and user_files is node-local. Install per node instead" >&2
echo "(omit --cluster; point --ch-host at each node), or serve the assets from a" >&2
echo "replicated table — see docs/ASSET-DISTRIBUTION.md." >&2
GUARD_ERR_FILE="$(mktemp)"
if ! NODES=$("${CH[@]}" --query "SELECT count() FROM system.clusters WHERE cluster = '${CLUSTER}'" 2>"$GUARD_ERR_FILE"); then
GUARD_ERR="$(head -n 1 "$GUARD_ERR_FILE")"
rm -f "$GUARD_ERR_FILE"
echo "ERROR: could not query system.clusters on ${CH_HOST} to check cluster" >&2
echo "'${CLUSTER}': ${GUARD_ERR:-no error output}" >&2
exit 2
fi
rm -f "$GUARD_ERR_FILE"
NODES="${NODES//[[:space:]]/}"
if [[ ! "$NODES" =~ ^[0-9]+$ ]]; then
echo "ERROR: unexpected system.clusters output for cluster '${CLUSTER}':" >&2
echo "'${NODES}' (expected a node count). Refusing to continue." >&2
exit 2
elif (( NODES == 0 )); then
echo "ERROR: cluster '${CLUSTER}' was not found in system.clusters on ${CH_HOST}" >&2
echo "(0 replicas). Check the cluster name and the node you connected to." >&2
exit 2
elif (( NODES > 1 )); then
echo "ERROR: cluster '${CLUSTER}' has ${NODES} replicas. --cluster is currently" >&2
echo "broken on multi-node clusters (#692); user_files is node-local. Use the" >&2
echo "manual procedure: 'Multi-node clusters (interim, manual)' in" >&2
echo "docs/ASSET-DISTRIBUTION.md." >&2
exit 2
fi
fi
Expand Down Expand Up @@ -119,13 +144,42 @@ HANDLERS_OUT="$ROOT/dist/http_handlers.xml"
sed -E "s#(connect-src 'self')[^;]*#\1 ${CONNECT_HOSTS}#" \
"$ROOT/deploy/http_handlers.xml" > "$HANDLERS_OUT"

# Print the post-install instructions. Called after upload, and also by --dry-run
# so the URL/scheme logic is testable without a ClickHouse. The native port
# --ch-host is used with (9000/9440) differs from the HTTP(S) port the browser
# needs, so print the HTTP defaults (8123 plain / 8443 TLS) explicitly.
print_next_steps() {
local scheme="http" port="8123" host="${CH_HOST:-<ch-host>}"
if [[ "$SECURE" == 1 ]]; then scheme="https"; port="8443"; fi
cat <<EOT

Final step - enable the HTTP routes. Add the rendered dist/http_handlers.xml
(its CSP connect-src is filled in for your issuer) to the server config.d/ (or
push it as an ACM cluster setting named "config.d/sql-browser.xml") and reload
ClickHouse. Then open:

${scheme}://${host}:${port}/sql

(${port} is the default ClickHouse HTTP$([[ "$SECURE" == 1 ]] && echo S) port; use the port your
HTTP endpoint, load balancer, or ingress actually exposes, and omit it if that
is the scheme default.)

Also register the OAuth redirect URI ${scheme}://${host}:${port}/sql (same
origin, same port as above) with your IdP, and make sure ClickHouse is
configured to accept the token the SPA sends: a Bearer JWT (token_processor +
JWKS), or with --ch-auth basic the JWT as the Basic password via a delegated
http_authentication verifier. See README.
EOT
}

if [[ "$DRY_RUN" == 1 ]]; then
echo
echo "==> DRY RUN — no ClickHouse contact. Rendered artifacts:"
echo "--- config.json ---"
cat "$CONFIG_FILE"
echo "--- dist/http_handlers.xml ---"
cat "$HANDLERS_OUT"
print_next_steps
exit 0
fi

Expand Down Expand Up @@ -155,18 +209,6 @@ upload "$ROOT/dist/sql.html" "sql.html"
echo "==> Uploading sql-config.json"
upload "$CONFIG_FILE" "sql-config.json"

cat <<EOF

==> Assets uploaded to ClickHouse user_files/.

Final step — enable the HTTP routes. Add the rendered dist/http_handlers.xml
(its CSP connect-src is filled in for your issuer) to the server config.d/ (or
push it as an ACM cluster setting named "config.d/sql-browser.xml") and reload
ClickHouse. Then open:

http${SECURE:+s}://$CH_HOST/sql

Also register the OAuth redirect URI http(s)://$CH_HOST/sql with your IdP,
and make sure ClickHouse is configured to accept the bearer JWT (token_processor
+ JWKS, or a delegated http_authentication verifier). See README.
EOF
echo
echo "==> Assets uploaded to ClickHouse user_files/."
print_next_steps
2 changes: 2 additions & 0 deletions deploy/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#
# Usage:
# CLICKHOUSE_PASSWORD=... ./deploy/uninstall.sh --ch-host H [--ch-user U] [--cluster C] [--secure]
# NOTE: with --cluster this only truncates the files on the connected node: the
# CREATE/DROP run ON CLUSTER but the INSERT that empties the file is local.
set -euo pipefail

CH_HOST="" CH_USER="default" CLUSTER="" SECURE=0
Expand Down
Loading
Loading