diff --git a/.github/renovate.json b/.github/renovate.json index 644f1a68b1..2817d6cb4a 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -119,7 +119,7 @@ }, { "customType": "regex", - "description": "Track the pinned Coroot CE component images (server, node-agent, cluster-agent) in the Coroot CR. The coroot-operator's `Coroot` CRD nests each image reference under `image.name` (an ImageSpec object), NOT a bare `image:` string, so Renovate's built-in `kubernetes` manager does not detect it — and when the field is left unset the operator auto-resolves the highest semver tag from ghcr on every reconcile (floating, untrackable; the source of the stale base-image CVEs in platform#2149). This regex manager captures each `name: ghcr.io/coroot/:` and tracks it via the docker datasource so the pins get the normal Renovate bump PRs (minor/patch automerge per the rules below).", + "description": "Track upstream Coroot component images when the Coroot CR consumes them directly. The operator nests each reference under image.name, so the kubernetes manager does not detect it; an unset reference floats on registry discovery. Signed compatibility builds remain pinned to their reviewed digests instead. Their upstream runtime bases are tracked by Renovate's built-in Dockerfile manager in CorootCheckpoint.Dockerfile and CorootClusterAgentCheckpoint.Dockerfile. Upstream refresh and compatibility retirement require coordinated source, patch, image and native API/browser verification; the undated-dependency guard follows these actual base-image inputs.", "managerFilePatterns": [ "/^k8s/bases/infrastructure/coroot/coroot\\.ya?ml$/" ], @@ -253,7 +253,7 @@ "automerge": false }, { - "description": "Six more ghcr.io dependencies get no release date from Renovate, the same cause as the flux-operator chart above: the Homepage and origin-ca-issuer charts (flux manager, read from their OCIRepository), the PostgreSQL operand image (the CloudNativePG custom manager), the Coroot server and cluster-agent images (the Coroot custom manager) and the Umami image (kubernetes manager, in its CronJob). Every one had newer releases, yet none was proposed after the repository-wide wait began on 2026-05-04 (#5008). minimumReleaseAge is therefore 0 days. Raising the PR adopts nothing: automerge is off for every ordinary update, whatever its type, because with no wait nothing soaks an update before it merges, and several of these are minor or major versions behind, so each update is read against its release notes before merge. A vulnerability-fix PR is the one exception and keeps the repository-wide vulnerabilityAlerts policy above (no wait, automerge): Renovate applies that block after package rules, and this rule deliberately leaves the security fast path unchanged. Placed after the broad automerge rules so none of them can turn automerge back on; scripts/tests/oci-chart-sources checks each dependency's manager and that every update of it resolves to both settings.", + "description": "Six more ghcr.io dependencies get no release date from Renovate, the same cause as the flux-operator chart above: the Homepage and origin-ca-issuer charts (flux manager, read from their OCIRepository), the PostgreSQL operand image (the CloudNativePG custom manager), the Coroot server and cluster-agent upstream runtime bases (Dockerfile manager while compatibility builds are used) and the Umami image (kubernetes manager, in its CronJob). Every one had newer releases, yet none was proposed after the repository-wide wait began on 2026-05-04 (#5008). minimumReleaseAge is therefore 0 days. Raising the PR adopts nothing: automerge is off for every ordinary update, whatever its type, because with no wait nothing soaks an update before it merges, and several of these are minor or major versions behind, so each update is read against its release notes before merge. A vulnerability-fix PR is the one exception and keeps the repository-wide vulnerabilityAlerts policy above (no wait, automerge): Renovate applies that block after package rules, and this rule deliberately leaves the security fast path unchanged. Placed after the broad automerge rules so none of them can turn automerge back on; scripts/tests/oci-chart-sources checks each dependency's manager and that every update of it resolves to both settings.", "matchPackageNames": [ "ghcr.io/m0nsterrr/helm-charts/homepage", "ghcr.io/cloudflare/origin-ca-issuer-charts/origin-ca-issuer", diff --git a/.github/workflows/validate-coroot-security-progress.yaml b/.github/workflows/validate-coroot-security-progress.yaml index dfe70b04d0..da2576901d 100644 --- a/.github/workflows/validate-coroot-security-progress.yaml +++ b/.github/workflows/validate-coroot-security-progress.yaml @@ -145,7 +145,7 @@ jobs: permissions: contents: read env: - COROOT_IMAGE: ghcr.io/coroot/coroot:1.28.1 + COROOT_IMAGE: ghcr.io/devantler-tech/platform-coroot:v1.28.1-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:9a2782f405f9ee673301912b3b2cf85a4a154fbfeb1542df2d4937d6600396f1 PROMETHEUS_IMAGE: ghcr.io/coroot/prometheus:2.55.1-ubi9-0@sha256:c2a4f92f728411b834b7abd07764cf587e28749c377b157443569dea4f81f833 SERVER_IMAGE: docker.io/library/busybox:1.38.0-musl@sha256:ea2b9914a16a4ac1981994af97b318f7c7d4db76b580c56177f08bf76f4a0be8 PLAYWRIGHT_VERSION: 1.56.1 @@ -163,9 +163,10 @@ jobs: - name: Verify the native API contract matches the versioned Coroot image shell: bash run: | - # Updating either pin requires reviewing the v1.28.1 API and browser - # source contract in scripts/tests/coroot-security-native/README.md. - if [ "$COROOT_IMAGE" != ghcr.io/coroot/coroot:1.28.1 ] || + # The checkpoint-only patch retains the reviewed v1.28.1 API/browser + # contract; this fixture exercises the exact proposed production image. + # Update the source audit and real startup-version assertion together. + if [ "$COROOT_IMAGE" != ghcr.io/devantler-tech/platform-coroot:v1.28.1-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:9a2782f405f9ee673301912b3b2cf85a4a154fbfeb1542df2d4937d6600396f1 ] || [ "$(grep -Fxc " name: $COROOT_IMAGE" k8s/bases/infrastructure/coroot/coroot.yaml)" != 1 ]; then echo 'Native Coroot contract/image differs from communityEdition.image.name; review compatibility before updating the fixture.' >&2 exit 1 diff --git a/k8s/bases/infrastructure/coroot/coroot.yaml b/k8s/bases/infrastructure/coroot/coroot.yaml index a39fb027af..b58c512f9a 100644 --- a/k8s/bases/infrastructure/coroot/coroot.yaml +++ b/k8s/bases/infrastructure/coroot/coroot.yaml @@ -57,15 +57,19 @@ spec: # container tags as BARE semver (`1.22.2`), NOT the `v`-prefixed GitHub # *release* tag (`v1.22.2`); a `v`-prefixed ref 404s on ghcr → ErrImagePull → # the operator's StatefulSet/DaemonSet rollout wedges and stalls the - # infrastructure Kustomization health check. Always pin the bare tag. The + # infrastructure Kustomization health check. Use bare tags for upstream + # releases; platform compatibility builds use their publisher's exact tag + # and immutable digest. The # three pins below are kept # Renovate-trackable by a dedicated regex customManager in # `.github/renovate.json` (the built-in kubernetes manager only reads a bare # `image:` string, not this `image.name` mapping). There is no spec.version # field; the operator HelmRelease itself also stays Flux/Renovate-pinned. communityEdition: + # Recognize actual checkpoint-record progress on idle archived databases; + # retain the existing warning when that evidence is missing or invalid. image: - name: ghcr.io/coroot/coroot:1.28.1 + name: ghcr.io/devantler-tech/platform-coroot:v1.28.1-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:9a2782f405f9ee673301912b3b2cf85a4a154fbfeb1542df2d4937d6600396f1 # CE has no native OIDC/SSO (Enterprise only), so the UI is fronted by the # existing oauth2-proxy (Dex) via http-route.yaml — exactly how Prometheus / @@ -244,4 +248,4 @@ spec: # operator-managed components; the database has a separate DNS policy. platform.devantler.tech/dns-policy-rollout: 2026-09-30-ndots-3 image: - name: ghcr.io/coroot/coroot-cluster-agent:1.11.7 + name: ghcr.io/devantler-tech/platform-coroot-cluster-agent:v1.11.7-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:46b6abe52dbf5445277ef3e3bbcb469a89a88e2c610411c5d9a4270a8a8a88c3 diff --git a/scripts/tests/coroot-security-native/README.md b/scripts/tests/coroot-security-native/README.md index 4217f8caa0..a5cbf4c4b0 100644 --- a/scripts/tests/coroot-security-native/README.md +++ b/scripts/tests/coroot-security-native/README.md @@ -1,6 +1,7 @@ # Native Coroot dashboard evidence -The hosted workflow runs `run.sh` against the released Coroot 1.28.1 image, an +The hosted workflow runs `run.sh` against the signed checkpoint compatibility +build of Coroot 1.28.1 pinned in the production resource, an ephemeral Prometheus and a loopback-only synthetic metrics server. It provisions the versioned dashboard definitions twice, checks persisted configurations and an unrelated dashboard, queries every panel through Coroot using the same @@ -54,8 +55,19 @@ no port is published or bound to a public interface. Cleanup removes only this invocation's named containers and temporary files, preserving diagnostic logs and screenshots as seven-day workflow artifacts. -The binary version is verified from its real startup log. Coroot 1.28.1 does -not expose a `--version` option. +The binary version is verified from its real startup log. The compatibility +publisher sets it to `v1.28.1-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477`; +Coroot 1.28.1 does not expose a `--version` option. + +The compatibility build uses upstream revision +`15137ef2f43e01d0a87245e44b937b25515e0afe` and the reviewed +[checkpoint patch](../../../k8s/bases/infrastructure/coroot/coroot-v1.28.1-checkpoint-record.patch). +That patch changes the PostgreSQL inspection, its metric construction and model, +and its regression tests. It changes none of the entrypoint, flags, bootstrap, +project selector, dashboard panel API, range-query alignment or browser routes +audited above. The final image retains the digest-pinned upstream runtime and +entrypoint. The fixture nevertheless runs the exact proposed image through all +persisted-dashboard, API, browser and negative controls before adoption. The workflow also runs for changes to the base Coroot resource and production Coroot patch. A preflight requires the native image to equal the versioned diff --git a/scripts/tests/coroot-security-native/run.sh b/scripts/tests/coroot-security-native/run.sh index 9aa660998c..3144502203 100644 --- a/scripts/tests/coroot-security-native/run.sh +++ b/scripts/tests/coroot-security-native/run.sh @@ -108,7 +108,7 @@ wait_json "${base}/api/user" \ "${artifacts}/user.json" # v1.28.1 has no --version option. main.go logs the compiled version before # loading configuration; assert that real startup line after API readiness. -docker logs "${prefix}-coroot" 2>&1 | grep -E 'version: 1\.28\.1([[:space:]]|$)' \ +docker logs "${prefix}-coroot" 2>&1 | grep -E 'version: v1\.28\.1-checkpoints\.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477([[:space:]]|$)' \ >"${artifacts}/coroot-version.txt" # db/project.go PrometheusConfig adds this selector when using the global # backend. A successful unscoped Prometheus scrape alone cannot prove that the diff --git a/scripts/tests/oci-chart-sources/main_test.go b/scripts/tests/oci-chart-sources/main_test.go index 33809bc36b..9cf9c615b4 100644 --- a/scripts/tests/oci-chart-sources/main_test.go +++ b/scripts/tests/oci-chart-sources/main_test.go @@ -240,7 +240,10 @@ func TestFluxOperatorUpdatesAreProposedAndReviewed(t *testing.T) { var fluxOperatorUpdatePolicy = map[string]any{"automerge": false, "minimumReleaseAge": "0 days"} type renovateConfig struct { - CustomManagers []struct { + EnabledManagers []string `json:"enabledManagers"` + IgnorePaths []string `json:"ignorePaths"` + IncludePaths []string `json:"includePaths"` + CustomManagers []struct { CustomType string `json:"customType"` ManagerFilePatterns []string `json:"managerFilePatterns"` MatchStrings []string `json:"matchStrings"` @@ -250,12 +253,14 @@ type renovateConfig struct { } `json:"customManagers"` Flux builtInManager `json:"flux"` Kubernetes builtInManager `json:"kubernetes"` + Dockerfile builtInManager `json:"dockerfile"` PackageRules []map[string]any `json:"packageRules"` } // builtInManager is the part of a built-in manager's settings that decides // which files it reads. type builtInManager struct { + Enabled *bool `json:"enabled"` ManagerFilePatterns []string `json:"managerFilePatterns"` } diff --git a/scripts/tests/oci-chart-sources/undated_ghcr_test.go b/scripts/tests/oci-chart-sources/undated_ghcr_test.go index fa4237352d..2895c0427f 100644 --- a/scripts/tests/oci-chart-sources/undated_ghcr_test.go +++ b/scripts/tests/oci-chart-sources/undated_ghcr_test.go @@ -21,8 +21,8 @@ var undatedGHCRDependencies = []renovateDependency{ {"ghcr.io/cloudflare/origin-ca-issuer-charts/origin-ca-issuer", "flux", "docker", "k8s/providers/hetzner/infrastructure/controllers/origin-ca-issuer/oci-repository.yaml"}, {"ghcr.io/cloudnative-pg/postgresql", "custom.regex", "docker", "k8s/providers/hetzner/infrastructure/coroot/cluster.yaml"}, {"ghcr.io/cloudnative-pg/postgresql", "custom.regex", "docker", "k8s/bases/apps/umami/cluster.yaml"}, - {"ghcr.io/coroot/coroot", "custom.regex", "docker", "k8s/bases/infrastructure/coroot/coroot.yaml"}, - {"ghcr.io/coroot/coroot-cluster-agent", "custom.regex", "docker", "k8s/bases/infrastructure/coroot/coroot.yaml"}, + {"ghcr.io/coroot/coroot", "dockerfile", "docker", "k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile"}, + {"ghcr.io/coroot/coroot-cluster-agent", "dockerfile", "docker", "k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile"}, {"ghcr.io/umami-software/umami", "kubernetes", "docker", "k8s/bases/apps/umami/cron-job.yaml"}, } @@ -81,11 +81,11 @@ func TestUndatedGHCRGuardRejectsMissingExemptionAndWrongManager(t *testing.T) { {"image read by the flux manager", renovateDependency{"ghcr.io/umami-software/umami", "flux", "docker", "k8s/bases/apps/umami/cron-job.yaml"}}, {"name that is only a prefix of the pinned image", - renovateDependency{"ghcr.io/coroot/coroot-cluster", "custom.regex", "docker", "k8s/bases/infrastructure/coroot/coroot.yaml"}}, + renovateDependency{"ghcr.io/coroot/coroot-cluster", "dockerfile", "docker", "k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile"}}, {"custom manager that does not read the file", renovateDependency{"ghcr.io/cloudnative-pg/postgresql", "custom.regex", "docker", "k8s/bases/apps/umami/cron-job.yaml"}}, {"wrong datasource", - renovateDependency{"ghcr.io/coroot/coroot", "custom.regex", "github-releases", "k8s/bases/infrastructure/coroot/coroot.yaml"}}, + renovateDependency{"ghcr.io/coroot/coroot", "dockerfile", "github-releases", "k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile"}}, } { t.Run(tc.name, func(t *testing.T) { text, err := os.ReadFile(filepath.Join(root, tc.dep.packageFile)) @@ -108,6 +108,31 @@ func extractedAs(config renovateConfig, dep renovateDependency, text string) err }) } switch dep.manager { + case "dockerfile": + // Renovate's documented defaults include prefix.Dockerfile. Model the + // fixed, tagged FROM references used by these rows, not ARG expansion. + // https://docs.renovatebot.com/modules/manager/dockerfile/ + patterns := []string{ + `/(^|/|\.)([Dd]ocker|[Cc]ontainer)file$/`, + `/(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$/`, + } + patterns = append(patterns, config.Dockerfile.ManagerFilePatterns...) + if (config.Dockerfile.Enabled != nil && !*config.Dockerfile.Enabled) || + (len(config.EnabledManagers) > 0 && !slices.Contains(config.EnabledManagers, dep.manager)) || + !readsFile(patterns) || (len(config.IncludePaths) > 0 && !readsFile(config.IncludePaths)) { + return fmt.Errorf("the dockerfile manager does not read %s", dep.packageFile) + } + for _, pattern := range config.IgnorePaths { + if patternMatch(pattern, dep.packageFile) != noMatch { + return fmt.Errorf("the dockerfile source is ignored or undecidable: %s", dep.packageFile) + } + } + image := regexp.MustCompile(`(?im)^\s*FROM\s+(?:--platform=\S+\s+)?` + regexp.QuoteMeta(dep.packageName) + + `:[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?(?:@sha256:[0-9a-f]{64})?(?:\s+AS\s+[A-Za-z0-9_-]+)?\s*$`) + if !image.MatchString(text) || dep.datasource != "docker" { + return fmt.Errorf("the dockerfile manager does not extract a fixed tagged FROM for %s from %s", dep.packageName, dep.packageFile) + } + return nil case "flux": if !readsFile(config.Flux.ManagerFilePatterns) { return fmt.Errorf("the flux manager does not read %s", dep.packageFile) @@ -162,3 +187,42 @@ func extractedAs(config renovateConfig, dep renovateDependency, text string) err } return fmt.Errorf("manager %q is not modelled", dep.manager) } + +// A base-image dependency must not disappear when its manager or file is +// excluded, or when its real FROM is replaced by an inert textual mention. +func TestUndatedGHCRDockerfileGuardRejectsLostDiscovery(t *testing.T) { + dep := undatedGHCRDependencies[4] + body, err := os.ReadFile(filepath.Join(repositoryRoot(t), dep.packageFile)) + if err != nil { + t.Fatal(err) + } + for _, mutation := range []string{"disabled", "other-managers-only", "ignored", "excluded", "wrong-filename", "commented-from", "unresolved-version"} { + t.Run(mutation, func(t *testing.T) { + config, candidate, text := readRenovateConfig(t), dep, string(body) + if err := extractedAs(config, candidate, text); err != nil { + t.Fatalf("positive control: %v", err) + } + switch mutation { + case "disabled": + enabled := false + config.Dockerfile.Enabled = &enabled + case "other-managers-only": + config.EnabledManagers = []string{"custom.regex"} + case "ignored": + config.IgnorePaths = []string{dep.packageFile} + case "excluded": + config.IncludePaths = []string{"unrelated/**"} + case "wrong-filename": + candidate.packageFile += ".txt" + case "commented-from": + text = strings.Replace(text, "FROM "+dep.packageName+":", "# FROM "+dep.packageName+":", 1) + case "unresolved-version": + reference := regexp.MustCompile(regexp.QuoteMeta(dep.packageName) + `:[^\s@]+`) + text = reference.ReplaceAllLiteralString(text, dep.packageName+":${UNRESOLVED_VERSION}") + } + if err := extractedAs(config, candidate, text); err == nil { + t.Fatal("accepted lost dependency discovery") + } + }) + } +} diff --git a/scripts/tests/test-coroot-api-key-contract.sh b/scripts/tests/test-coroot-api-key-contract.sh index c4c36d7d71..b1a01ac6bf 100755 --- a/scripts/tests/test-coroot-api-key-contract.sh +++ b/scripts/tests/test-coroot-api-key-contract.sh @@ -53,13 +53,15 @@ assert "${infrastructure}" \ # Component images stay pinned. The operator lists the registry only when an # image is unset, so pins remain the primary path and GHCR egress the fallback. -# The node-agent may use either Coroot's upstream image or the platform's -# signed compatibility build; both repositories are explicit allow-list entries. +# The node-agent also accepts its platform compatibility repository. The server +# and collector additionally accept only these exact reviewed checkpoint builds. assert "${infrastructure}" \ "${coroot} | .[0].spec | - (.communityEdition.image.name | test(\"^ghcr[.]io/coroot/coroot:[0-9]\")) and + ((.communityEdition.image.name | test(\"^ghcr[.]io/coroot/coroot:[0-9]\")) or + .communityEdition.image.name == \"ghcr.io/devantler-tech/platform-coroot:v1.28.1-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:9a2782f405f9ee673301912b3b2cf85a4a154fbfeb1542df2d4937d6600396f1\") and (.nodeAgent.image.name | test(\"^ghcr[.]io/(coroot/coroot-node-agent:[0-9]|devantler-tech/platform-coroot-node-agent:v[0-9])\")) and - (.clusterAgent.image.name | test(\"^ghcr[.]io/coroot/coroot-cluster-agent:[0-9]\"))" \ + ((.clusterAgent.image.name | test(\"^ghcr[.]io/coroot/coroot-cluster-agent:[0-9]\")) or + .clusterAgent.image.name == \"ghcr.io/devantler-tech/platform-coroot-cluster-agent:v1.11.7-checkpoints.1-27c6c840e2006076c0ff5ed7dabe15daa1c38477@sha256:46b6abe52dbf5445277ef3e3bbcb469a89a88e2c610411c5d9a4270a8a8a88c3\")" \ 'the Coroot component images must remain pinned to explicit GHCR tags' policy='map(select(.kind == "CiliumNetworkPolicy" and .metadata.namespace == "observability" and .metadata.name == "allow-coroot"))'