diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c4f87a..9739106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,12 @@ jobs: - name: Test release tag validation run: bash tests/release-tag.sh + - name: Test entrypoint path preparation + run: bash tests/entrypoint-paths.sh + + - name: Test SPDX augmentation + run: python -m unittest discover --start-directory tests --pattern "test_*.py" + - name: Audit GitHub Actions security uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 with: @@ -51,11 +57,29 @@ jobs: min-severity: medium image: - runs-on: ubuntu-latest - timeout-minutes: 30 + name: image (${{ matrix.architecture }}) + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + platform: linux/amd64 + machine: x86_64 + - architecture: arm64 + runner: ubuntu-24.04-arm + platform: linux/arm64 + machine: aarch64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 permissions: contents: read security-events: write + env: + TEST_IMAGE: ghcr.io/datopsis/clickhouse-server-ubi9:test-${{ matrix.architecture }} + SBOM_FILE: clickhouse-server-ubi9-${{ matrix.architecture }}.spdx.json + GRYPE_SARIF: grype-${{ matrix.architecture }}.sarif + GRYPE_ALL: grype-all-${{ matrix.architecture }}.json steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -65,6 +89,11 @@ jobs: - name: Set up Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + - name: Confirm native runner architecture + env: + EXPECTED_MACHINE: ${{ matrix.machine }} + run: test "$(uname -m)" = "${EXPECTED_MACHINE}" + - name: Scan build configuration uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: @@ -79,21 +108,30 @@ jobs: with: context: . file: Containerfile + platforms: ${{ matrix.platform }} load: true push: false - tags: ghcr.io/datopsis/clickhouse-server-ubi9:test - cache-from: type=gha - cache-to: type=gha,mode=max + tags: ${{ env.TEST_IMAGE }} + cache-from: type=gha,scope=image-${{ matrix.architecture }} + cache-to: type=gha,mode=max,scope=image-${{ matrix.architecture }} + + - name: Confirm loaded image architecture + env: + EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} + run: >- + test "$(docker image inspect --format '{{.Architecture}}' "${TEST_IMAGE}")" + = "${EXPECTED_ARCHITECTURE}" - name: Run smoke tests env: - IMAGE: ghcr.io/datopsis/clickhouse-server-ubi9:test + CONTAINER_RUNTIME: docker + IMAGE: ${{ env.TEST_IMAGE }} run: bash tests/smoke.sh - name: Scan image uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: - image-ref: ghcr.io/datopsis/clickhouse-server-ubi9:test + image-ref: ${{ env.TEST_IMAGE }} format: table exit-code: "1" ignore-unfixed: true @@ -102,39 +140,72 @@ jobs: - name: Generate SPDX SBOM with Syft uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 with: - image: ghcr.io/datopsis/clickhouse-server-ubi9:test + image: ${{ env.TEST_IMAGE }} format: spdx-json - output-file: clickhouse-server-ubi9.spdx.json + output-file: ${{ env.SBOM_FILE }} syft-version: v1.51.1 upload-artifact: false upload-release-assets: false + - name: Add declared ClickHouse TGZ components to SPDX SBOM + run: >- + python scripts/augment-spdx.py + --input "${SBOM_FILE}" + --output "${SBOM_FILE}" + - name: Scan Syft SBOM with Grype id: grype uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 with: - sbom: clickhouse-server-ubi9.spdx.json + sbom: ${{ env.SBOM_FILE }} output-format: sarif - output-file: grype.sarif + output-file: ${{ env.GRYPE_SARIF }} severity-cutoff: high only-fixed: true fail-build: true + cache-db: true + grype-version: v0.118.0 + + - name: Record all Grype findings + if: ${{ always() && hashFiles(env.SBOM_FILE) != '' }} + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: ${{ env.SBOM_FILE }} + output-format: json + output-file: ${{ env.GRYPE_ALL }} + severity-cutoff: negligible + only-fixed: false + fail-build: false + cache-db: true grype-version: v0.118.0 - name: Retain security artifacts if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: image-security-${{ github.sha }} + name: image-security-${{ github.sha }}-${{ matrix.architecture }} path: | - clickhouse-server-ubi9.spdx.json - grype.sarif + ${{ env.SBOM_FILE }} + ${{ env.GRYPE_SARIF }} + ${{ env.GRYPE_ALL }} if-no-files-found: warn retention-days: 14 - name: Publish Grype findings to code scanning - if: ${{ always() && github.event_name != 'pull_request' && hashFiles('grype.sarif') != '' }} + if: ${{ always() && github.event_name != 'pull_request' && hashFiles(env.GRYPE_SARIF) != '' }} uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: - sarif_file: grype.sarif - category: grype-image + sarif_file: ${{ env.GRYPE_SARIF }} + category: grype-image-${{ matrix.architecture }} + + image-result: + name: image + if: ${{ always() }} + needs: image + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every native image job + env: + MATRIX_RESULT: ${{ needs.image.result }} + run: test "${MATRIX_RESULT}" = success diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f62e1d0..00e9e9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,17 @@ jobs: provenance: mode=max sbom: true + - name: Verify published manifest architectures + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -Eeuo pipefail + docker buildx imagetools inspect --raw "${IMAGE}@${DIGEST}" | + jq -e ' + ([.manifests[].platform | select(.os == "linux" and .architecture == "amd64")] | length >= 1) + and ([.manifests[].platform | select(.os == "linux" and .architecture == "arm64")] | length >= 1) + ' + - name: Scan published image uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: @@ -89,6 +100,12 @@ jobs: upload-artifact: false upload-release-assets: false + - name: Add declared ClickHouse TGZ components to SPDX SBOM + run: >- + python scripts/augment-spdx.py + --input image.spdx.json + --output image.spdx.json + - name: Scan Syft SBOM with Grype uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 with: @@ -98,6 +115,20 @@ jobs: severity-cutoff: high only-fixed: true fail-build: true + cache-db: true + grype-version: v0.118.0 + + - name: Record all Grype findings + if: ${{ always() && hashFiles('image.spdx.json') != '' }} + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: image.spdx.json + output-format: json + output-file: grype-all.json + severity-cutoff: negligible + only-fixed: false + fail-build: false + cache-db: true grype-version: v0.118.0 - name: Retain release security artifacts @@ -108,6 +139,7 @@ jobs: path: | image.spdx.json grype.sarif + grype-all.json if-no-files-found: warn retention-days: 30 @@ -121,6 +153,13 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Attest complete SPDX SBOM with GitHub OIDC + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: >- + cosign attest --yes --predicate image.spdx.json --type spdxjson + "${IMAGE}@${DIGEST}" + - name: Sign image with GitHub OIDC env: DIGEST: ${{ steps.build.outputs.digest }} diff --git a/.gitignore b/.gitignore index 79e26df..285619f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .smoke-secrets.*/ /clickhouse-server-ubi9.spdx.json /grype.sarif +/grype-all.json /image.intoto.jsonl /image.sigstore.json /image.spdx.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c71857..1833ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - A documented container and repository versioning and release standard. - CI security-layer, enforcement, result-review, and Endor Labs guidance. - Structured bug reporting and pull-request review checklists. +- Production, TLS, disconnected-deployment, vulnerability-triage, upstream-license, and official-image comparison documentation. +- Entrypoint initialization and health checks over TLS-only native-port configurations. +- Complete Grype JSON inventories, including unfixed findings, retained beside the fixed High/Critical blocking result in CI and release runs. +- Explicit ClickHouse TGZ component records in SPDX inventories and a keyless, digest-bound complete SPDX release attestation. +- Rootless storage guidance for named volumes, bind mounts, Kubernetes/OpenShift identities, custom data paths, additional disks, SELinux, and NFS. +- Native AMD64 and ARM64 CI builds, smoke tests, vulnerability evidence, and release-manifest architecture validation. +- Explicit repository scope and official-image storage compatibility guidance, including the XML-based replacement for the unreleased `CLICKHOUSE_DATA_DIR` interface. +- Podman-first user procedures, a tested Podman support baseline, and rootless user-namespace permission guidance. + +### Changed + +- The entrypoint now derives primary and additional writable directories from the effective ClickHouse configuration, rejects the misleading `CLICKHOUSE_DATA_DIR` variable, and reports non-root permission failures before server startup. diff --git a/CLAUDE.md b/CLAUDE.md index bc45071..fe2d57a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,8 +9,8 @@ This repository builds a security-oriented ClickHouse Server container on Red Ha Build and test commands are documented in `README.md`. The primary local verification is: ```bash -docker build --file Containerfile --tag ghcr.io/datopsis/clickhouse-server-ubi9:test . -IMAGE=ghcr.io/datopsis/clickhouse-server-ubi9:test bash tests/smoke.sh +podman build --format docker --file Containerfile --tag ghcr.io/datopsis/clickhouse-server-ubi9:test . +CONTAINER_RUNTIME=podman IMAGE=ghcr.io/datopsis/clickhouse-server-ubi9:test bash tests/smoke.sh ``` ## Git conventions diff --git a/Containerfile b/Containerfile index 1139d08..ad801b0 100644 --- a/Containerfile +++ b/Containerfile @@ -52,7 +52,7 @@ RUN microdnf install -y dnf gzip tar \ /runtime/docker-entrypoint-initdb.d \ /runtime/etc/clickhouse-server/config.d \ /runtime/etc/clickhouse-server/users.d \ - /runtime/var/lib/clickhouse/generated \ + /runtime/var/lib/clickhouse \ /runtime/var/log/clickhouse-server \ && chown -R 101:0 \ /runtime/docker-entrypoint-initdb.d \ @@ -86,8 +86,7 @@ COPY --chown=101:0 --chmod=0644 container/config.d/container.xml /etc/clickhouse ENV LANG="C.UTF-8" \ TZ="UTC" \ - CLICKHOUSE_CONFIG="/etc/clickhouse-server/config.xml" \ - CLICKHOUSE_DATA_DIR="/var/lib/clickhouse" + CLICKHOUSE_CONFIG="/etc/clickhouse-server/config.xml" USER 101:0 WORKDIR /var/lib/clickhouse diff --git a/README.md b/README.md index f337371..c6bbc7b 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,23 @@ This is an independent Datopsis packaging project. It is not an official ClickHo - Compatible with a read-only root filesystem - Requires no Linux capabilities for its baseline configuration - Restricts the default user to localhost unless a password is supplied -- CI smoke tests, repository linting, workflow auditing, and blocking Trivy and Grype scans for fixed high and critical vulnerabilities +- Native AMD64 and ARM64 CI smoke tests, repository linting, workflow auditing, and blocking Trivy and Grype scans for fixed high and critical vulnerabilities - Syft-generated SPDX JSON inventories retained for CI builds and attached to releases - Multi-architecture release images for `linux/amd64` and `linux/arm64` - Keyless Cosign signatures, SBOM attestations, and build provenance on tagged releases The builder uses UBI Minimal and is discarded. Only UBI Micro, a small set of UBI runtime packages, ClickHouse, and the entrypoint are present in the published image. +## Project scope + +This repository owns the container artifact and its contract: build inputs, entrypoint behavior, ports, environment variables, configuration/mount conventions, rootless operation, primary Podman usage, Docker compatibility, image security evidence, and minimal platform-qualification fixtures. + +Reusable production deployment topology belongs in the planned [clickhouse-production-stack](https://github.com/datopsis/clickhouse-production-stack) repository. That project should own deployable Compose/Kubernetes/OpenShift resources, clustering, ingress and network policy, secret integration, monitoring, backups, restore automation, and environment-specific sizing. Examples here remain intentionally small and exist to explain or test behavior specific to this image. + ## Quick start ```console -docker run --detach \ +podman run --detach \ --name clickhouse \ --publish 18123:8123 \ --publish 19000:9000 \ @@ -53,7 +59,7 @@ clickhouse-client --host 127.0.0.1 --port 19000 \ --query 'SELECT version()' ``` -The included `compose.yaml` provides the same hardened local baseline. Change its example password before use. +The included `compose.yaml` provides the same hardened local baseline. Change its example password before running `podman compose up --detach`. Podman's Compose command requires an installed Compose provider; see [Podman compatibility and version support](docs/PODMAN.md). ## Configuration @@ -65,7 +71,6 @@ The included `compose.yaml` provides the same hardened local baseline. Change it | `CLICKHOUSE_INIT_TIMEOUT` | Seconds to wait for the temporary initialization server | `60` | | `CLICKHOUSE_ALWAYS_RUN_INITDB_SCRIPTS` | Run initialization scripts on every start when non-empty | Unset | | `CLICKHOUSE_CONFIG` | Main server configuration file | `/etc/clickhouse-server/config.xml` | -| `CLICKHOUSE_DATA_DIR` | Data and generated-configuration directory | `/var/lib/clickhouse` | The image intentionally manages only the built-in `default` user through environment variables. Create additional users with SQL or mounted ClickHouse configuration. @@ -76,7 +81,7 @@ Mount configuration fragments under: Place first-start initialization files in `/docker-entrypoint-initdb.d`. Executable and sourced `.sh`, `.sql`, and `.sql.gz` files are supported and processed in lexical order. -Persistent data belongs at `/var/lib/clickhouse`. The image writes generated user configuration there, allowing the root filesystem to remain read-only. +The effective ClickHouse `` setting controls persistent data and defaults to `/var/lib/clickhouse`. The entrypoint discovers configured primary and additional local paths, creates permitted subdirectories as the current identity, and fails with UID/GID guidance when storage is not writable. It never starts as root or changes volume ownership. See [rootless storage and permissions](docs/ROOTLESS.md) before using bind mounts, arbitrary UIDs, custom paths, or additional disks. ## Build and test @@ -90,15 +95,7 @@ pre-commit run --all-files The hooks normalize text files to LF, reject common repository mistakes and private keys, lint shell scripts and the `Containerfile`, audit GitHub Actions syntax, and reject Claude co-author trailers in commit messages. The same checks run in CI. `.gitattributes` enforces LF in Git regardless of the contributor's operating system. -Docker: - -```console -docker build --file Containerfile \ - --tag ghcr.io/datopsis/clickhouse-server-ubi9:test . -IMAGE=ghcr.io/datopsis/clickhouse-server-ubi9:test bash tests/smoke.sh -``` - -Podman uses OCI format by default, which omits Docker health-check metadata. Use Docker image format when building locally: +Podman is the primary documented local runtime. Podman uses OCI format by default, which does not preserve the `HEALTHCHECK` instruction, so local builds use Docker manifest format: ```console podman build --format docker --file Containerfile \ @@ -110,7 +107,9 @@ CONTAINER_RUNTIME=podman \ Build-time arguments are `CLICKHOUSE_VERSION`, `CLICKHOUSE_CHANNEL`, `UBI_MINIMAL_IMAGE`, and `UBI_MICRO_IMAGE`. Release builds should retain immutable UBI digests and an exact ClickHouse version. -The smoke suite verifies startup with a read-only root filesystem and no capabilities, package-manager absence, authenticated local and network queries, first-start initialization, persistent-data restarts, password-file support, the passwordless network restriction, graceful shutdown, and operation under an arbitrary OpenShift-style UID. +The supported Podman baseline is version 5.3 or newer because 5.3.1 is the oldest engine on which the full smoke suite has been recorded. This is a tested support floor, not a claim that older versions cannot run the image. Docker Engine remains compatible and is used by GitHub Actions for its native architecture jobs and release Buildx workflow. See [Podman compatibility and version support](docs/PODMAN.md) for tested versions, rootless bind mounts, remote clients, and Compose behavior. + +The smoke suite verifies startup with a read-only root filesystem and no capabilities, package-manager absence, authenticated local and network queries, first-start initialization, persistent-data restarts, password-file support, the passwordless network restriction, TLS-only native initialization and health, graceful shutdown, and operation under an arbitrary OpenShift-style UID. It requires `openssl` on the test host to create an ephemeral TLS fixture. ## Release process @@ -118,7 +117,7 @@ The smoke suite verifies startup with a read-only root filesystem and no capabil 2. Merge the change to `main` after CI passes. 3. Complete the release gates in [docs/ROADMAP.md](docs/ROADMAP.md). 4. Choose the next release version according to [docs/VERSION.md](docs/VERSION.md), validate it with `bash scripts/validate-release-tag.sh `, and create its tag, such as `v26.8.2.7-ubi9.8-1`. -5. Push the tag. GitHub Actions builds both architectures, scans the image with Trivy and Grype, publishes it to GHCR, attaches SBOM and provenance attestations, signs the resulting digest, and creates a GitHub release containing the SPDX SBOM, Sigstore bundle, and provenance evidence. +5. Push the tag. GitHub Actions builds both architectures, scans the image with Trivy and Grype, publishes it to GHCR, attaches the complete SPDX SBOM and build provenance as attestations, signs the resulting digest, and creates a GitHub release containing the SPDX SBOM, Sigstore bundle, and in-toto evidence. Verify a release with GitHub as the keyless identity provider: @@ -127,16 +126,23 @@ cosign verify \ --certificate-identity-regexp='https://github.com/datopsis/clickhouse-server-ubi9/.github/workflows/release.yml@refs/tags/.*' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \ ghcr.io/datopsis/clickhouse-server-ubi9@sha256: + +cosign verify-attestation --type spdxjson \ + --certificate-identity-regexp='https://github.com/datopsis/clickhouse-server-ubi9/.github/workflows/release.yml@refs/tags/.*' \ + --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: ``` ## Operational notes ClickHouse commonly benefits from `nofile=262144:262144`. Optional capabilities such as `IPC_LOCK`, `NET_ADMIN`, and `SYS_NICE` enable specific advanced behavior, but are deliberately absent from the baseline. -Treat `/var/lib/clickhouse` as durable state, back it up according to your ClickHouse topology, and pin production deployments to an image digest rather than a mutable tag. +Treat the effective ClickHouse data path (default `/var/lib/clickhouse`) as durable state, back it up according to your ClickHouse topology, and pin production deployments to an image digest rather than a mutable tag. + +Before a production rollout, follow the [production deployment guide](docs/PRODUCTION.md). Configure inbound encryption and public/private outbound trust with the [TLS guide](docs/TLS.md). The secure ClickHouse ports (`8443`, `9440`, and `9010`) are configuration choices and are not enabled by default. -See [SECURITY.md](SECURITY.md) for vulnerability reporting and the support policy. Contributor references include the [versioning and release standard](docs/VERSION.md), [first-release roadmap](docs/ROADMAP.md), [CI and security process](docs/CI.md), [Endor Labs posture](docs/ENDOR.md), [badge policy](docs/BADGING.md), and [OpenSSF Scorecard controls](docs/OPENSSF_SCORECARD.md). +See [SECURITY.md](SECURITY.md) for vulnerability reporting and the support policy. Contributor references include [Podman compatibility](docs/PODMAN.md), [rootless storage and permissions](docs/ROOTLESS.md), [qualification evidence](docs/QUALIFICATION.md), the [vulnerability-management process](docs/VULNERABILITY-MANAGEMENT.md), [official-image comparison](docs/IMAGE-COMPARISON.md), [versioning and release standard](docs/VERSION.md), [first-release roadmap](docs/ROADMAP.md), [CI and security process](docs/CI.md), [Endor Labs posture](docs/ENDOR.md), [badge policy](docs/BADGING.md), and [OpenSSF Scorecard controls](docs/OPENSSF_SCORECARD.md). ## License -The packaging code in this repository is licensed under Apache License 2.0. ClickHouse and Red Hat UBI remain subject to their respective upstream licenses and terms. +The packaging code and documentation in this repository are licensed under Apache License 2.0. ClickHouse, Red Hat UBI, and their component packages remain subject to their respective upstream licenses and terms. See [third-party software and terms](THIRD_PARTY_NOTICES.md) for the distribution notices and release-review requirements. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..f5b83a5 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,27 @@ +# Third-party software and terms + +The root [Apache License 2.0](LICENSE) applies to Datopsis-authored packaging code and documentation in this repository. It does not replace the licenses or terms of software assembled into the container image. + +## ClickHouse + +The image uses unmodified official ClickHouse release archives for `clickhouse-common-static`, `clickhouse-server`, and `clickhouse-client`. ClickHouse open-source software is licensed under the [Apache License 2.0](https://github.com/ClickHouse/ClickHouse/blob/master/LICENSE). Copies supplied by the upstream archives remain in the image under `/usr/share/doc/clickhouse-*/LICENSE`. + +ClickHouse is a trademark of ClickHouse, Inc. This repository is an independent packaging project and is not affiliated with or endorsed by ClickHouse, Inc. Use of the name is descriptive and remains subject to the [ClickHouse trademark policy](https://clickhouse.com/legal/trademark-policy). No ClickHouse logo is used as this project's mark. + +## Red Hat Universal Base Image + +The base and installed runtime RPMs come only from Red Hat UBI 9 images and UBI repositories. UBI content is freely redistributable subject to the [Red Hat UBI EULA and the components' individual open-source licenses](https://developers.redhat.com/articles/ubi-faq). Red Hat support is not included with this community image; support for Red Hat technologies depends on the applicable subscription and supported deployment combination. + +The image retains RPM license texts under `/usr/share/licenses`. Its SPDX SBOM uses Syft for the exact RPM inventory and explicitly declares the three ClickHouse TGZ components from the pinned `Containerfile` inputs. Because the final image is assembled from multiple works under multiple licenses, the OCI `org.opencontainers.image.licenses=Apache-2.0` label describes this packaging project; consumers must also review the SBOM, embedded notices, ClickHouse license, UBI EULA, and component licenses. + +## Release review + +Before publishing a release: + +1. Confirm every installed Red Hat package comes from a UBI repository and remains redistributable. +2. Inspect the release SBOM for new packages, licenses marked unknown, or missing license files. +3. Confirm ClickHouse archive license files remain in the final image. +4. Retain upstream copyright, license, attribution, and trademark notices. +5. Update this notice if the base, package source, archive contents, branding, or distribution channels change. + +This notice is operational documentation, not legal advice. diff --git a/container/config.d/container.xml b/container/config.d/container.xml index 4a8eb1d..e5829f9 100644 --- a/container/config.d/container.xml +++ b/container/config.d/container.xml @@ -12,6 +12,6 @@ :: 0.0.0.0 - - /var/lib/clickhouse/generated/users.xml + + /tmp/clickhouse-entrypoint/users.xml diff --git a/container/config.d/outbound-ca.example.xml b/container/config.d/outbound-ca.example.xml new file mode 100644 index 0000000..5a3e16f --- /dev/null +++ b/container/config.d/outbound-ca.example.xml @@ -0,0 +1,15 @@ + + + + + false + /etc/clickhouse-server/certs/outbound-ca-bundle.pem + true + sslv2,sslv3,tlsv1,tlsv1_1 + strict + + RejectCertificateHandler + + + + diff --git a/container/config.d/tls.example.xml b/container/config.d/tls.example.xml new file mode 100644 index 0000000..c6d3454 --- /dev/null +++ b/container/config.d/tls.example.xml @@ -0,0 +1,21 @@ + + + 8443 + 9440 + + + + + + + + /etc/clickhouse-server/certs/tls.crt + /etc/clickhouse-server/certs/tls.key + none + true + true + sslv2,sslv3,tlsv1,tlsv1_1 + true + + + diff --git a/container/entrypoint.sh b/container/entrypoint.sh index a5d7a2d..b51807c 100644 --- a/container/entrypoint.sh +++ b/container/entrypoint.sh @@ -3,10 +3,89 @@ set -Eeuo pipefail shopt -s nullglob readonly CONFIG_FILE="${CLICKHOUSE_CONFIG:-/etc/clickhouse-server/config.xml}" -readonly DATA_DIR="${CLICKHOUSE_DATA_DIR:-/var/lib/clickhouse}" -readonly GENERATED_DIR="${DATA_DIR}/generated" +readonly GENERATED_DIR="/tmp/clickhouse-entrypoint" readonly USERS_FILE="${GENERATED_DIR}/users.xml" readonly INIT_DIR="/docker-entrypoint-initdb.d" +DATA_DIR="" + +extract_config_values() { + local key=$1 + + clickhouse extract-from-config \ + --config-file "${CONFIG_FILE}" --key "${key}" --try 2>/dev/null || true +} + +normalize_directory() { + local path=$1 + + if [[ "${path}" == /* ]]; then + printf '%s\n' "${path%/}" + elif [[ "${path}" == "." ]]; then + printf '%s\n' "${DATA_DIR}" + else + printf '%s\n' "${DATA_DIR}/${path%/}" + fi +} + +prepare_directory() { + local path=$1 + + if ! mkdir -p -- "${path}" || [[ ! -d "${path}" || ! -w "${path}" || ! -x "${path}" ]]; then + echo "Required ClickHouse directory is not writable: ${path}" >&2 + echo "Container identity: uid=$(id -u) gid=$(id -g)" >&2 + echo "Provision the mount for this identity or an OpenShift-compatible writable group; this image does not start as root or change ownership." >&2 + exit 1 + fi +} + +prepare_configured_directories() { + local key path log_path + local -a values=() + local -A prepared=() + + if [[ -n "${CLICKHOUSE_DATA_DIR:-}" ]]; then + echo "CLICKHOUSE_DATA_DIR is not supported because ClickHouse storage is controlled by in ${CONFIG_FILE}." >&2 + echo "Mount a configuration fragment that sets and provision that configured directory instead." >&2 + exit 1 + fi + + DATA_DIR="$(extract_config_values path)" + DATA_DIR="${DATA_DIR%/}" + if [[ -z "${DATA_DIR}" || "${DATA_DIR}" != /* ]]; then + echo "ClickHouse must resolve to a non-empty absolute directory in ${CONFIG_FILE}." >&2 + exit 1 + fi + + prepare_directory "${DATA_DIR}" + prepared["${DATA_DIR}"]=1 + + for key in tmp_path user_files_path format_schema_path \ + 'storage_configuration.disks.*.path' \ + 'storage_configuration.disks.*.metadata_path'; do + readarray -t values < <(extract_config_values "${key}") + for path in "${values[@]}"; do + [[ -z "${path}" ]] && continue + path="$(normalize_directory "${path}")" + if [[ -z "${prepared["${path}"]:-}" ]]; then + prepare_directory "${path}" + prepared["${path}"]=1 + fi + done + done + + for key in logger.log logger.errorlog; do + log_path="$(extract_config_values "${key}")" + [[ -z "${log_path}" ]] && continue + if [[ "${log_path}" == /* && -e "${log_path}" && -w "${log_path}" ]]; then + continue + fi + path="$(normalize_directory "$(dirname -- "${log_path}")")" + if [[ -z "${prepared["${path}"]:-}" ]]; then + prepare_directory "${path}" + prepared["${path}"]=1 + fi + done +} load_password() { if [[ -n "${CLICKHOUSE_PASSWORD_FILE:-}" ]]; then @@ -64,10 +143,30 @@ EOF client_command() { local -n command_ref=$1 + local port + command_ref=(clickhouse-client --host 127.0.0.1 --user default) if [[ -n "${CLICKHOUSE_PASSWORD}" ]]; then command_ref+=(--password "${CLICKHOUSE_PASSWORD}") fi + + port="$(clickhouse extract-from-config \ + --config-file "${CONFIG_FILE}" --key tcp_port --try 2>/dev/null || true)" + if [[ -n "${port}" ]]; then + command_ref+=(--port "${port}") + return + fi + + port="$(clickhouse extract-from-config \ + --config-file "${CONFIG_FILE}" --key tcp_port_secure --try 2>/dev/null || true)" + if [[ -z "${port}" ]]; then + echo "Neither tcp_port nor tcp_port_secure is configured" >&2 + exit 1 + fi + + # This client connects only over container loopback for initialization and + # health. External clients must validate the server certificate normally. + command_ref+=(--port "${port}" --secure --accept-invalid-certificate) } healthcheck() { @@ -158,13 +257,16 @@ main() { if [[ $# -eq 0 || "${1:-}" == --* ]]; then load_password - mkdir -p "${DATA_DIR}" /var/log/clickhouse-server + prepare_configured_directories write_users_config initialize_database + cd "${DATA_DIR}" exec clickhouse-server --config-file="${CONFIG_FILE}" "$@" fi exec "$@" } -main "$@" +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/docs/CI.md b/docs/CI.md index 8bba682..f8f4230 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -6,7 +6,7 @@ This repository treats the built image as the primary deliverable. CI therefore | Workflow | Triggers | Purpose | | --- | --- | --- | -| `CI` | Pull requests, pushes to `main`, weekly schedule, manual dispatch | Lint, workflow audit, configuration scan, image build, smoke tests, Trivy scan, Syft SBOM, and Grype scan. | +| `CI` | Pull requests, pushes to `main`, weekly schedule, manual dispatch | Lint and workflow audit, followed by native AMD64 and ARM64 image builds, smoke tests, Trivy scans, Syft SBOMs, and Grype scans. | | `CodeQL` | Workflow changes, weekly schedule, manual dispatch | Static analysis of GitHub Actions with the security-extended query suite. | | `OpenSSF Scorecard` | Pushes to `main`, ruleset changes, weekly schedule, manual dispatch | Supply-chain posture analysis, SARIF upload, and public Scorecard publication. | | `Release image` | Tags matching `v*` | Tag/input/changelog validation, multi-architecture publish, digest scans, evidence generation, keyless signing, and GitHub release creation. | @@ -21,7 +21,7 @@ Workflow-level permissions default to read-only. Write scopes are applied only t | Source-specific lint | ShellCheck and Hadolint | Common shell defects and unsafe or wasteful container-build patterns. | These are static rules, not runtime evidence. | | Workflow security | Actionlint, Zizmor, and CodeQL `actions` with `security-extended` | Workflow syntax, dangerous expressions, excessive permissions, untrusted checkout/data flows, artifact risks, and immutable references. | CodeQL runs on workflow changes and a schedule; Zizmor runs in every `lint` job. | | Build configuration | Trivy configuration scan | High/critical Containerfile and infrastructure misconfigurations. | A clean configuration scan says nothing about packages in the built image. | -| Runtime behavior | Buildx plus `tests/smoke.sh` | The exact test image starts and stops correctly under production-oriented restrictions and supports documented initialization/authentication behavior. | Hosted-runner tests do not replace native-architecture and OpenShift release qualification. | +| Runtime behavior | Native GitHub-hosted AMD64 and ARM64 runners, Buildx, and `tests/smoke.sh` | Each architecture's exact test image starts and stops correctly under production-oriented restrictions and supports documented initialization/authentication behavior. Runner and loaded-image assertions prevent emulation or a mislabeled image from being treated as native evidence. | Hosted-runner tests do not replace OpenShift qualification or application-specific performance testing. | | Image vulnerabilities | Trivy image scan | No fixed high/critical findings according to Trivy's current databases and vendor severity selection. | `ignore-unfixed` intentionally leaves unfixed risk for human release review. | | Independent inventory and scan | Syft plus Grype | SPDX inventory of the tested image and a second vulnerability matcher/database; fixed high/critical findings block. | Overlap is intentional, but scanner agreement is not proof of absence. | | Supply-chain posture | OpenSSF Scorecard | Repository and build-pipeline practice signals published independently. | Historical and popularity signals improve only through genuine project operation. | @@ -31,8 +31,8 @@ No additional general-purpose scanner is currently justified. Dependency Review ## Pull request and merge process -1. A pull request starts the two required checks: `lint` and `image`. Workflow-file changes also start CodeQL. -2. Reviewers inspect the diff, check annotations, scanner summaries, smoke-test version, and the retained SBOM/SARIF. They confirm skipped steps are expected for the event and review warnings or ignored findings. +1. A pull request starts the two protected checks: `lint` and the aggregate `image` check. The aggregate succeeds only after both `image (amd64)` and `image (arm64)` succeed. Workflow-file changes also start CodeQL. +2. Reviewers inspect both architecture jobs, the diff, annotations, scanner summaries, smoke-test versions, and the retained architecture-specific SBOM/SARIF artifacts. They confirm skipped steps are expected for the event and review warnings or ignored findings. 3. The active `main` ruleset requires a pull request, resolved review threads, and the latest `lint` and `image` results before merge; it also blocks deletion and force pushes. Required approving reviews remain deliberately disabled until the post-first-release review described in [ROADMAP.md](ROADMAP.md). 4. A merge starts `CI` on the exact `main` commit. Workflow changes start CodeQL, and every main push refreshes Scorecard. These post-merge runs are reviewed because merge-commit context, secrets, permissions, and SARIF publication differ from pull requests. 5. Weekly schedules refresh time-sensitive vulnerability and workflow analysis even when source has not changed. Manual dispatch supports investigation; it is not a substitute for the pull-request checks. @@ -46,7 +46,7 @@ Repository configuration is part of the security boundary, even though it is not - Actions are enabled, the default `GITHUB_TOKEN` permission is read-only, and workflows cannot approve pull requests. - GitHub requires third-party Actions to be referenced by a full commit SHA. Workflow files also keep the release tag in a comment for review and Dependabot updates. -- The `Protect main` ruleset requires pull requests, resolved review threads, and successful, up-to-date `lint` and `image` checks; it prevents branch deletion and non-fast-forward updates. The required approval count is intentionally zero for now. +- The `Protect main` ruleset requires pull requests, resolved review threads, and successful, up-to-date `lint` and aggregate `image` checks; it prevents branch deletion and non-fast-forward updates. The aggregate preserves the stable protected-check name while requiring both native architecture jobs. The required approval count is intentionally zero for now. - Secret scanning, push protection, Dependabot security updates, and private vulnerability reporting are enabled. - Workflow permissions are narrowed per job; only code-scanning publication, OIDC signing, package publication, and release creation receive write scopes. @@ -57,7 +57,7 @@ Audit these settings before each release and after organization policy changes. For every required run, verify the event and head SHA first. Then review the following evidence: - `lint`: every hook and the release-tag test ran, Zizmor audited every workflow, and there are no warnings or annotations hidden behind a successful wrapper. -- `image`: the configuration scan count, ClickHouse version printed by the smoke suite, Trivy target/OS/package count and result count, SBOM package count, and Grype found-versus-ignored counts. +- `image (amd64)` and `image (arm64)`: the native runner assertion, loaded-image architecture assertion, configuration scan count, ClickHouse version printed by the smoke suite, Trivy target/OS/package count and result count, SBOM package count, and both Grype's blocking fixed-findings result and full finding inventory. The aggregate `image` job is only the merge gate; inspect the two jobs that produced the evidence. - `CodeQL` and Scorecard: analysis covered the intended files, SARIF processing completed, and the Security tab has no new open alert. A successful upload is not the same as zero findings. - skipped steps: PR SARIF publication is intentionally skipped to avoid permission failures from untrusted forks; it runs on `main`. A skipped build, smoke test, or scanner is not acceptable. - warnings: Trivy may use another vendor's severity when Red Hat data is absent. Grype's `only-fixed` option can ignore real but currently unfixable findings. Review both against Red Hat and ClickHouse advisories before a release. @@ -66,14 +66,15 @@ Record accepted findings in the release pull request with the advisory, affected ## Image security pipeline -The image job runs these controls in order: +Each native image matrix job runs these controls in order. AMD64 uses `ubuntu-24.04` and `linux/amd64`; ARM64 uses `ubuntu-24.04-arm` and `linux/arm64`. QEMU is not installed and does not count as native-runtime evidence. 1. **Trivy configuration scan** checks the `Containerfile`, Compose configuration, and repository infrastructure configuration for high and critical misconfigurations. 2. **Build and smoke tests** exercise startup, authentication, initialization, persistence, shutdown, read-only operation, dropped capabilities, and arbitrary UIDs. 3. **Trivy image scan** blocks fixed high and critical operating-system or application vulnerabilities and reports its detected OS and package count for review. -4. **Syft inventory** generates `clickhouse-server-ubi9.spdx.json` in SPDX JSON format from the tested image. -5. **Grype SBOM scan** scans that exact SPDX document and blocks fixed high and critical vulnerabilities. Its log may also report ignored unfixed matches, which remain part of release review. -6. **Artifact and SARIF publication** retains the inventory and result for investigation and publishes non-PR Grype results to GitHub code scanning. +4. **Complete SPDX inventory** uses Syft to inventory the tested filesystem and RPM database, then `scripts/augment-spdx.py` declares the three pinned ClickHouse TGZ components that have no RPM metadata. The script takes their version and channel from `Containerfile`, records Apache-2.0 licensing and package identifiers, and fails instead of duplicating a component Syft already found. +5. **Blocking Grype SBOM scan** scans that exact SPDX document and blocks fixed high and critical vulnerabilities. +6. **Full Grype inventory** performs a non-blocking scan of the same SBOM without filtering unfixed matches and retains `grype-all.json`. Non-blocking means “record for triage,” not “accepted risk.” +7. **Artifact and SARIF publication** retains the inventory and results for investigation and publishes fixed Grype findings from non-PR runs to GitHub code scanning. Trivy and Grype deliberately overlap. They use different databases and matching logic, so a clean result from one does not replace the other. Both gates ignore vulnerabilities without an upstream fix; unfixed findings still require periodic review before release. Scanner disagreements should be investigated against the vendor advisory and documented if accepted. @@ -81,11 +82,12 @@ Trivy and Grype deliberately overlap. They use different databases and matching | Artifact | Location | Retention or lifecycle | Purpose | | --- | --- | --- | --- | -| `clickhouse-server-ubi9.spdx.json` | CI artifact `image-security-` | 14 days | Package inventory for the exact tested image. | -| `grype.sarif` | Same CI artifact and GitHub code scanning on non-PR runs | 14 days for the downloadable artifact | Machine-readable findings and review evidence. | +| `clickhouse-server-ubi9-.spdx.json` | CI artifact `image-security--` | 14 days | Package inventory for the exact native AMD64 or ARM64 test image. | +| `grype-.sarif` | Same architecture-specific CI artifact and GitHub code scanning on non-PR runs | 14 days for the downloadable artifact | Machine-readable findings and architecture-specific review evidence. | +| `grype-all-.json` | Architecture-specific CI artifact | 14 days | Complete point-in-time inventory including unfixed Low and Medium matches for human triage. The release workflow separately retains `grype-all.json` for 30 days. | | `image.spdx.json` | Tag-run artifact and GitHub release asset | 30-day Actions copy; release asset retained with the release | Downloadable inventory for the published digest. | | Release `grype.sarif` | Tag-run artifact and GitHub code scanning | 30 days for the downloadable artifact | Point-in-time scan evidence; not attached to the release because vulnerability data ages rapidly. | -| BuildKit SBOM and provenance | OCI registry attestations; downloaded together as `image.intoto.jsonl` | Lifetime of the package/release | Registry-native inventory and build provenance. | +| BuildKit SBOM/provenance and complete SPDX attestation | OCI registry attestations; downloaded together as `image.intoto.jsonl` | Lifetime of the package/release | Registry-native build evidence plus the keyless, digest-bound copy of `image.spdx.json`. | | `image.sigstore.json` | GitHub release asset | Lifetime of the release | Offline verification bundle for the keyless image signature. | | Scorecard SARIF | Scorecard workflow artifact and code scanning | 5 days for the workflow artifact | Supply-chain control findings. | @@ -93,26 +95,46 @@ Upload steps use `always()` so useful evidence survives a vulnerability gate fai ## Reproducing checks locally -Run the same repository checks and build first: +Run the repository checks and build on a native Linux host. Set the expected values to `amd64`/`x86_64` on an AMD64 host or `arm64`/`aarch64` on an ARM64 host: ```console pre-commit run --all-files --show-diff-on-failure -docker build --file Containerfile --tag clickhouse-server-ubi9:test . -IMAGE=clickhouse-server-ubi9:test bash tests/smoke.sh +ARCHITECTURE=amd64 +MACHINE=x86_64 +test "$(uname -m)" = "${MACHINE}" +podman build --format docker --platform "linux/${ARCHITECTURE}" \ + --file Containerfile --tag "clickhouse-server-ubi9:test-${ARCHITECTURE}" . +test "$(podman image inspect --format '{{.Architecture}}' \ + "clickhouse-server-ubi9:test-${ARCHITECTURE}")" = "${ARCHITECTURE}" +CONTAINER_RUNTIME=podman \ + IMAGE="clickhouse-server-ubi9:test-${ARCHITECTURE}" bash tests/smoke.sh +``` + +Running an ARM64 image under emulation on an AMD64 workstation can help diagnose portable build failures, but it does not reproduce the native ARM64 qualification. GitHub's `ubuntu-24.04-arm` runner supplies that evidence using Docker Engine and Buildx. To reproduce both jobs faithfully, run the procedure once on each native architecture and retain separate results. Podman and Docker exercise the same image contract but remain distinct runtime implementations, so first-release evidence records both the native CI results and the separately tested Podman version. + +For the scanner examples below, keep using the architecture-specific image name: + +```console +ARCHITECTURE=amd64 +IMAGE="clickhouse-server-ubi9:test-${ARCHITECTURE}" ``` With Trivy, Syft 1.51.1, and Grype 0.118.0 installed from their official release instructions: ```console trivy config --severity HIGH,CRITICAL --exit-code 1 . -trivy image --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1 \ - clickhouse-server-ubi9:test -syft clickhouse-server-ubi9:test --output spdx-json=clickhouse-server-ubi9.spdx.json -grype sbom:clickhouse-server-ubi9.spdx.json \ +trivy image --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1 "${IMAGE}" +syft "${IMAGE}" --output "spdx-json=clickhouse-server-ubi9-${ARCHITECTURE}.spdx.json" +python scripts/augment-spdx.py \ + --input "clickhouse-server-ubi9-${ARCHITECTURE}.spdx.json" \ + --output "clickhouse-server-ubi9-${ARCHITECTURE}.spdx.json" +grype "sbom:clickhouse-server-ubi9-${ARCHITECTURE}.spdx.json" \ --only-fixed --fail-on high --output table +grype "sbom:clickhouse-server-ubi9-${ARCHITECTURE}.spdx.json" \ + --fail-on critical --output json > "grype-all-${ARCHITECTURE}.json" ``` -The vulnerability databases are time-dependent, so a local result can differ from an earlier workflow. Record the database update time and scanner version when investigating a discrepancy. Do not commit generated SBOM or SARIF files; CI and releases are their authoritative storage locations. +The second local command can return nonzero if a Critical finding exists even though it still writes JSON; inspect the file and the status. Vulnerability databases are time-dependent, so a local result can differ from an earlier workflow. Record the database update time and scanner version when investigating a discrepancy. Do not commit generated SBOM, SARIF, or full-scan JSON files; CI and releases are their authoritative storage locations. ## Contributor expectations @@ -125,9 +147,9 @@ The vulnerability databases are time-dependent, so a local result can differ fro ## Release behavior -The release workflow builds and pushes a multi-architecture manifest before scanners run because both architectures must be addressed by the immutable registry digest. If a post-push scan fails, the workflow does not sign or create a GitHub release, but the registry may contain the non-release tag and digest. Maintainers must investigate and remove or clearly quarantine such failed candidates through the GHCR interface. +The release workflow builds and pushes a multi-architecture manifest before scanners run because both architectures must be addressed by the immutable registry digest. It immediately inspects that digest and fails unless Linux AMD64 and ARM64 descriptors are both present. If manifest validation or a post-push scan fails, the workflow does not sign or create a GitHub release, but the registry may contain the non-release tag and digest. Maintainers must investigate and remove or clearly quarantine such failed candidates through the GHCR interface. -After both scans pass, the workflow signs the digest through GitHub OIDC, downloads its attestations, and creates a GitHub release containing the SPDX SBOM, Sigstore bundle, and in-toto evidence. Follow the final checklist in [ROADMAP.md](ROADMAP.md) for the first release. +After both scans pass, the workflow publishes the complete SPDX document as a keyless, digest-bound `spdxjson` attestation and signs the digest through GitHub OIDC. It then downloads all attestations and creates a GitHub release containing the SPDX SBOM, Sigstore bundle, and in-toto evidence. Follow the final checklist in [ROADMAP.md](ROADMAP.md) for the first release. Authoritative references: @@ -138,5 +160,6 @@ Authoritative references: - [Trivy documentation](https://trivy.dev/latest/docs/) - [GitHub artifact storage](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) - [Uploading SARIF to GitHub](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github) +- [GitHub-hosted runners, including Arm64 labels](https://docs.github.com/en/actions/reference/runners/github-hosted-runners) - [BuildKit attestations](https://docs.docker.com/build/metadata/attestations/) - [Cosign container signing](https://docs.sigstore.dev/cosign/signing/signing_with_containers/) diff --git a/docs/ENDOR.md b/docs/ENDOR.md index c99fb64..64421a8 100644 --- a/docs/ENDOR.md +++ b/docs/ENDOR.md @@ -26,7 +26,9 @@ The controls must exist for users and risk reduction first. A later score increa ## Current integration decision -As of 2026-09-06, no Endor namespace or authorization policy is configured in this repository and no Endor result is available to claim as a baseline. Adding the GitHub Action now would either fail CI, require a long-lived secret, or remain skipped forever. It would also duplicate existing scans without creating an Endor-monitored default-branch baseline. +As of 2026-09-06, Endor's pricing page offers a free **AURI for Developers** tier for individual, local editor use. It requires no account and provides read-only vulnerability intelligence, but it has no dashboard, policies, or scan history. Endor's documentation says standalone `endorctl` scanning requires an Endor namespace; the Open Source Core and Pro platform tiers use sales-based, per-contributor licensing. The free developer tool is therefore not a free hosted open-source-project/CI tier. + +No Endor namespace or authorization policy is configured in this repository and no Endor result is available to claim as a baseline. Do not add an Endor GitHub Action now. Without a purchased or explicitly sponsored tenant it would fail, require inappropriate credentials, or remain permanently skipped, while duplicating parts of the existing scan stack. Continue using the existing free/open tools—Trivy, Grype, Syft, CodeQL, Zizmor, and OpenSSF Scorecard—and revisit Endor only if Endor offers the project a suitable program or Datopsis decides the distinct reachability/RSPM benefits justify procurement. Do not add `ENDOR_API_CREDENTIALS_KEY` or `ENDOR_API_CREDENTIALS_SECRET` merely to get started. Endor recommends GitHub OIDC keyless authentication for CI. Installing the Endor GitHub App is also an organization-level trust decision because cloud scanning grants the app repository access and sends retained scan metadata to the Endor tenant. Datopsis must review the requested permissions and Endor's data-handling terms before installation. @@ -55,6 +57,9 @@ For each review, capture the Endor project/version, scan time, policy version, f Authoritative references: +- [Current Endor Labs pricing and Developer-tier limits](https://www.endorlabs.com/pricing) +- [AURI for Developers](https://www.endorlabs.com/platform/developer) +- [Endor Labs license model](https://docs.endorlabs.com/introduction/licenses) - [Endor Scores](https://docs.endorlabs.com/scan/sca/scores) - [Repository code-quality score factors](https://docs.endorlabs.com/scan/sca/scores/repository-scores/code-quality-score-factors/) - [Repository activity score factors](https://docs.endorlabs.com/scan/sca/scores/repository-scores/activity-score-factors) diff --git a/docs/IMAGE-COMPARISON.md b/docs/IMAGE-COMPARISON.md new file mode 100644 index 0000000..01deb79 --- /dev/null +++ b/docs/IMAGE-COMPARISON.md @@ -0,0 +1,48 @@ +# Comparison with the official ClickHouse image + +This project is an alternative package of the same upstream ClickHouse release, not a drop-in clone of every official-image behavior. The following snapshot was measured on `linux/amd64` on 2026-09-06 using this repository's local test image and `clickhouse/clickhouse-server:26.8.2.7`. + +## Image and runtime + +| Area | Datopsis UBI image | Official Ubuntu image | +| --- | --- | --- | +| Base | Red Hat UBI 9 Micro | Ubuntu 22.04 | +| ClickHouse | Official `clickhouse-common-static`, server, and client TGZ content, SHA-512 verified | Official ClickHouse Debian packages | +| Default process user | `101:0` in image metadata; never starts as root | Starts as root by default, prepares/chowns paths, then switches to UID/GID 101; root/custom-ID modes are supported | +| Package manager/download tool | No `dnf`, `microdnf`, `apt`, `curl`, or `wget` | `apt`, `apt-get`, and `wget` remain installed | +| Other notable tools | Bash, coreutils-single, gzip, CA trust, timezone data | Bash, BusyBox links, coreutils, gzip, OpenSSL CLI, CA trust, locales, timezone data, and normal Ubuntu base utilities | +| Installed OS package records | 34 RPM database entries, including 2 signing-key records | 111 Debian packages | +| Local unpacked storage | 845,298,018 bytes (806.1 MiB) | 902,021,165 bytes (860.2 MiB) | +| Filesystem layers | 7 | 11 | +| Built-in health check | Authenticated `SELECT 1` over configured native TCP or native TLS | None in image metadata | +| Read-only root baseline | Supported and smoke-tested | Not the official default; its entrypoint writes configuration and manages ownership | + +The UBI image was 56,723,147 bytes (54.1 MiB, about 6.3%) smaller by this local engine's unpacked-size measurement. Registry transfer size, manifest/attestation size, storage-driver sharing, and `arm64` size are different measurements and must be captured from the actual release digest before making published size claims. ClickHouse's static binary dominates both images, so a much smaller base produces a modest total reduction. + +## Ports and volumes + +Both images declare ports `8123` (HTTP), `9000` (native TCP), and `9009` (inter-server HTTP), and both declare `/var/lib/clickhouse` as a volume. Neither declaration publishes a port or protects it with a firewall. This image logs to the container console and does not require a separate log volume. + +Secure ports are configured rather than declared by default: `8443` for HTTPS, `9440` for native TLS, and `9010` for inter-server HTTPS. See [TLS certificates and trust](TLS.md). + +## Environment and entrypoint compatibility + +| Variable/behavior | Datopsis UBI image | Official image | +| --- | --- | --- | +| `CLICKHOUSE_PASSWORD` / `_FILE` | Yes | Yes | +| `CLICKHOUSE_DB` | Yes; validated as an unquoted identifier | Yes | +| `CLICKHOUSE_USER` | Only `default`; another value fails with guidance | Creates/configures a named user | +| `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` | No; default user gets access management | Yes | +| `CLICKHOUSE_SKIP_USER_SETUP` | Deliberately no insecure bypass | Yes | +| `CLICKHOUSE_ALWAYS_RUN_INITDB_SCRIPTS` | Yes | Yes | +| `CLICKHOUSE_INIT_TIMEOUT` | Yes; default `60` seconds | Yes; default `1000` retries | +| `CLICKHOUSE_CONFIG` | Yes | Yes | +| Primary data path | Extracted from the effective ClickHouse config; default `/var/lib/clickhouse` | Extracted from the effective ClickHouse config | +| `CLICKHOUSE_RUN_AS_ROOT`, UID/GID, no-chown options | No; fixed non-root security model | Yes | +| Init files | Executable/sourced `.sh`, `.sql`, `.sql.gz`, lexical order | Same file types; shell glob order | +| Additional configured disk paths | Entrypoint discovers and creates paths as the current identity; operator must provision writable mounts and ownership | Official entrypoint discovers paths and can create/chown them when it starts as root; non-root mode still requires writable parents | +| Arbitrary OpenShift UID | Smoke-tested with group `0` permissions | Custom `--user` documented upstream; different entrypoint model | + +Applications that depend on the official image's named-user creation, root mode, automatic custom-disk ownership, skip-user-setup escape hatch, or exact initialization semantics need an explicit migration plan. Do not add those features automatically: several conflict with this image's non-root and least-privilege goals. This image prepares configuration-derived paths only with its existing identity and never claims it can repair a host, PVC, or NFS export. See [rootless storage and permissions](ROOTLESS.md). + +The comparison source is ClickHouse's [official image documentation](https://github.com/ClickHouse/ClickHouse/blob/master/docker/server/README.md), [Ubuntu Dockerfile](https://github.com/ClickHouse/ClickHouse/blob/master/docker/server/Dockerfile.ubuntu), and [entrypoint](https://github.com/ClickHouse/ClickHouse/blob/master/docker/server/entrypoint.sh). Re-measure on every supported release because upstream content and behavior change. diff --git a/docs/OPENSSF_SCORECARD.md b/docs/OPENSSF_SCORECARD.md index 9290155..536b58d 100644 --- a/docs/OPENSSF_SCORECARD.md +++ b/docs/OPENSSF_SCORECARD.md @@ -93,7 +93,7 @@ Use a GitHub token with read access. Do not place the token on the command line ```bash export GITHUB_AUTH_TOKEN="$(gh auth token)" -docker run --rm \ +podman run --rm \ --env GITHUB_AUTH_TOKEN \ ghcr.io/ossf/scorecard:v5.5.0@sha256:2ad2ced1cc8d080a589fac211944834c0da3dd82a4d7b0e70a642b6be76987d7 \ --repo=github.com/datopsis/clickhouse-server-ubi9 \ diff --git a/docs/PODMAN.md b/docs/PODMAN.md new file mode 100644 index 0000000..03d7e9e --- /dev/null +++ b/docs/PODMAN.md @@ -0,0 +1,64 @@ +# Podman compatibility and version support + +Podman is the primary documented local container engine for this Red Hat UBI image. The published OCI image remains portable: GitHub Actions uses Docker Engine and Buildx for native AMD64/ARM64 qualification and multi-architecture release assembly, while user procedures use Podman unless they specifically describe that CI implementation or compare with the official ClickHouse image. + +## Supported and tested versions + +The first-release support baseline is Podman 5.3 or newer. This is an evidence-based support floor, not a known technical minimum. The complete local smoke suite was run on 2026-09-07 with: + +| Component | Version | Platform | +| --- | --- | --- | +| Podman client | 5.3.2 | Windows AMD64 | +| Podman server | 5.3.1 | Linux AMD64 in Podman Machine | + +Run `podman version` and record both client and server versions when using a remote client. The server version and architecture execute the container; the client values alone are not runtime qualification evidence. Native Linux AMD64 and ARM64 image behavior is separately qualified in GitHub Actions, currently using Docker Engine. A future Podman version or architecture result should be added here only after the complete smoke suite passes. + +## Build and smoke test + +Podman's default build format is OCI. Use Docker manifest format for local builds because the OCI image configuration does not preserve the Containerfile `HEALTHCHECK` metadata used by the smoke suite: + +```console +podman build --format docker --file Containerfile \ + --tag ghcr.io/datopsis/clickhouse-server-ubi9:test . +CONTAINER_RUNTIME=podman \ + IMAGE=ghcr.io/datopsis/clickhouse-server-ubi9:test \ + bash tests/smoke.sh +``` + +Images pulled from the project's release registry already contain the release workflow's image metadata; `--format docker` is a local build instruction, not a `podman pull` requirement. + +## Rootless operation and storage + +Prefer rootless Podman and a named volume for the basic case. Named volumes avoid host user-namespace ownership calculations: + +```console +podman volume create clickhouse-data +podman run --detach --name clickhouse \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume clickhouse-data:/var/lib/clickhouse \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +For a Linux bind mount, use `podman unshare chown 101:0 ` to express the image identity through the rootless user namespace. Do not assume that container UID `101` must appear as host UID `101`. Podman's `:U` volume option is an alternative, but it recursively changes the host tree and can be slow. Follow [rootless storage and permissions](ROOTLESS.md) for complete named-volume, bind-mount, SELinux, additional-disk, and OpenShift procedures. + +On macOS and Windows, Podman normally uses a remote Linux virtual machine. Bind-mount source paths are resolved and made available through that environment, and their ownership behavior differs from a native Linux host. Prefer a named volume for quick starts and perform production storage qualification on the target Linux or OpenShift storage implementation. + +## Compose + +`podman compose` is a wrapper around an external Compose provider. Install either `podman-compose` or another compatible provider, confirm the selected provider with `podman compose version`, change the example password in `compose.yaml`, and then run: + +```console +podman compose up --detach +podman compose ps +podman compose down +``` + +Removing the Compose application does not replace an intentional backup or retention decision for the named data volume. + +Authoritative references: + +- [Podman build formats](https://docs.podman.io/en/stable/markdown/podman-build.1.html) +- [Podman run volume ownership and user namespaces](https://docs.podman.io/en/latest/markdown/podman-run.1.html) +- [Podman Compose providers](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md new file mode 100644 index 0000000..b59fcb8 --- /dev/null +++ b/docs/PRODUCTION.md @@ -0,0 +1,99 @@ +# Production deployment guide + +Production readiness is a property of a tested deployment, not an image label. Complete this runbook for the exact image digest, architecture, ClickHouse topology, storage class, configuration, and platform that will be operated. + +## 1. Choose the architecture and support boundary + +- Decide whether one node is sufficient. A single node has no database-service high availability; use ClickHouse replication and Keeper only after designing failure domains, quorum, inter-server authentication, and recovery. +- Use a ClickHouse LTS line when long maintenance windows matter, and document the supported UBI/ClickHouse combinations and end-of-support date. +- Assign owners for the image, database, storage, certificates, backups, vulnerability triage, and incident response. +- Pin `ghcr.io/datopsis/clickhouse-server-ubi9@sha256:`. Verify the release signature, SBOM, and provenance before promotion. Never deploy a failed or unsigned candidate tag. + +## 2. Prepare identity and secrets + +- Run non-root with `runAsUser: 101`, `runAsGroup: 0`, or a platform-assigned OpenShift UID with group `0`. Set `runAsNonRoot`, drop all capabilities, enable `no-new-privileges`, and use the runtime default seccomp profile. +- Store the password in a Podman/Kubernetes secret and set `CLICKHOUSE_PASSWORD_FILE`; do not put it in a manifest, command history, or image layer. +- Create named users and roles with SQL or mounted `users.d` fragments. The environment-variable interface intentionally manages only `default`. +- Rotate credentials and certificates through a rehearsed rolling procedure. See [TLS certificates and trust](TLS.md). + +## 3. Provision durable storage + +- Mount durable, low-latency storage at the effective ClickHouse ``, which defaults to `/var/lib/clickhouse`. Configure custom paths in ClickHouse XML rather than an environment variable. Size it for data, merges, mutations, temporary work, replication backlog, and recovery headroom—not only current table bytes. +- Set permissions for UID `101`, group `0`, and group write, or validate the platform's arbitrary-UID policy. Test the actual CSI/NFS/storage backend; root-squash and `fsGroup` behavior vary. +- Provision every configured local disk and metadata path before startup. Follow [rootless storage and permissions](ROOTLESS.md) for named volumes, bind mounts, Kubernetes/OpenShift identities, SELinux, and failure diagnostics. +- Keep the root filesystem read-only and mount `/tmp` as a bounded `tmpfs`. Do not place durable data or backups in the container writable layer. +- Define disk-full alerts and retention policies. A persistent volume is not a backup. + +## 4. Configure networking and TLS + +- Expose only interfaces clients actually use. Restrict `8123`/`8443` and `9000`/`9440` by network policy and firewall. Never expose inter-server port `9009` or `9010` to untrusted networks. +- Use TLS for traffic crossing an untrusted boundary. Prefer platform TLS termination where appropriate; configure direct ClickHouse TLS for native TCP or end-to-end requirements. +- Allow-list required egress destinations. Public endpoints use the included CA store; internal services require an explicit CA bundle. +- Use stable DNS, synchronized clocks, and certificate SANs matching the names clients use. + +## 5. Set resources and kernel limits + +- Set `nofile=262144:262144`, as used by the smoke suite and upstream examples. +- Establish CPU and memory requests from a representative load test. Set memory limits only after accounting for queries, caches, merges, dictionaries, and concurrent background work; an arbitrary low container limit causes OOM kills. +- Align ClickHouse memory and concurrency settings with the container limit so ClickHouse rejects or queues work before the kernel kills it. +- Benchmark ingestion, representative queries, merges, restart recovery, backup, and restore on the production storage class. Record saturation thresholds and safe operating headroom. +- Add `IPC_LOCK`, `NET_ADMIN`, or `SYS_NICE` only when a tested feature requires it and the risk is approved. The baseline needs none. + +This project does not publish universal CPU/memory values because workload shape and storage dominate them. Release approval requires recorded test inputs, p50/p95/p99 latency, throughput, peak memory, disk growth, and recovery time for the target deployment. + +## 6. Health, startup, and shutdown + +The image health check runs an authenticated local `SELECT 1`. For orchestrators, use separate probes: + +- startup: allow enough time for metadata loading and recovery on the largest tested data set; +- readiness: query `/ping` or `SELECT 1` with an appropriately scoped secret so traffic stops before shutdown; +- liveness: use a conservative threshold so load spikes do not create a restart loop. + +Set a termination grace period longer than the measured ClickHouse shutdown time. Confirm the runtime sends `SIGTERM` to PID 1 and waits. Test node drain, forced termination, startup after an unclean stop, and a full-volume condition. + +## 7. Back up and prove restoration + +- Choose a ClickHouse-supported backup design appropriate to local disks, object storage, or replicated topology. Back up access-control/configuration material and encryption keys separately from table data. +- Define recovery point and recovery time objectives, retention, immutability, off-site/off-cluster copies, and who can restore. +- Restore into an isolated environment on a schedule and validate row counts, schemas, users, dictionaries, and representative queries. A successful backup job without a restore test is insufficient evidence. +- Before an upgrade, take and validate the required backup or snapshot and retain the previous image digest and configuration for the documented rollback window. + +## 8. Observe and operate + +- Collect container stdout/stderr centrally. Alert on repeated restarts, failed queries, authentication failures, replica delay, Keeper quorum, background merge pressure, disk/inode exhaustion, memory pressure, certificate expiry, and backup failure. +- Scrape ClickHouse metrics and define service-level indicators for availability, ingestion lag, query latency, and correctness relevant to the workload. +- Protect logs and query history because SQL and errors can contain sensitive values. Set retention and access controls. +- Maintain dashboards, on-call routing, incident runbooks, capacity forecasts, and a tested break-glass process. + +## 9. Patch and upgrade + +1. Renovate proposes new ClickHouse versions and UBI digests; review upstream release notes and security advisories. +2. Build both architectures, run the smoke suite and scanners, and inspect the SBOM delta. +3. Restore a recent backup into staging and run application compatibility, performance, TLS, and failure tests. +4. Roll out through a canary or one replica at a time while monitoring merges, replication, errors, and latency. +5. Do not downgrade data files unless ClickHouse explicitly supports that path. Rollback normally means restore/recover according to the tested plan. + +For unchanged ClickHouse versions, rebuild when UBI publishes relevant security updates. Review the [vulnerability process](VULNERABILITY-MANAGEMENT.md) rather than treating unfixed counts as patchable versions. + +## 10. Disconnected environments + +- Mirror the exact verified image digest and its release evidence through a controlled transfer station. Import into an internal registry and record the resulting internal digest mapping. +- Mirror vulnerability databases and update metadata on a defined cadence; an offline scan with a stale database is not equivalent to current CI. +- Mirror every artifact needed for rebuilds if the disconnected side must build: both UBI images by digest, ClickHouse archives and `.sha512` files, build tools, and Actions/tool binaries as applicable. +- Use an internal CA and offline certificate lifecycle as described in [TLS certificates and trust](TLS.md). Mirror time sources, package/advisory data, and revocation information required by policy. +- Rehearse image promotion, certificate renewal, vulnerability-data refresh, backup restore, and rollback entirely inside the boundary. + +## Go-live evidence + +Before declaring a deployment supported, retain: + +- exact image/configuration digests and signature verification; +- native `amd64` or `arm64` test evidence; +- security-context, network-policy, TLS, secret, and storage review; +- representative capacity and failure-test results; +- successful backup restoration and measured recovery times; +- current vulnerability triage with owners and expiries; +- monitoring screenshots/queries, alert routing, runbooks, and support contacts; +- approved upgrade, rollback, incident, and disconnected-update procedures. + +The repository's [first-release roadmap](ROADMAP.md) remains the publication gate; this document is the deployment operator's runbook. diff --git a/docs/QUALIFICATION.md b/docs/QUALIFICATION.md new file mode 100644 index 0000000..3a93124 --- /dev/null +++ b/docs/QUALIFICATION.md @@ -0,0 +1,30 @@ +# Release qualification evidence + +This file records durable links and concise results for first-release gates. Downloadable GitHub Actions artifacts expire according to [CI retention policy](CI.md#artifacts-and-retention); the workflow and job logs remain the review record after artifact expiry. Results apply only to the identified commit, image inputs, scanner databases, architecture, and runtime. + +## Native AMD64 and ARM64 CI — 2026-09-07 + +- Pull request: [#5](https://github.com/datopsis/clickhouse-server-ubi9/pull/5) +- Head commit: `2e2b07045990c31acbab32c59f53e2979efbedd5` +- Pull-request merge commit tested by CI: `523ae3754ef8aa81bd8a3f765ec2591dcb8f2182` +- Workflow: [CI run 34125673268](https://github.com/datopsis/clickhouse-server-ubi9/actions/runs/34125673268) +- Aggregate `image` gate: passed after both native jobs completed. + +| Evidence | AMD64 | ARM64 | +| --- | --- | --- | +| Native job | [image (amd64)](https://github.com/datopsis/clickhouse-server-ubi9/actions/runs/34125673268/job/101753657567) | [image (arm64)](https://github.com/datopsis/clickhouse-server-ubi9/actions/runs/34125673268/job/101753657581) | +| Runner architecture | `x86_64` | `aarch64` | +| Loaded image architecture | `amd64` | `arm64` | +| ClickHouse smoke result | 26.8.2.7 passed | 26.8.2.7 passed | +| Trivy image inventory | Red Hat 9.8, 32 packages | Red Hat 9.8, 32 packages | +| Trivy fixed High/Critical gate | 0 findings | 0 findings | +| Augmented SPDX/Grype inventory | 37 packages | 37 packages | +| Blocking Grype result | Passed; all 24 matches were excluded by the fixed High/Critical gate | Passed; all 24 matches were excluded by the fixed High/Critical gate | +| Full Grype inventory | Retained; 24 unfixed matches require release triage | Retained; 24 unfixed matches require release triage | +| Security artifact | [AMD64 artifact 10020245728](https://github.com/datopsis/clickhouse-server-ubi9/actions/runs/34125673268/artifacts/10020245728) | [ARM64 artifact 10020200009](https://github.com/datopsis/clickhouse-server-ubi9/actions/runs/34125673268/artifacts/10020200009) | + +This is successful architecture and fixed-vulnerability-gate evidence, not acceptance of the 24 unfixed Grype matches. Those findings remain subject to the documented release triage, ownership, compensating-control, and expiry process. + +## Podman runtime — 2026-09-07 + +The complete smoke suite passed locally for ClickHouse 26.8.2.7 using Podman client 5.3.2 on Windows AMD64 and Podman server 5.3.1 on Linux AMD64 in Podman Machine. This establishes the current Podman 5.3 support baseline. It does not replace native Linux ARM64 Podman evidence or target-platform OpenShift qualification. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d52d30f..303c193 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,9 +4,110 @@ This roadmap is the release gate for the first supported image. A checked item m ## Release blockers +### Immediate priorities + +1. Triage the current unfixed matches according to [VULNERABILITY-MANAGEMENT.md](VULNERABILITY-MANAGEMENT.md), then rebuild on the newest reviewed UBI digests and retain the before/after evidence. +2. Validate [TLS.md](TLS.md) with CA-issued certificates for HTTPS and native TCP, including rotation and a fully disconnected rehearsal. +3. Execute [PRODUCTION.md](PRODUCTION.md) on native `amd64`, native `arm64`, and OpenShift, recording resource, storage, backup/restore, shutdown, and recovery evidence. +4. Complete the ClickHouse/UBI notice and SBOM review described in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md). +5. Publish the first signed GHCR release only after every blocker below is complete. Reconsider Docker Hub and paid security services after that release has real consumer demand. + +### Incremental delivery plan + +Complete these work packages in order. Each package should normally be a separate pull request and commit so its behavior, documentation, and evidence can be reviewed before the next package changes the same release surface. Do not check a package merely because its implementation exists; retain the listed exit evidence. + +This repository owns image-specific behavior, basic usage, and minimal platform qualification. Production-ready deployment compositions, cluster topology, operational automation, and environment overlays belong in the separate `clickhouse-production-stack` repository. Qualification fixtures here should be directly reusable there where practical, but must not grow into a second deployment stack. + +#### 1. Rootless storage and configuration contract + +**Implementation** + +- [x] Replace `CLICKHOUSE_DATA_DIR` as an independent source of truth. Extract the effective ClickHouse `path` from `CLICKHOUSE_CONFIG`, use it for initialization-state detection and the server working directory, and either remove the environment variable or make any retained compatibility behavior fail on disagreement. +- [x] Make the generated users-configuration path consistent with a custom primary data path without requiring a writable container root. +- [x] Discover local writable paths from the effective configuration: the primary path, `tmp_path`, `user_files_path`, `format_schema_path`, file log directories, and every configured disk `path` and `metadata_path`. +- [x] Create missing directories as the current identity and validate that required paths are writable. Never start as root or perform an entrypoint `chown`; report the failing path and current UID/GID with actionable guidance. +- [x] Preserve compatibility with arbitrary OpenShift UIDs, UID `101`, group `0`, read-only roots, SELinux bind mounts, and storage backends where root squash prevents ownership repair. + +**Tests** + +- [x] Add smoke and deterministic cases for the default path, a custom config-derived primary path, additional local disk and metadata paths, restart persistence, and initialization detection on an existing custom data directory. +- [x] Add negative cases for a non-writable primary path, non-writable additional disk, disagreement with any retained legacy environment variable, relative or empty path handling, and operation without root or extra capabilities. +- [x] Run the path suite with UID `101:0` and an arbitrary OpenShift-style UID. Confirm that failures occur before partial initialization and contain no secret values. + +**User documentation and exit evidence** + +- [x] Add `docs/ROOTLESS.md` explaining the security model, why the image does not start as root, supported UID/GID patterns, named volumes, bind-mount preparation, Kubernetes `fsGroup`, OpenShift arbitrary UIDs, SELinux `:Z`, NFS/root-squash limitations, custom data paths, additional disks, and permission troubleshooting. +- [x] Update the README environment table, production guide, and official-image comparison so none imply that an environment variable changes ClickHouse storage by itself. +- [ ] Retain successful and intentional-failure smoke logs demonstrating every path and identity case. Review the final entrypoint against the current official ClickHouse entrypoint without copying its root/chown behavior. + +#### 2. Native architecture CI qualification + +**Implementation** + +- [x] Change the image job to an explicit native matrix: `ubuntu-24.04` with `linux/amd64` and `ubuntu-24.04-arm` with `linux/arm64`. Build and load one native image per job; do not use QEMU as native-runtime evidence. +- [x] Run the complete smoke suite, Trivy image gate, augmented SPDX generation, blocking Grype gate, and full Grype inventory on each architecture. +- [x] Give image tags, artifacts, SARIF categories, and cache scopes architecture-specific names so parallel jobs cannot overwrite or conflate evidence. +- [x] Keep the release workflow's multi-platform manifest build, then inspect the manifest and prove that it contains the tested `linux/amd64` and `linux/arm64` variants. +- [x] Preserve the protected `image` check as an aggregate job that fails unless both native matrix jobs pass before merge. + +**User documentation and exit evidence** + +- [x] Update `docs/CI.md` with runner labels, native-versus-emulated boundaries, artifact names, expected architecture checks, and local reproduction commands. +- [x] Retain successful workflow URLs and per-architecture image version, package count, SBOM, vulnerability results, and smoke logs. Record runner architecture from `uname -m` rather than inferring it only from a workflow label. See [qualification evidence](QUALIFICATION.md#native-amd64-and-arm64-ci--2026-09-07). + +#### 3. CA-issued connected and disconnected TLS rehearsal + +**Automated connected test** + +- [ ] Add a test that creates an ephemeral root and intermediate CA, issues a leaf certificate with the exact test DNS SAN, and mounts only the leaf key, chain, and public trust bundle into the container. +- [ ] Exercise HTTPS and native TLS with clear-text listeners removed. Require success with the correct CA and hostname and failure with an unrelated CA, wrong hostname, clear-text client, unreadable key, and incomplete chain. +- [ ] Exercise outbound TLS against both a normally trusted Internet endpoint and a controlled private-CA endpoint, proving the correct CA-bundle behavior for each rather than assuming all ClickHouse integrations share one TLS client configuration. +- [ ] Replace the leaf certificate through the documented rotation procedure, restart or roll out the server, verify the new serial/expiry, and prove the retired certificate is no longer served. + +**Disconnected rehearsal** + +- [ ] Add `docs/TLS-REHEARSAL.md` with connected preparation, artifact inventory, SHA-256 recording, controlled transfer, internal image import, internal DNS, offline CSR signing, Secret/read-only mount creation, network isolation, validation, rotation, rollback, and cleanup procedures. +- [ ] Preload every required image and tool, then perform the server/client phase on a network with external egress denied. Generate the server private key inside the disconnected boundary and never transfer a CA private key into the workload. +- [ ] Test inbound HTTPS/native TLS, outbound trust to an internal HTTPS endpoint, wrong-CA and wrong-hostname rejection, renewal, restart, and rollback without public DNS, ACME, OCSP, CRL, or package downloads unless an approved internal mirror is part of the design. + +**Exit evidence** + +- [ ] Retain sanitized commands, certificate subjects/issuers/SANs/serials/expiry, network-isolation proof, positive and negative connection results, rotation evidence, and confirmation that no generated private key or certificate artifact entered Git. + +#### 4. OpenShift qualification and operator procedure + +**Procedure documentation** + +- [ ] Add `docs/OPENSHIFT-TESTING.md` with step-by-step instructions for obtaining a Red Hat Developer Sandbox or using OpenShift Local, installing/logging in with `oc`, selecting a project, verifying quotas, and cleaning up all test resources. +- [ ] Provide minimal qualification resources for a digest-pinned image, password Secret, TLS Secret, configuration ConfigMap, RWO PVC, Service, and HTTPS Route where applicable. Keep native TCP behind a suitable Service or TCP-capable ingress rather than implying an HTTP Route supports it. Production overlays, topology, and automation belong in `clickhouse-production-stack`. +- [ ] Document GHCR public access and private `imagePullSecret` alternatives, restricted SCC expectations, arbitrary UID/group behavior, `runAsNonRoot`, read-only root filesystem, runtime-default seccomp, dropped capabilities, bounded `/tmp`, resource requests/limits, probes, and termination grace periods. +- [ ] Include exact commands for inspecting assigned UID/GID, SCC admission, mounts, permissions, events, logs, health, TLS, PVC binding, and image digest. Explain common `permission denied`, `CrashLoopBackOff`, route, and certificate failures from an operator's perspective. + +**Qualification run** + +- [ ] Deploy under the default restricted SCC without requesting `anyuid`, privileged mode, root, host paths, or additional capabilities. +- [ ] Verify first-start initialization, password-file handling, arbitrary-UID operation, HTTPS, native TLS inside the cluster, readiness/liveness/startup behavior, graceful deletion, PVC persistence across pod replacement, and a backup/restore of representative test data. +- [ ] Exercise a non-writable volume failure and confirm the rootless diagnostics from work package 1 identify the operator action required. +- [ ] Record the OpenShift version, cluster type, SCC, storage class/access mode, assigned UID/GID, image digest, manifests, commands, sanitized logs, and results in the release pull request. + +**Fallback decision** + +- [ ] If no real OpenShift environment can be obtained, run a restricted Kubernetes proxy test and explicitly mark OpenShift as unvalidated and unsupported in the first release. A Kind/K3s test does not satisfy or replace the OpenShift checklist above. + +#### 5. Final candidate and signed release + +- [ ] Refresh and review the UBI Minimal and Micro manifest-list digests together. Confirm both architectures resolve, rebuild from scratch, and retain the old/new digest and vulnerability comparison. +- [ ] Confirm the selected ClickHouse release/channel and archive checksums, run both native CI jobs, and complete the current unfixed-finding triage with owner, rationale, compensating controls, and review expiry. +- [ ] Inspect each architecture's complete SPDX inventory and embedded license files, then complete the redistribution/trademark review. +- [ ] Rehearse the tag workflow without presenting a failed candidate as a supported release. Verify manifest variants, scans, keyless signature, SPDX attestation, provenance, downloaded evidence, and release assets. +- [ ] Review every README and operator procedure from a clean clone, including rootless storage, TLS, OpenShift, production, backup/restore, upgrade, rollback, and offline transfer instructions. +- [ ] Define the first release's support boundary. Workload-specific capacity numbers remain an operator responsibility unless the project publishes a named reference workload; do not imply universal production sizing. +- [ ] Complete the changelog, obtain second-maintainer review, merge without bypassing checks, create the annotated immutable tag, watch the workflow, verify the published digest on both architectures, and announce the GHCR release with known limitations. + ### Image behavior and compatibility - [ ] Run the complete smoke suite on the final ClickHouse and UBI digests. +- [ ] Run the complete smoke suite with the supported Podman baseline and record both client and server versions; retain the native Docker-based GitHub Actions results as separate runtime evidence. - [ ] Validate native `linux/amd64` and `linux/arm64` images, not only an emulated multi-platform build. - [ ] Exercise the image on an OpenShift 4 cluster with an arbitrary UID, restricted security context constraints, a read-only root filesystem, and a persistent volume. - [ ] Verify first-start initialization, password files, mounted configuration, restart persistence, graceful shutdown, and backup/restore instructions against the release candidate. @@ -62,7 +163,7 @@ This roadmap is the release gate for the first supported image. A checked item m ## After the first release - [ ] Automate a recurring rebuild policy for unchanged ClickHouse versions when UBI security updates arrive. -- [ ] Add an end-to-end OpenShift test when a safe test cluster and short-lived credentials are available. +- [ ] Automate a recurring end-to-end OpenShift test with short-lived credentials after the manual first-release qualification establishes a safe baseline. - [ ] Evaluate reproducible-build variance across GitHub-hosted runners. - [ ] Add package-consumer and upgrade tests for each supported ClickHouse update path. - [ ] Review artifact retention after real usage and adjust only with a documented storage/forensics rationale. diff --git a/docs/ROOTLESS.md b/docs/ROOTLESS.md new file mode 100644 index 0000000..3f4a2e8 --- /dev/null +++ b/docs/ROOTLESS.md @@ -0,0 +1,191 @@ +# Rootless storage and permissions + +The image starts ClickHouse directly as UID `101`, group `0`, and does not contain a root phase that changes mounted-file ownership. A runtime may assign another non-root UID, as OpenShift commonly does, provided that identity can traverse and write every configured local directory. + +This model prevents startup scripts from recursively changing production data ownership and works naturally with restricted security policies. It also means the operator must provision storage correctly before starting the server. The entrypoint reports the first required path it cannot create or write together with its effective UID and GID. + +## Official-image compatibility and the removed variable + +`CLICKHOUSE_DATA_DIR` was an unreleased interface from this Datopsis image; it is not an environment variable provided by the official ClickHouse image. Removing it makes the two images more consistent: both now read the primary data location from the effective ClickHouse `` configuration, whose upstream default is `/var/lib/clickhouse/`. + +The important difference is directory ownership. The official image normally starts its entrypoint as root, discovers configured paths, creates or changes their ownership, and then launches ClickHouse as its runtime user. This image starts non-root, discovers the same classes of local path, creates permitted subdirectories, and fails with preparation instructions when the mount itself is not writable. It never repairs ownership. + +The equivalent of choosing a data location for a particular `podman run` is to mount a ClickHouse configuration fragment and the corresponding volume in that command. Create `storage.xml` in the current directory: + +```xml + + + /data/clickhouse/ + +``` + +For rootless Podman on Linux, prepare the host directory within Podman's user namespace, then start the container: + +```console +mkdir -p ./clickhouse-storage/data +podman unshare chown 101:0 ./clickhouse-storage/data +podman unshare chmod 0770 ./clickhouse-storage/data +podman run --detach --name clickhouse \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume ./storage.xml:/etc/clickhouse-server/config.d/storage.xml:ro,Z \ + --volume ./clickhouse-storage/data:/data/clickhouse:Z \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +This remains runtime-selectable container configuration: no derived image is required. The difference is that the value is expressed in ClickHouse XML rather than an environment variable, preventing the entrypoint and server from using conflicting locations. Deployment automation may render the XML or ConfigMap before creating the container, but the resulting `` and mounted volume must agree. + +This short example moves only the primary data path. The [complete custom-path procedure](#custom-primary-data-path) also relocates temporary data, user files, and format schemas. Additional ClickHouse disks require one configured ``, one writable mount, and the same host-side permission preparation per disk; see [Additional local disks](#additional-local-disks). + +| Behavior | Official image | Datopsis UBI image | +| --- | --- | --- | +| Default path | `/var/lib/clickhouse/` | `/var/lib/clickhouse/` | +| Custom path source | Effective ClickHouse configuration | Effective ClickHouse configuration | +| Environment variable named `CLICKHOUSE_DATA_DIR` | No | No | +| Default entrypoint identity | Root, then drops privileges | Non-root throughout | +| Can recursively repair volume ownership | Yes, in its root startup mode | No | +| Custom path can be selected without rebuilding | Yes, with mounted configuration | Yes, with mounted configuration | + +## Default named volume + +The image declares `/var/lib/clickhouse` and creates it as `101:0` with group permissions matching the owner. A new Podman named volume therefore works without a manual ownership-changing startup step: + +```console +podman volume create clickhouse-data +podman run --detach --name clickhouse \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume clickhouse-data:/var/lib/clickhouse \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +The `/tmp` mount is required with a read-only root filesystem. The entrypoint writes its generated users configuration under `/tmp/clickhouse-entrypoint`; ClickHouse table data remains on the persistent configured data path. + +## Linux bind mount + +Rootless Podman maps container IDs through the invoking user's subordinate UID/GID ranges. Prepare a directory through `podman unshare`; do not use a literal host owner of `101:0`, which is correct only for rootful Podman without user-namespace remapping: + +```console +mkdir -p ./clickhouse-data +podman unshare chown 101:0 ./clickhouse-data +podman unshare chmod 0770 ./clickhouse-data +podman run --detach --name clickhouse \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume ./clickhouse-data:/var/lib/clickhouse:Z \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +The `:Z` option gives a private SELinux label. Coordinate labeling with the host administrator when the same content must be shared; do not disable SELinux to work around a denial. Podman's `:U` option can perform the mapped recursive ownership change automatically, but it modifies the host tree and can delay startup, so this guide uses the explicit `podman unshare` preparation instead. + +For rootful Podman without user-namespace remapping, prepare a system path with `sudo install -d -m 0770 -o 101 -g 0 /srv/clickhouse/data` and mount that path. Do not mix the rootless and rootful ownership procedures. + +If organizational policy assigns a different UID while retaining writable group `0`, prepare shared content according to the OpenShift-compatible image convention: + +```console +sudo chgrp -R 0 /srv/clickhouse/data +sudo chmod -R g=u /srv/clickhouse/data +``` + +Avoid `chmod 0777`. It grants write access to identities outside the intended container security context. + +## Kubernetes and OpenShift volumes + +For a Kubernetes deployment with the image's fixed identity, start with this pod-level security context and verify that the selected CSI driver honors it: + +```yaml +securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 0 + fsGroup: 0 + fsGroupChangePolicy: OnRootMismatch +``` + +The container-level policy should additionally set `allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true`, `capabilities.drop: ["ALL"]`, and `seccompProfile.type: RuntimeDefault`. Mount an `emptyDir` at `/tmp` and the PVC at the effective ClickHouse data path. + +Under OpenShift's restricted SCC, omit a fixed `runAsUser` when the project must use its assigned UID range. Keep the image directories group-0 writable, inspect the actual identity with `oc exec -- id`, and confirm the storage class makes the PVC writable to that pod. Do not request `anyuid`, privileged mode, or root solely to repair storage. + +`fsGroup` behavior, recursive ownership changes, and mount startup time vary by CSI driver. NFS with root squash can prevent both kubelet and administrators acting as root from changing ownership. In that case, provision the export with the required UID/GID or ACL on the storage server; the container cannot repair it. + +## Custom primary data path + +ClickHouse configuration is the only source of truth for storage. `CLICKHOUSE_DATA_DIR` is intentionally unsupported because changing an environment variable alone does not change ClickHouse's `` setting. + +Create a configuration fragment such as `storage.xml`: + +```xml + + + /data/clickhouse/ + /data/clickhouse-tmp/ + /data/user-files/ + /data/format-schemas/ + +``` + +For rootless Podman, provision every top-level mount inside its user namespace and mount the fragment read-only: + +```console +mkdir -p ./clickhouse-storage/data ./clickhouse-storage/tmp \ + ./clickhouse-storage/user-files ./clickhouse-storage/format-schemas +podman unshare chown -R 101:0 ./clickhouse-storage +podman unshare chmod -R 0770 ./clickhouse-storage + +podman run --detach --name clickhouse \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume ./storage.xml:/etc/clickhouse-server/config.d/storage.xml:ro,Z \ + --volume ./clickhouse-storage/data:/data/clickhouse:Z \ + --volume ./clickhouse-storage/tmp:/data/clickhouse-tmp:Z \ + --volume ./clickhouse-storage/user-files:/data/user-files:Z \ + --volume ./clickhouse-storage/format-schemas:/data/format-schemas:Z \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +At startup the entrypoint reads the effective `path`, `tmp_path`, `user_files_path`, and `format_schema_path`. Relative auxiliary paths are resolved below the primary data path. The primary `` must be absolute so its persistent-storage boundary is unambiguous. + +## Additional local disks + +An additional local disk needs both a ClickHouse configuration entry and a writable mount: + +```xml + + + + + + /data/archive/ + + + + +``` + +Prepare the archive directory using the same rootless `podman unshare` or rootful UID/GID procedure, then mount it at `/data/archive`. The entrypoint discovers every configured disk `path` and `metadata_path`, creates missing subdirectories where permitted, and fails before launching the server when a required local location is not writable. Object-storage credentials and remote endpoints have their own configuration and are not made valid by local directory preparation. + +## Troubleshooting + +For an error such as: + +```text +Required ClickHouse directory is not writable: /data/archive +Container identity: uid=100123 gid=0 +``` + +check the container identity, mount flags, ownership, mode, ACL, SELinux label, and storage backend: + +```console +podman inspect clickhouse --format '{{.Config.User}} {{json .Mounts}}' +podman run --rm --entrypoint id \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +namei -l ./clickhouse-data +getfacl ./clickhouse-data +ls -ldZ ./clickhouse-data +``` + +For Kubernetes or OpenShift, also inspect `id`, the pod security context, PVC events, and the mounted directory from a diagnostic pod using the same security context. Correct the storage provisioning or workload identity outside the running ClickHouse container. Do not solve the problem by starting the database as root, adding broad capabilities, disabling SELinux, or making the volume world-writable. diff --git a/docs/TLS.md b/docs/TLS.md new file mode 100644 index 0000000..e8d7254 --- /dev/null +++ b/docs/TLS.md @@ -0,0 +1,189 @@ +# TLS certificates and trust + +TLS has two independent directions in this image: + +- **Ingress TLS** encrypts client connections to ClickHouse. Terminate TLS at a trusted ingress/load balancer, or enable ClickHouse's HTTPS (`8443`) and secure native (`9440`) listeners. +- **Egress TLS trust** lets ClickHouse validate servers that it calls, such as S3-compatible storage, HTTPS dictionaries, URL engines, or remote ClickHouse nodes. Public Internet endpoints normally need no change because the image includes Red Hat's public CA bundle. Private CAs and TLS-inspection proxies require an additional trust bundle. + +Do not use a server certificate as an outbound trust anchor, disable certificate verification, put a private key in an image layer, or store it in Git. + +## Recommended production boundary + +Prefer TLS termination at the platform ingress, load balancer, or service mesh when it supports every protocol in use and the network from that proxy to ClickHouse is trusted. This centralizes certificate issuance and rotation. HTTP ingress products commonly cover HTTPS only; the ClickHouse native protocol needs a TCP-capable load balancer, TLS passthrough, or ClickHouse's `tcp_port_secure` listener. + +Use ClickHouse termination when end-to-end encryption is required, no suitable TCP proxy exists, or policy requires the application to own its key. The image contains the TLS implementation needed by ClickHouse but intentionally does not generate keys. Generate keys outside the container and mount them read-only. + +## Issue an ingress certificate + +Use the organization's CA in production. The certificate's Subject Alternative Name must contain every DNS name or IP address clients use. Generate the private key where the container will be deployed when possible, then send only the CSR to the CA: + +```console +umask 077 +openssl req -new -newkey rsa:3072 -nodes \ + -keyout tls.key -out clickhouse.csr \ + -subj '/CN=clickhouse.example.internal' \ + -addext 'subjectAltName=DNS:clickhouse.example.internal,DNS:clickhouse' +``` + +Have the CA return `tls.crt` containing the leaf certificate followed by any intermediate certificates. Keep the root CA separate for clients. Verify the result before deployment: + +```console +openssl req -in clickhouse.csr -noout -verify +openssl x509 -in tls.crt -noout -subject -issuer -dates -ext subjectAltName +openssl verify -CAfile organization-ca-bundle.pem tls.crt +``` + +For a disposable local test only, a self-signed certificate can be made without a CA: + +```console +umask 077 +openssl req -x509 -newkey rsa:3072 -sha256 -nodes -days 30 \ + -keyout tls.key -out tls.crt \ + -subj '/CN=localhost' \ + -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' +``` + +## Enable direct TLS with Podman + +Copy [`container/config.d/tls.example.xml`](../container/config.d/tls.example.xml) to `tls.xml` beside the certificate and key. It contains: + +```xml + + + 8443 + 9440 + + + + + + + + /etc/clickhouse-server/certs/tls.crt + /etc/clickhouse-server/certs/tls.key + none + true + true + sslv2,sslv3,tlsv1,tlsv1_1 + true + + + +``` + +`verificationMode` controls whether the server requires client certificates; `none` still provides server-authenticated TLS. Use a reviewed mutual-TLS configuration if client-certificate authentication is required. + +The entrypoint detects that the clear-text native port was removed and uses `tcp_port_secure` for initialization and its local health check. Certificate validation is skipped only for that loopback-only internal client, where the server certificate commonly does not identify `127.0.0.1`; external clients must validate the certificate and hostname normally. + +For rootless Podman on Linux, map the files to the image identity inside Podman's user namespace and keep the private key unreadable to other container users: + +```console +chmod 0444 tls.crt tls.xml +chmod 0400 tls.key +podman unshare chown 101:0 tls.key +``` + +The key may display subordinate host IDs afterward; `podman unshare ls -l tls.key` shows its container-visible ownership. For rootful Podman, use `sudo chown 101:0 tls.key` instead. Do not make the private key world-readable to bypass a mapping problem. + +Run the image with separate read-only mounts. Add `:Z` to bind mounts on SELinux hosts: + +```console +podman run --detach --name clickhouse \ + --publish 18443:8443 --publish 19440:9440 \ + --env CLICKHOUSE_PASSWORD='replace-me' \ + --read-only --tmpfs /tmp:size=256m,mode=1777 \ + --cap-drop ALL --security-opt no-new-privileges \ + --volume clickhouse-data:/var/lib/clickhouse \ + --volume ./tls.xml:/etc/clickhouse-server/config.d/tls.xml:ro,Z \ + --volume ./tls.crt:/etc/clickhouse-server/certs/tls.crt:ro,Z \ + --volume ./tls.key:/etc/clickhouse-server/certs/tls.key:ro,Z \ + ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +``` + +`EXPOSE` is image metadata, not a firewall; publishing `8443` and `9440` works even though the image metadata lists the upstream defaults. The example removes ports `8123` and `9000`. Do not publish those clear-text ports when TLS is mandatory. Port `9009` is inter-server HTTP and must remain private; clustered deployments should separately configure `interserver_https_port` and credentials. + +Validate both protocols from a client that trusts the issuing CA: + +```console +curl --fail --cacert organization-ca-bundle.pem \ + 'https://clickhouse.example.internal:18443/ping' + +clickhouse-client --secure --host clickhouse.example.internal --port 19440 \ + --user default --password 'replace-me' \ + --config-file ./client-config.xml --query 'SELECT 1' +``` + +Configure the client's CA according to that client or driver; never use an `insecure` or `skip verification` option as the production solution. + +## Kubernetes and OpenShift secret mount + +Create the Secret locally without putting key material in YAML or shell history, then apply it through the approved cluster-management channel: + +```console +kubectl create secret tls clickhouse-ingress-tls \ + --cert=tls.crt --key=tls.key \ + --dry-run=client -o yaml | kubectl apply -f - +kubectl create configmap clickhouse-tls-config \ + --from-file=tls.xml \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Mount the Secret at `/etc/clickhouse-server/certs` and the ConfigMap file at `/etc/clickhouse-server/config.d/tls.xml`. Use `readOnly: true` and `defaultMode: 0440` for the Secret. The baseline image runs as `101:0`, and OpenShift-compatible arbitrary UIDs normally receive group `0`; confirm the platform security context can read the projected files. A platform ingress Secret is mounted into the ingress controller, not this container. + +Restart or perform a controlled rollout after rotation, and test both the new certificate and the old certificate's removal. Automate renewal alerts before expiry. Do not depend on public ACME in a disconnected environment. + +## Outbound trust + +No extra setup is needed for normally trusted public Internet services. The image ships `/etc/pki/tls/certs/ca-bundle.crt`. Egress should still be allow-listed with a network policy or firewall. + +For a private CA, construct one complete PEM bundle outside the container: + +- Internet-connected workload: public CA roots plus the organization's root and required intermediate CAs. +- Fully isolated workload: only the internal roots and intermediates that policy authorizes. + +Mount that bundle read-only at `/etc/clickhouse-server/certs/outbound-ca-bundle.pem` and copy [`container/config.d/outbound-ca.example.xml`](../container/config.d/outbound-ca.example.xml) to `/etc/clickhouse-server/config.d/outbound-ca.xml`: + +```xml + + + + + false + /etc/clickhouse-server/certs/outbound-ca-bundle.pem + true + sslv2,sslv3,tlsv1,tlsv1_1 + strict + + RejectCertificateHandler + + + + +``` + +This configures the ClickHouse/Poco TLS client with strict validation. Individual integrations can have a separate CA-file setting; point each one at the same bundle and verify it in a staging query. Do not assume one successful HTTPS dictionary test proves S3, Kafka, LDAP, and inter-server TLS all use the same client stack. + +An alternative is a derived image with the private CA added to the RHEL trust store: + +```dockerfile +FROM ghcr.io/datopsis/clickhouse-server-ubi9@sha256: +USER 0 +COPY organization-root-ca.pem /etc/pki/ca-trust/source/anchors/ +RUN update-ca-trust +USER 101:0 +``` + +Build this only from a reviewed context, keep the CA certificate (never a private CA key) in the context, scan the derived image, and sign it under the deploying organization's identity. Rebuild when either the upstream image or CA set changes. + +## Disconnected deployment + +An offline network changes delivery, not TLS fundamentals: + +1. On a connected staging system, pull the image by digest, verify its signature and release evidence, export it as an OCI or Docker archive, and scan it with vulnerability databases approved for transfer. +2. Transfer the image archive, SBOM, signature bundle, scanner database snapshot, and public verification material through the organization's controlled media process. Record hashes on both sides. Import the image into the disconnected registry; deploy by its internal digest. +3. Generate the server private key inside the disconnected security boundary. Send a CSR to the offline/internal CA and return the signed leaf plus chain. Never move the CA private key to the workload and avoid moving the server private key at all. +4. Distribute the internal root CA to every client and create the ingress Secret or read-only mounts described above. Configure an internal certificate-renewal owner and calendar; public ACME and public revocation endpoints will not be reachable. +5. Build the outbound bundle from only the internal trust anchors needed in that network. Test every allowed destination by DNS name, confirm time synchronization, and deny all other egress. +6. Rehearse renewal, revocation, image update, backup restoration, and rollback without Internet access before production approval. + +The [ClickHouse TLS guide](https://clickhouse.com/docs/guides/sre/tls/configuring-tls) documents secure HTTP, native, inter-server, and Keeper channels. Red Hat documents the [RHEL 9 shared trust store](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/securing_networks/using-shared-system-certificates_securing-networks). diff --git a/docs/VULNERABILITY-MANAGEMENT.md b/docs/VULNERABILITY-MANAGEMENT.md new file mode 100644 index 0000000..9070c90 --- /dev/null +++ b/docs/VULNERABILITY-MANAGEMENT.md @@ -0,0 +1,45 @@ +# Vulnerability management + +The CI and release gates block fixed High and Critical vulnerabilities. Findings without a vendor fix are not silently treated as resolved: they are reviewed before a release and refreshed weekly, but they cannot be patched by rebuilding the same inputs. + +Every CI and release scan now retains two distinct Grype results: `grype.sarif` contains the fixed High/Critical blocking scope, while `grype-all.json` inventories every matched severity regardless of fix availability. The latter is non-blocking so an upstream-unfixed match does not make builds permanently unusable; it is mandatory review evidence, not an automatic acceptance. + +Syft discovers the UBI RPM inventory directly. Because ClickHouse is installed from upstream TGZ archives rather than an RPM or Debian package, `scripts/augment-spdx.py` adds the three exact pinned ClickHouse components to the SPDX document before either Grype scan. The ClickHouse common-static record includes a CPE so vulnerability matching covers the application as well as the operating-system packages. Review this declared metadata whenever the archive layout or packaging method changes. + +## The current 24 unfixed matches + +On 2026-09-06, Grype 0.118.0 reported 24 package-to-CVE matches in the local `linux/amd64` image for ClickHouse `26.8.2.7` on UBI `9.8`. They represent 12 unique CVEs; a CVE appears more than once when several installed RPM records share the affected source package. + +| RPM records | CVEs | Matches | Grype severity | Current disposition | +| --- | --- | ---: | --- | --- | +| `glibc`, `glibc-common`, `glibc-minimal-langpack` | CVE-2026-6791, CVE-2026-18374, CVE-2026-6368, CVE-2026-19542, CVE-2026-80489 | 15 | Medium | No fix version reported; required runtime libraries. Track Red Hat and refresh the UBI digest. | +| `coreutils-single` | CVE-2026-56391, CVE-2026-56392 | 2 | Medium | No fix version reported; entrypoint uses core utilities. Track Red Hat and refresh the UBI digest. | +| `sed` | CVE-2026-5958 | 1 | Medium | No fix version reported; inherited through the UBI runtime set. Track Red Hat and refresh the UBI digest. | +| `pcre2`, `pcre2-syntax` | CVE-2022-41409 | 2 | Low | No fix version reported; transitive runtime dependency. Track Red Hat. | +| `ncurses-base`, `ncurses-libs` | CVE-2023-50495 | 2 | Low | No fix version reported; Bash needs `libtinfo` from ncurses. Track Red Hat. | +| `libgcc` | CVE-2022-27943, CVE-2021-46195 | 2 | Low | Red Hat describes flaws in `libiberty`; for CVE-2022-27943 Red Hat explicitly says `libgcc` does not contain the affected code. Treat as a likely package-name mapping false positive and re-check both records after database updates. | + +There were no High or Critical matches in this reproduction. “24 matches” does not mean 24 independently exploitable defects, and “unfixed” does not mean safe. It means Grype has no fixed version to recommend for the matched distro package. + +## What can actually fix a match + +1. Refresh the pinned UBI Micro and Minimal digests together, rebuild, and scan the resulting image. This is the normal remediation when Red Hat publishes updated RPMs. +2. Update ClickHouse when a finding belongs to a ClickHouse archive rather than UBI. +3. Remove a package only after proving it is not a direct, file-level, or runtime dependency and rerunning the complete smoke suite on both architectures. Do not break RPM dependency metadata or copy individual shared libraries merely to reduce a scanner count. +4. Correct or suppress a false positive only with vendor evidence, exact package/image scope, an owner, rationale, review date, and expiry. Prefer reporting the match upstream to a permanent local ignore. +5. If a material remotely exploitable issue has no fix, reduce exposure, disable the affected feature, or delay the release. Severity, reachability, and deployment controls—not the raw count—drive that decision. + +Replacing UBI with another distribution solely to clear a point-in-time scan is not a durable fix. It changes the project's purpose and substitutes a different update stream and vulnerability database. + +## Release triage record + +For every release candidate, retain or link: + +- image digest and architecture; +- scanner and vulnerability database versions/timestamps; +- full SBOM and SARIF/table output; +- unique CVEs and package matches grouped separately; +- vendor status, exploitability, exposure, compensating controls, owner, and expiry for every accepted finding; +- the rescan after any base or ClickHouse update. + +The authoritative evidence is the release artifact and current vendor advisory. This page is a point-in-time explanation and must be updated when the package set or disposition changes. Red Hat explains why its package-specific assessment and backported fixes can differ from version-only scanner data in its [CVE guidance](https://access.redhat.com/security/security-updates/security-advisories). diff --git a/scripts/augment-spdx.py b/scripts/augment-spdx.py new file mode 100644 index 0000000..d87b2e2 --- /dev/null +++ b/scripts/augment-spdx.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Add ClickHouse TGZ components that Syft cannot infer from the image filesystem.""" + +from __future__ import annotations + +import argparse +import json +import re +import tempfile +from pathlib import Path +from typing import Any + + +CLICKHOUSE_PACKAGES = ( + "clickhouse-common-static", + "clickhouse-server", + "clickhouse-client", +) +BUILD_ARG_PATTERN = re.compile( + r'^ARG (?PCLICKHOUSE_(?:VERSION|CHANNEL))="(?P[^"]+)"$', + re.MULTILINE, +) + + +def read_clickhouse_build_args(containerfile: Path) -> tuple[str, str]: + values: dict[str, set[str]] = {} + for match in BUILD_ARG_PATTERN.finditer(containerfile.read_text(encoding="utf-8")): + values.setdefault(match.group("name"), set()).add(match.group("value")) + + required = ("CLICKHOUSE_VERSION", "CLICKHOUSE_CHANNEL") + missing = [name for name in required if name not in values] + if missing: + raise ValueError(f"{', '.join(missing)} is missing from {containerfile}") + inconsistent = [name for name in required if len(values[name]) != 1] + if inconsistent: + raise ValueError(f"{', '.join(inconsistent)} is inconsistent in {containerfile}") + return values["CLICKHOUSE_VERSION"].pop(), values["CLICKHOUSE_CHANNEL"].pop() + + +def augment(document: dict[str, Any], version: str, channel: str) -> dict[str, Any]: + if document.get("spdxVersion") != "SPDX-2.3": + raise ValueError("expected an SPDX 2.3 document") + + packages = document.get("packages") + relationships = document.get("relationships") + creation_info = document.get("creationInfo") + if not isinstance(packages, list) or not isinstance(relationships, list): + raise ValueError("SPDX document must contain package and relationship lists") + if not isinstance(creation_info, dict): + raise ValueError("SPDX document must contain creationInfo") + + package_names = {package.get("name") for package in packages} + already_present = package_names.intersection(CLICKHOUSE_PACKAGES) + if already_present: + names = ", ".join(sorted(already_present)) + raise ValueError(f"refusing to duplicate existing ClickHouse packages: {names}") + + container_roots = [ + package + for package in packages + if package.get("primaryPackagePurpose") == "CONTAINER" + ] + if len(container_roots) != 1: + raise ValueError("expected exactly one SPDX package with CONTAINER purpose") + root_id = container_roots[0].get("SPDXID") + if not isinstance(root_id, str) or not root_id.startswith("SPDXRef-"): + raise ValueError("container package has no valid SPDXID") + + for name in CLICKHOUSE_PACKAGES: + package_id = f"SPDXRef-Package-generic-{name}" + external_refs = [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": f"pkg:generic/clickhouse/{name}@{version}", + } + ] + if name == "clickhouse-common-static": + external_refs.append( + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": ( + f"cpe:2.3:a:clickhouse:clickhouse:{version}:*:*:*:*:*:*:*" + ), + } + ) + + packages.append( + { + "name": name, + "SPDXID": package_id, + "versionInfo": version, + "supplier": "Organization: ClickHouse, Inc.", + "downloadLocation": f"https://packages.clickhouse.com/tgz/{channel}/", + "filesAnalyzed": False, + "sourceInfo": ( + "Declared from the pinned Containerfile build input; the upstream " + "TGZ and published SHA-512 are verified during the image build." + ), + "licenseConcluded": "Apache-2.0", + "licenseDeclared": "Apache-2.0", + "copyrightText": "NOASSERTION", + "externalRefs": external_refs, + "primaryPackagePurpose": "APPLICATION", + } + ) + relationships.append( + { + "spdxElementId": root_id, + "relatedSpdxElement": package_id, + "relationshipType": "CONTAINS", + } + ) + + creators = creation_info.setdefault("creators", []) + creator = "Tool: datopsis-spdx-augment" + if creator not in creators: + creators.append(creator) + return document + + +def write_json_atomic(path: Path, document: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + "w", encoding="utf-8", dir=path.parent, delete=False + ) as output: + json.dump(document, output, indent=2) + output.write("\n") + temporary_path = Path(output.name) + temporary_path.replace(path) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--input", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--containerfile", default=Path("Containerfile"), type=Path) + args = parser.parse_args() + + version, channel = read_clickhouse_build_args(args.containerfile) + document = json.loads(args.input.read_text(encoding="utf-8")) + write_json_atomic(args.output, augment(document, version, channel)) + + +if __name__ == "__main__": + main() diff --git a/tests/config.d/custom-storage.xml b/tests/config.d/custom-storage.xml new file mode 100644 index 0000000..6782d0c --- /dev/null +++ b/tests/config.d/custom-storage.xml @@ -0,0 +1,18 @@ + + + /var/lib/clickhouse/custom/ + custom-tmp/ + /var/lib/clickhouse/custom-user-files/ + /var/lib/clickhouse/custom-format-schemas/ + + /var/lib/clickhouse/custom-logs/server.log + /var/lib/clickhouse/custom-logs/error.log + + + + + /var/lib/clickhouse/additional/ + + + + diff --git a/tests/config.d/empty-primary.xml b/tests/config.d/empty-primary.xml new file mode 100644 index 0000000..ad99c89 --- /dev/null +++ b/tests/config.d/empty-primary.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/config.d/nonwritable-disk.xml b/tests/config.d/nonwritable-disk.xml new file mode 100644 index 0000000..39bbd1d --- /dev/null +++ b/tests/config.d/nonwritable-disk.xml @@ -0,0 +1,10 @@ + + + + + + /etc/clickhouse-disk/ + + + + diff --git a/tests/config.d/nonwritable-storage.xml b/tests/config.d/nonwritable-storage.xml new file mode 100644 index 0000000..cb475ca --- /dev/null +++ b/tests/config.d/nonwritable-storage.xml @@ -0,0 +1,4 @@ + + + /etc/clickhouse-data/ + diff --git a/tests/config.d/relative-primary.xml b/tests/config.d/relative-primary.xml new file mode 100644 index 0000000..98dd111 --- /dev/null +++ b/tests/config.d/relative-primary.xml @@ -0,0 +1,4 @@ + + + relative-data/ + diff --git a/tests/entrypoint-paths.sh b/tests/entrypoint-paths.sh new file mode 100644 index 0000000..790f596 --- /dev/null +++ b/tests/entrypoint-paths.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +test_root="$(mktemp -d)" + +cleanup() { + rm -rf "${test_root}" +} +trap cleanup EXIT + +# shellcheck source=container/entrypoint.sh +source "${repo_root}/container/entrypoint.sh" + +# Isolate directory preparation from ClickHouse itself so every supported key, +# including object-storage metadata paths, can be checked deterministically. +extract_config_values() { + case "$1" in + path) printf '%s/\n' "${test_root}/data" ;; + tmp_path) printf '%s\n' 'relative-tmp/' ;; + user_files_path) printf '%s/\n' "${test_root}/user-files" ;; + format_schema_path) printf '%s/\n' "${test_root}/format-schemas" ;; + 'storage_configuration.disks.*.path') + printf '%s\n' 'relative-disk/' + ;; + 'storage_configuration.disks.*.metadata_path') + printf '%s/\n' "${test_root}/disk-metadata" + ;; + logger.log) printf '%s\n' '/dev/stdout' ;; + logger.errorlog) printf '%s\n' 'logs/error.log' ;; + esac +} + +prepare_configured_directories + +for expected in \ + "${test_root}/data" \ + "${test_root}/data/relative-tmp" \ + "${test_root}/user-files" \ + "${test_root}/format-schemas" \ + "${test_root}/data/relative-disk" \ + "${test_root}/disk-metadata" \ + "${test_root}/data/logs"; do + test -d "${expected}" +done + +echo "Entrypoint configured-path tests passed" diff --git a/tests/smoke.sh b/tests/smoke.sh index 5f307fe..54073a2 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -Eeuo pipefail -runtime="${CONTAINER_RUNTIME:-docker}" +runtime="${CONTAINER_RUNTIME:-podman}" image="${IMAGE:-ghcr.io/datopsis/clickhouse-server-ubi9:test}" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" run_id="${RANDOM}-$$" @@ -9,6 +9,8 @@ prefix="clickhouse-ubi9-smoke-${run_id}" network="${prefix}-network" data_volume="${prefix}-data" arbitrary_volume="${prefix}-arbitrary-data" +tls_volume="${prefix}-tls-data" +custom_volume="${prefix}-custom-data" secret_dir="$(mktemp -d "${repo_root}/.smoke-secrets.XXXXXX")" password="smoke-test-password" @@ -17,13 +19,26 @@ restart="${prefix}-restart" passwordless="${prefix}-passwordless" password_file_server="${prefix}-password-file" arbitrary_uid="${prefix}-arbitrary-uid" +tls_server="${prefix}-tls" +custom_server="${prefix}-custom" +custom_restart="${prefix}-custom-restart" +legacy_data_dir="${prefix}-legacy-data-dir" +nonwritable="${prefix}-nonwritable" +nonwritable_disk="${prefix}-nonwritable-disk" +relative_primary="${prefix}-relative-primary" +empty_primary="${prefix}-empty-primary" cleanup() { "${runtime}" rm -f \ "${primary}" "${restart}" "${passwordless}" \ - "${password_file_server}" "${arbitrary_uid}" >/dev/null 2>&1 || true + "${password_file_server}" "${arbitrary_uid}" \ + "${tls_server}" "${custom_server}" "${custom_restart}" \ + "${legacy_data_dir}" "${nonwritable}" "${nonwritable_disk}" \ + "${relative_primary}" "${empty_primary}" >/dev/null 2>&1 || true "${runtime}" network rm "${network}" >/dev/null 2>&1 || true - "${runtime}" volume rm "${data_volume}" "${arbitrary_volume}" >/dev/null 2>&1 || true + "${runtime}" volume rm \ + "${data_volume}" "${arbitrary_volume}" "${tls_volume}" \ + "${custom_volume}" >/dev/null 2>&1 || true rm -rf "${secret_dir}" } trap cleanup EXIT @@ -65,6 +80,25 @@ wait_healthy() { return 1 } +wait_failed_with() { + local server_name=$1 + local expected_message=$2 + local running + + for _ in {1..30}; do + running="$("${runtime}" inspect --format '{{.State.Running}}' "${server_name}")" + if [[ "${running}" == false ]]; then + "${runtime}" logs "${server_name}" 2>&1 | grep -Fq "${expected_message}" + return + fi + sleep 1 + done + + "${runtime}" logs "${server_name}" + echo "Expected ${server_name} to fail startup" >&2 + return 1 +} + query_server() { local server_name=$1 local server_password=$2 @@ -89,6 +123,8 @@ query_remote() { "${runtime}" network create "${network}" >/dev/null "${runtime}" volume create "${data_volume}" >/dev/null "${runtime}" volume create "${arbitrary_volume}" >/dev/null +"${runtime}" volume create "${tls_volume}" >/dev/null +"${runtime}" volume create "${custom_volume}" >/dev/null run_server "${primary}" \ --network-alias primary \ @@ -101,7 +137,14 @@ actual_version="$(query_server "${primary}" "${password}" 'SELECT version()')" expected_version="$("${runtime}" inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}}' "${image}")" test "${actual_version}" = "${expected_version}" test "$("${runtime}" inspect --format '{{.Config.User}}' "${image}")" = "101:0" +if "${runtime}" inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "${image}" | \ + grep -q '^CLICKHOUSE_DATA_DIR='; then + echo "Image metadata unexpectedly contains CLICKHOUSE_DATA_DIR" >&2 + exit 1 +fi test "$("${runtime}" exec "${primary}" id -u)" = 101 +test "$("${runtime}" exec "${primary}" stat -c '%a' /tmp/clickhouse-entrypoint/users.xml)" = 600 +"${runtime}" exec "${primary}" test ! -e /var/lib/clickhouse/generated/users.xml "${runtime}" exec "${primary}" sh -c \ '! command -v microdnf && ! command -v dnf && ! command -v yum && ! command -v rpm && ! command -v curl && ! command -v wget' query_server "${primary}" "${password}" 'SELECT 1' | grep -qx 1 @@ -155,4 +198,80 @@ wait_healthy "${arbitrary_uid}" test "$("${runtime}" exec "${arbitrary_uid}" id -u)" = 100123 query_server "${arbitrary_uid}" "${password}" 'SELECT 1' | grep -qx 1 +# Storage paths come from the effective ClickHouse configuration. The +# entrypoint creates configured subdirectories as the current non-root user. +run_server "${custom_server}" \ + --user 100124:0 \ + --env CLICKHOUSE_PASSWORD="${password}" \ + --volume "${custom_volume}:/var/lib/clickhouse" \ + --volume "${repo_root}/tests/config.d/custom-storage.xml:/etc/clickhouse-server/config.d/custom-storage.xml:ro" \ + --volume "${repo_root}/tests/fixtures:/docker-entrypoint-initdb.d:ro" +wait_healthy "${custom_server}" +test "$("${runtime}" exec "${custom_server}" id -u)" = 100124 +query_server "${custom_server}" "${password}" \ + "SELECT path FROM system.disks WHERE name = 'default'" | grep -qx '/var/lib/clickhouse/custom/' +query_server "${custom_server}" "${password}" \ + 'SELECT value FROM default.container_smoke_test' | grep -qx initialized +"${runtime}" exec "${custom_server}" test -d /var/lib/clickhouse/custom/custom-tmp +"${runtime}" exec "${custom_server}" test -d /var/lib/clickhouse/custom-user-files +"${runtime}" exec "${custom_server}" test -d /var/lib/clickhouse/custom-format-schemas +"${runtime}" exec "${custom_server}" test -d /var/lib/clickhouse/additional +"${runtime}" exec "${custom_server}" test -d /var/lib/clickhouse/custom-logs +"${runtime}" rm -f "${custom_server}" >/dev/null + +# Initialization detection must follow the configured primary path on restart. +run_server "${custom_restart}" \ + --user 100124:0 \ + --env CLICKHOUSE_PASSWORD="${password}" \ + --volume "${custom_volume}:/var/lib/clickhouse" \ + --volume "${repo_root}/tests/config.d/custom-storage.xml:/etc/clickhouse-server/config.d/custom-storage.xml:ro" \ + --volume "${repo_root}/tests/fixtures:/docker-entrypoint-initdb.d:ro" +wait_healthy "${custom_restart}" +query_server "${custom_restart}" "${password}" \ + 'SELECT count() FROM default.container_smoke_test' | grep -qx 1 +"${runtime}" rm -f "${custom_restart}" >/dev/null + +# The removed legacy environment variable must fail instead of silently +# disagreeing with the ClickHouse configuration. +run_server "${legacy_data_dir}" --env CLICKHOUSE_DATA_DIR=/different-data +wait_failed_with "${legacy_data_dir}" 'CLICKHOUSE_DATA_DIR is not supported' + +# A configured path on the read-only root must fail early with operator-facing +# identity and permission guidance. +run_server "${nonwritable}" \ + --volume "${repo_root}/tests/config.d/nonwritable-storage.xml:/etc/clickhouse-server/config.d/nonwritable-storage.xml:ro" +wait_failed_with "${nonwritable}" 'Required ClickHouse directory is not writable: /etc/clickhouse-data' + +run_server "${nonwritable_disk}" \ + --volume "${repo_root}/tests/config.d/nonwritable-disk.xml:/etc/clickhouse-server/config.d/nonwritable-disk.xml:ro" +wait_failed_with "${nonwritable_disk}" 'Required ClickHouse directory is not writable: /etc/clickhouse-disk' + +run_server "${relative_primary}" \ + --volume "${repo_root}/tests/config.d/relative-primary.xml:/etc/clickhouse-server/config.d/relative-primary.xml:ro" +wait_failed_with "${relative_primary}" 'ClickHouse must resolve to a non-empty absolute directory' + +run_server "${empty_primary}" \ + --volume "${repo_root}/tests/config.d/empty-primary.xml:/etc/clickhouse-server/config.d/empty-primary.xml:ro" +wait_failed_with "${empty_primary}" 'ClickHouse must resolve to a non-empty absolute directory' + +# A TLS-only native listener must support initialization and the image health +# check without retaining the clear-text native port. +env -u MSYS_NO_PATHCONV MSYS2_ARG_CONV_EXCL='/CN=' \ + openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 1 \ + -keyout "${secret_dir}/tls.key" \ + -out "${secret_dir}/tls.crt" \ + -subj '/CN=localhost' \ + -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1' >/dev/null 2>&1 +chmod 0444 "${secret_dir}/tls.key" "${secret_dir}/tls.crt" +run_server "${tls_server}" \ + --env CLICKHOUSE_PASSWORD="${password}" \ + --volume "${tls_volume}:/var/lib/clickhouse" \ + --volume "${repo_root}/container/config.d/tls.example.xml:/etc/clickhouse-server/config.d/tls.xml:ro" \ + --volume "${secret_dir}/tls.crt:/etc/clickhouse-server/certs/tls.crt:ro" \ + --volume "${secret_dir}/tls.key:/etc/clickhouse-server/certs/tls.key:ro" +wait_healthy "${tls_server}" +"${runtime}" exec "${tls_server}" clickhouse-client \ + --secure --accept-invalid-certificate --host 127.0.0.1 --port 9440 \ + --user default --password "${password}" --query 'SELECT 1' | grep -qx 1 + echo "Smoke tests passed for ClickHouse ${actual_version}" diff --git a/tests/test_augment_spdx.py b/tests/test_augment_spdx.py new file mode 100644 index 0000000..58d1a4b --- /dev/null +++ b/tests/test_augment_spdx.py @@ -0,0 +1,65 @@ +import importlib.util +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).parents[1] / "scripts" / "augment-spdx.py" +SPEC = importlib.util.spec_from_file_location("augment_spdx", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +def base_document(): + return { + "spdxVersion": "SPDX-2.3", + "creationInfo": {"creators": ["Tool: syft"]}, + "packages": [ + { + "name": "example-image", + "SPDXID": "SPDXRef-Container", + "primaryPackagePurpose": "CONTAINER", + } + ], + "relationships": [], + } + + +class AugmentSpdxTests(unittest.TestCase): + def test_adds_clickhouse_packages_and_relationships(self): + result = MODULE.augment(base_document(), "26.8.2.7", "stable") + + packages = {package["name"]: package for package in result["packages"]} + for name in MODULE.CLICKHOUSE_PACKAGES: + self.assertEqual(packages[name]["versionInfo"], "26.8.2.7") + self.assertEqual(packages[name]["licenseDeclared"], "Apache-2.0") + self.assertEqual( + packages[name]["downloadLocation"], + "https://packages.clickhouse.com/tgz/stable/", + ) + + cpes = packages["clickhouse-common-static"]["externalRefs"] + self.assertTrue(any(ref["referenceType"] == "cpe23Type" for ref in cpes)) + self.assertEqual(len(result["relationships"]), 3) + self.assertIn( + "Tool: datopsis-spdx-augment", result["creationInfo"]["creators"] + ) + + def test_rejects_duplicate_clickhouse_package(self): + document = base_document() + document["packages"].append({"name": "clickhouse-client"}) + + with self.assertRaisesRegex(ValueError, "duplicate"): + MODULE.augment(document, "26.8.2.7", "stable") + + def test_reads_pinned_build_args(self): + self.assertEqual( + MODULE.read_clickhouse_build_args( + Path(__file__).parents[1] / "Containerfile" + ), + ("26.8.2.7", "stable"), + ) + + +if __name__ == "__main__": + unittest.main()