JuliusBrussee/caveman · critical

production requires an https:// CLICKHOUSE_URL (TLS only); C

Error message

production requires an https:// CLICKHOUSE_URL (TLS only); ClickHouse credentials travel as HTTP Basic and a plaintext endpoint ships them in the clear

What it means

Thrown by ValidateProduction in shared/platform/chhttp/chhttp.go:317 when the process runs in production mode (per IsProduction) and CLICKHOUSE_URL is unset, unparseable, not https-scheme, or has an empty host. The rationale in the code: ClickHouse credentials travel as HTTP Basic auth, so a plaintext http:// endpoint would ship them in the clear. This gate turns what would be an errTransport refusal at request time into a loud non-zero exit at boot.

Source

Thrown at shared/platform/chhttp/chhttp.go:317

// It refuses, in production, a non-HTTPS CLICKHOUSE_URL (the ClickHouse password
// travels as HTTP Basic, so plaintext ships it in the clear) and any attempt to
// disable certificate verification. It also resolves the TLS configuration
// eagerly so an unreadable or unparseable CA bundle fails the process at boot
// instead of at the first telemetry flush. Outside production it warns — loudly
// and once — when verification has been switched off.
//
// It is NOT guaranteed to run before every client is constructed: control-api
// builds a package-level query client at init (internal/httpapi/chclients.go), so
// that one exists before main calls anything. That ordering is safe rather than
// lucky — a client built from a rejected configuration carries errTransport and
// refuses to send, so the failure is the refusal either way; ValidateProduction's
// job is to turn it into a loud non-zero exit at boot instead of a runtime error.
func ValidateProduction(logger *slog.Logger) error {
	if production() {
		raw := strings.TrimSpace(env.String("CLICKHOUSE_URL", ""))
		parsed, err := url.Parse(raw)
		if err != nil || !strings.EqualFold(parsed.Scheme, "https") || parsed.Hostname() == "" {
			return fmt.Errorf("production requires an https:// CLICKHOUSE_URL (TLS only); ClickHouse credentials travel as HTTP Basic and a plaintext endpoint ships them in the clear")
		}
	}
	cfg, err := tlsClientConfig()
	if err != nil {
		return err
	}
	if cfg != nil && cfg.InsecureSkipVerify && logger != nil {
		logger.Warn("ClickHouse TLS certificate verification is DISABLED",
			"variable", skipVerifyEnv,
			"env", env.String("CAVE_ENV", "local"),
			"impact", "telemetry and aggregate queries can be read or forged by any in-path party; never set this in production")
	}
	return nil
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set CLICKHOUSE_URL to an https:// endpoint, e.g. https://clickhouse.internal:8443.
  2. Enable native TLS on ClickHouse (openSSL server config, https_port 8443) or put it behind an HTTPS-terminating proxy and point the URL there.
  3. If the value looked set but still fails, print it masked: the scheme and host must survive url.Parse (watch quotes/whitespace in env files).
  4. Never route around with skip-verify plus http - the check is scheme-based and http is refused regardless.

Example fix

# before
CAVE_ENV=prod
CLICKHOUSE_URL=http://clickhouse:8123

# after
CAVE_ENV=prod
CLICKHOUSE_URL=https://clickhouse.internal:8443
Defensive patterns

Strategy: validation

Validate before calling

// deploy-time check mirroring the gate
u, err := url.Parse(os.Getenv("CLICKHOUSE_URL"))
if err != nil || !strings.EqualFold(u.Scheme, "https") || u.Hostname() == "" {
    return errors.New("CLICKHOUSE_URL must be https:// with a host before prod deploy")
}

Try / catch

if err := chhttp.ValidateProduction(logger); err != nil {
    log.Fatalf("refusing to start: %v", err) // non-zero exit at boot is the intended contract
}

Prevention

When it happens

Trigger: CAVE_ENV resolves to production while CLICKHOUSE_URL is http://clickhouse:8123, localhost:8123 (no scheme), empty, or a URL whose parse fails or whose hostname is blank. Any of these returns this error from ValidateProduction.

Common situations: Promoting a compose/staging config to prod without switching the ClickHouse endpoint to TLS; ClickHouse deployed without TLS and fronted by nothing; a proxy terminator added later so the app still speaks http internally; env var lost during secret migration.

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/0b93e78ed75bdfe0. Report an issue: GitHub.