{"record":{"id":"0b93e78ed75bdfe0","repo":"JuliusBrussee/caveman","slug":"production-requires-an-https-clickhouse-url-tl","errorCode":null,"errorMessage":"production requires an https:// CLICKHOUSE_URL (TLS only); ClickHouse credentials travel as HTTP Basic and a plaintext endpoint ships them in the clear","messagePattern":"production requires an https:// CLICKHOUSE_URL \\(TLS only\\); ClickHouse credentials travel as HTTP Basic and a plaintext endpoint ships them in the clear","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/chhttp/chhttp.go","lineNumber":317,"sourceCode":"// It refuses, in production, a non-HTTPS CLICKHOUSE_URL (the ClickHouse password\n// travels as HTTP Basic, so plaintext ships it in the clear) and any attempt to\n// disable certificate verification. It also resolves the TLS configuration\n// eagerly so an unreadable or unparseable CA bundle fails the process at boot\n// instead of at the first telemetry flush. Outside production it warns — loudly\n// and once — when verification has been switched off.\n//\n// It is NOT guaranteed to run before every client is constructed: control-api\n// builds a package-level query client at init (internal/httpapi/chclients.go), so\n// that one exists before main calls anything. That ordering is safe rather than\n// lucky — a client built from a rejected configuration carries errTransport and\n// refuses to send, so the failure is the refusal either way; ValidateProduction's\n// job is to turn it into a loud non-zero exit at boot instead of a runtime error.\nfunc ValidateProduction(logger *slog.Logger) error {\n\tif production() {\n\t\traw := strings.TrimSpace(env.String(\"CLICKHOUSE_URL\", \"\"))\n\t\tparsed, err := url.Parse(raw)\n\t\tif err != nil || !strings.EqualFold(parsed.Scheme, \"https\") || parsed.Hostname() == \"\" {\n\t\t\treturn 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\")\n\t\t}\n\t}\n\tcfg, err := tlsClientConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif cfg != nil && cfg.InsecureSkipVerify && logger != nil {\n\t\tlogger.Warn(\"ClickHouse TLS certificate verification is DISABLED\",\n\t\t\t\"variable\", skipVerifyEnv,\n\t\t\t\"env\", env.String(\"CAVE_ENV\", \"local\"),\n\t\t\t\"impact\", \"telemetry and aggregate queries can be read or forged by any in-path party; never set this in production\")\n\t}\n\treturn nil\n}\n","sourceCodeStart":299,"sourceCodeEnd":332,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/chhttp/chhttp.go#L299-L332","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set CLICKHOUSE_URL to an https:// endpoint, e.g. https://clickhouse.internal:8443.","Enable native TLS on ClickHouse (openSSL server config, https_port 8443) or put it behind an HTTPS-terminating proxy and point the URL there.","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).","Never route around with skip-verify plus http - the check is scheme-based and http is refused regardless."],"exampleFix":"# before\nCAVE_ENV=prod\nCLICKHOUSE_URL=http://clickhouse:8123\n\n# after\nCAVE_ENV=prod\nCLICKHOUSE_URL=https://clickhouse.internal:8443","handlingStrategy":"validation","validationCode":"// deploy-time check mirroring the gate\nu, err := url.Parse(os.Getenv(\"CLICKHOUSE_URL\"))\nif err != nil || !strings.EqualFold(u.Scheme, \"https\") || u.Hostname() == \"\" {\n    return errors.New(\"CLICKHOUSE_URL must be https:// with a host before prod deploy\")\n}","typeGuard":null,"tryCatchPattern":"if err := chhttp.ValidateProduction(logger); err != nil {\n    log.Fatalf(\"refusing to start: %v\", err) // non-zero exit at boot is the intended contract\n}","preventionTips":["Run ValidateProduction in a pre-deploy smoke stage against prod env, not only at container start.","Add a CI policy rule that rejects http:// CLICKHOUSE_URL in prod manifests.","Terminate TLS at or before ClickHouse so the app always speaks https in prod."],"tags":["go","production","config","tls","clickhouse","security"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}