Skip to content

Platform CI refuses every image reference that is neither an attested app release nor a listed vendor at a digest #49

Closed
3 of 3 issues completed

Description

@FlexOr2

Platform CI gets a mandatory job images. It renders the tree as Flux applies it, every Kustomization with the locked flux2, refuses every HelmRelease for good (#49 line 2; a chart is rendered by a shipped command into plain manifests in the repository, which this job judges like any object), and refuses every image of every rendered Pod template unless it is an attested release of an overnightworks app or a digest-pinned image of an allowlisted vendor repository. It is a boundary over every rendered object and over the render inputs CI controls (#50 expectation line 2: "B1 stays a boundary in full"; ruling C, not option A's tripwire). What it cannot render is named under Named gaps; the admission check in the cluster (#50, not built yet) is to be a second layer after the merge.

Caller

Ruled

Decided by the head

Re-planned to line 1's ruling C. Points 1, 2, 7, 9 and 10 change; line 2's ruling A (07.11.2026) removes the HelmRelease render from them. Points 3 to 6 and 8 stand. The rounds of 06.11.2026 judged the replaced source-parsing design.

  1. The job renders, it does not decode.

    • flux build kustomization <root> --dry-run --in-memory-build --recursive of the locked flux2 builds the root Kustomization from clusters/pc/flux-system/gotk-sync.yaml, in a container without network, so a remote kustomize resource fails the render. Every nested Kustomization on this repository's GitRepository is built with Flux's own generator, images, patches and inline postBuild.substitute; a suspended one too.
    • The render is trusted only as far as it is the tree Flux applies. Red: a rendered root that differs from the committed one; two Kustomizations with one identity; a health-checked controller the render lacks; a mapped source with include, ignore, recurseSubmodules or sparseCheckout; a root GitRepository other than this repository's default branch, read from the run; a platform-secrets url other than the owner's repository named as the object (an optional .git; this repository and every other repository of the owner red); any .sourceignore; ignoreMissingComponents: true.
    • A HelmRelease is not rendered: it is red for good (line 2). A chart reaches the tree as committed plain manifests that its generator writes under a regeneration gate (Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's charts:render), and the rule judges them like any other object.
    • The rule judges every container, init container, ephemeral container and image volume image of every Pod template in Pod, PodTemplate, ReplicationController, ReplicaSet, Deployment, StatefulSet, DaemonSet, Job and CronJob, matched by apiVersion and kind.
    • Another kind that holds an image key or a host-qualified ref with a tag or digest is red. Exempt, by group, version and kind: a kustomize.toolkit.fluxcd.io/v1 Kustomization the build built, and an apiextensions.k8s.io/v1 CustomResourceDefinition. Every helm.toolkit.fluxcd.io object is red for good.
    • Case: the registry host and the repository path must be lowercase; a tag follows [\w][\w.-]{0,127}.
    • A render that fails is red with the renderer's message, and a source that is not mapped fails the render.
    • The PC-only source platform-secrets is mapped to an empty source and named in the log.
    • A substituteFrom Secret renders its variables empty, at their defaults or literal, and is named in the log; a substituteFrom ConfigMap is red.
    • In flux-system, an image that flux install --export of the locked flux2 names passes as Flux's own.

    The old point 1's constructs (escapes, aliases, custom tags, map keys, YAML in strings, patch operations, split mappings, images overrides, replacements and their delimiters, fieldPaths, case-insensitive kustomize keys, ImageTagTransformer, generator literals, repeated keys) are now resolved by kustomize or the chart. The rule judges only their rendered result; a repeated key in a manifest fails the render, and one inside a patch string is not driven (U-7 of the re-plan). The exempt generated file and the two policy-file exemptions go.

    Weighed:

    • The Kustomization render, chosen: flux build kustomization --dry-run --in-memory-build --recursive --local-sources. It is Flux's own build code at the locked version, one command renders the whole tree, an unmapped source fails loud with no code of ours, and in a container without network it fetches nothing.
    • Refusing remote kustomize resources, chosen: the render runs with --network none, which refuses https:// files and git bases alike at the pull request, in B1's one script. Rejected: kustomize-controller --no-remote-bases=true as the fix (it refuses only kustomize's git clones, not https:// file resources, lets CI pass what the PC then refuses, and needs a scope addition and a cluster cell; kept as Only attested app images and digest-pinned listed vendor images start on the PC #50's hardening follow-up); a dead proxy plus GIT_ALLOW_PROTOCOL (several unverified bypass paths); sudo unshare --net (needs root and differs from local runs); detecting remote entries in kustomization.yaml (source parsing again, against the ruling).
    • kustomize build or kubectl kustomize per path, rejected: ./clusters/pc has no kustomization.yaml (Flux generates it), and a Kustomization's images, patches, postBuild and targetNamespace live outside kustomize, so the script would reimplement Flux.
    • flux build without --recursive, rejected: code for what the flag does.
    • A changed self-reference, chosen red by comparing the rendered root's spec with the committed one. Processing it instead is a fixpoint loop for a state no tree needs, and the PC would run each intermediate spec in turn.
    • --strict-substitute, rejected: it fails the whole render on any Secret variable, including ones outside images.
    • Reading the k3d cluster after cluster reconciles, rejected: it ties images to the 30-minute job and its success, misses hook Jobs already deleted, and the ruling names a build with flux build or kustomize and helm template in CI, not a reconcile.
    • flate (home-operations/flate), not chosen today: its Kustomization half is its own orchestration around kustomize's library, not Flux's code, with its fidelity resting on a five-month-old v0.x (not driven); it fetches every source with machine credentials and skips an unauthenticated one only under a broad --allow-missing-kinds Secret; its get images coverage of init containers, ephemeral containers and image volumes is unverified. It is AGPL-3.0 and the maintained successor of flux-local. Not re-weighed: no HelmRelease render is built (line 2).
    • flux-local, rejected: its README says it is deprecated and points to flate.
    • HelmReleases, chosen (line 2): refused, and charts rendered into committed manifests by a shipped command. Rejected: rendering each HelmRelease in this job with helm template (B1c; its plan drew six blocking findings, each a way CI's render can differ from helm-controller's render on the PC), kustomize's helmCharts inflator (needs helm anyway, plus its own chart cache) and flate.
    • The rule, chosen: a yq extraction over Pod-template kinds and a bash judge. Rejected: kyverno CLI applying AD1a's ValidatingAdmissionPolicy to the rendered objects (AD1a is not built, kyverno CLI is a new tool, its paramRef support offline is unverified, and the vendor-list rules and B1b still need the script; a follow-up once AD1a lands); conftest or kubeconform (a second policy language for one rule, and kubeconform checks schemas, not images); judging every string of every rendered object (false reds and no gain, since the node pulls only from Pod specs and unknown kinds fail loud).
    • Flux's own controllers, chosen: compare with flux install --export of the locked flux2 (a relationship read from the owner, it judges a hand edit, it runs offline). Rejected: a literal list of the four repositories (restates the generated file) and exempting the file (impossible once the tree is rendered).
    • The PC-only source, chosen: one named empty mapping in the script, a pair with the cluster overlay whose drift fails loud. Rejected: deriving it from secretRef (a heuristic that would turn every private source into a silent gap). One owner for both jobs (a marker on the source object) is a follow-up.
    • The evidence, chosen: committed fixture trees run by the job, so about 70 sub-cells become CI evidence on every run, B1b cannot regress B1a's cells, and every flux2 bump through mise lock is covered. Rejected: driven scratch trees only, which are cheaper today but last one review.
  2. App or vendor, by a vendor allowlist platform owns. A classified ref is either an app ref under ghcr.io/<owner>/, checked by points 3 to 5, or a vendor ref. A vendor ref must be <listed repository>[:<tag>]@sha256:<64 hex>: its repository is an entry of the vendor list, a tag before the digest is allowed, so Traefik's …@sha256: and Kyverno's …:v<version>@sha256: both pass, and the admission item's form rule holds the same grammar. Anything else is red, naming the file, the path and the decoded value: docker.io/someone/hopin@sha256:… is red.

  3. The tag (unchanged). The image tag is the version without v; the git tag is v<version> (quality's docs/release.md at 7715afe: an app releases YYYY.MM.DD.N, tagged vYYYY.MM.DD.N, and the caller passes the same version to image.yml; hopin#537 follows it).

    • Lookup: the job runs git ls-remote https://github.com/<owner>/<repo> refs/tags/v<version> 'refs/tags/v<version>^{}' and keeps only lines whose ref name is exactly one of the two (ls-remote matches patterns against the tail). It takes the peeled ^{} commit when it exists, else the direct one, so a lightweight tag (release-tag) and an annotated tag (today's hand tags) both resolve to their commit.
    • Why the tag, not the release: the tag job runs after the image job (needs: [gate, <image job>] in hopin#537), so an image can exist without a tag. A tag left without a release is completed by hand (docs/release.md, Refusals). The tag is therefore what the guard requires.
    • Weighed: refusing annotated tags (the kind of tag proves nothing; the attestation and the gate do); no peel (an annotated tag reads as its tag object and fails --source-digest with a misleading reason).
  4. The attestation, only through flags on unforgeable fields, read by digest. The job runs gh attestation verify oci://ghcr.io/<owner>/<repo>[/<name>]@sha256:<hex> --repo <owner>/<repo> --signer-workflow <owner>/quality/.github/workflows/image.yml --source-digest <tag commit> --source-ref refs/heads/main --predicate-type https://slsa.dev/provenance/v1 --deny-self-hosted-runners --format json.

    • By digest: gh gets the ref without its registry tag. The version is proven through git, and gh cannot then fail on a missing registry tag instead of on the attestation. This is new against D5 point 2.
    • The predicate type is passed although it is gh's default, so a changed default cannot loosen the check.
    • Nothing from statement: the job reads no field of it.
    • From the API: attestations come from GitHub's API, not --bundle-from-oci, which gh's help says cannot detect a renamed repository.
  5. The signer's version (unchanged). For every verified attestation, .verificationResult.signature.certificate.buildSignerDigest must be 40 hex characters, and gh api repos/<owner>/quality/compare/<digest>...main --jq .status must print ahead or identical. An empty field, a 404, diverged or behind is red, naming the commit. Weighed: --signer-digest with one quality commit (platform would copy every app's pin, D5 point 3); a git fetch of quality and merge-base --is-ancestor (a clone for one bit the compare API gives).

  6. Grants and registry login (unchanged). The job grants contents: read and packages: read, plus attestations: read only if cell E14 shows the API lookup needs it.

    • Login: gh's oci:// path reads the registry with Docker's keychain (cli/cli pkg/cmd/attestation/artifact/oci/client.go at v2.103.2). A step therefore runs docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin with the job's token from env, never in argv.
    • Package grant: each package grants platform Read under its Actions access (O8, the operator's).
    • Weighed: docker/login-action (a pinned dependency for one command); a PAT secret (a secret where the job token serves).
  7. flux2, yq and gh from the lock. flux2 is locked already; yq and gh (B1b) enter mise.toml and mise.lock (aqua:mikefarah/yq, aqua:cli/cli); helm enters with Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's generator, never in this job, and the job installs them with scripts/bootstrap_pc.sh --only tools as cluster does. yq reads the rendered stream and the vendor list.

    • Why: the verifier and the renderer decide what goes live, and the runner image's copies change with the image. gh's --signer-workflow matching has changed between releases (the end-anchored SAN pattern in pkg/cmd/attestation/verify/policy.go at v2.103.2).
    • Coverage: the bootstrap job's every-executable check then covers each without an edit.
    • Weighed: the runner's binaries (no diff shows a verifier change); a download with its own sha256 (a second install path beside mise).
  8. A script in scripts/, run by its own job. scripts/verify_image_release.sh is called from a job images. It holds functions per check, reports every failing ref and exits 1 if any fails. It reads the vendor list from its one owner (point 2).

  9. Tools checked against (Platform on k3s and Flux: from an empty PC to every app live, with hal archived #1 invariant 2). None replaces the script, because none checks the git tag or that the signer commit is on quality's main:

    • Admission in the cluster (the admission item, draft: a native ValidatingAdmissionPolicy for the form and Kyverno's ImageValidatingPolicy for the attestation): a second layer after the merge. It judges every Pod the API server admits outside k3s's and Flux's own parts, by form and, for app refs in Pods, by attestation, so chart defaults, values from Secrets and ConfigMaps, postBuild substitution and parser differences cannot hide an image from it. It is no boundary for this job's checks: it acts only after the merge, while the ruled guard refuses the pull request, and it cannot check the git tag or the signer commit on quality's main, so an app ref this job cannot read gets neither. Kyverno's Sigstore bundle path was read in source, not driven.
    • slsa-verifier: built for known builders such as slsa-github-generator; quality's reusable workflow is none of them (not verified against its current release).
    • cosign verify-attestation: could replace the gh call, but gh is the documented verifier of GitHub attestations, which quality's docs/release.md names; cosign's reading of the registry-pushed GitHub bundle is not verified.
    • flate, flux-local, kubeconform, conftest, kyverno CLI and reading the k3d cluster: each weighed in point 1 with its reason. None replaces the script: it also checks the git tag and the signer commit on quality's main.
  10. Cut into two pull requests. The whole is beyond the slice limit. It splits along the seams of the render (B1c, B1c: the job images renders each HelmRelease with helm template and judges its images #60, is closed as superseded by line 2):

What is known

  • image.yml at 89069a1 (quality#402, PR The weekly backup Schedule and its PodConfig are a component of their own #404; unchanged at 7715afe):
    • Calls and outputs: one image per call; outputs digest and ref = ghcr.io/<owner>/<repository>[/<image>]:<version>@<digest>, lowercased.
    • Build: builds the gated commit for linux/amd64.
    • Attestation: with push, actions/attest (v4.2.2, push-to-registry: true, create-storage-record: false) writes SLSA provenance v1. BuildKit's provenance and SBOM sit in the image, not separately signed.
    • The docs: quality's docs/release.md gives the consumer command without --source-digest.
  • The certificate field. The signer digest is Fulcio's Build Signer Digest, OID 1.3.6.1.4.1.57264.1.10 (docs/oid-info.md in sigstore/fulcio; for GitHub its job_workflow_sha).
    • In gh: sigstore-go v1.3.0, which gh 2.103.2 pins, names it buildSignerDigest in certificate.Extensions.
    • Read once, on 06.11.2026 with gh 2.103.2 against cli/cli's own attested release: .verificationResult.signature.certificate holds buildSignerDigest, buildSignerURI, sourceRepositoryDigest, sourceRepositoryRef and runnerEnvironment.
  • The compare API, driven 06.11.2026: compare/<signer>...main printed ahead for 89069a1 and 8f766e3, and diverged for PR The weekly backup Schedule and its PodConfig are a component of their own #404's head 239e51e, which the squash merge left off main.
  • Tags today, driven with git ls-remote:
    • refs/tags/v2026.11.06.1 alone yields hopin's tag object 831fa65; only the ^{} pattern yields its commit df39828.
    • hopin, presentator and speech-runtime tag annotated by hand today; songmaker has lightweight tags beside annotated ones. release-tag creates lightweight tags at the gated commit.
  • Driven, 07.11.2026 (the locked or named binary in a throwaway ubuntu:26.04 container, --network none except for helm's chart pull, the repository mounted read-only, no kubeconfig):
    • flux2 2.10.0: main renders offline only with --in-memory-build (36 objects, four images, all Flux's own); --recursive fails loud on an unmapped GitRepository or OCIRepository and with platform-secrets mapped empty equals the plain root build.
    • In a nested Kustomization spec.images, spec.patches and inline postBuild.substitute apply; substituteFrom is skipped (null, a default, or literal); --strict-substitute fails the render.
    • The escape is decoded, the alias expanded, the custom tag dropped; a repeated image key fails the render; image volumes are rendered.
    • targetNamespace, namePrefix, commonMetadata and components together render Deployment/tns/pre-web with the component's initContainer.
    • A changed root self-reference renders the root at ./apps and never builds it; a repeated nested identity leaves the second path unbuilt; both exit 0. A suspended Kustomization is rendered.
    • With network, an https:// file resource is fetched and rendered; with --network none, it and a git base fail ("network is unreachable"), exit 1.
    • flux install --export runs offline and equals the committed gotk-components.yaml.
    • yq 4.54.1: an 18-line extraction of object, field path and image, and the unknown-kind detection, on the renders above.
    • helm v4.3.1: Traefik 42.3.1 renders by digest sha256:a702a4316735397ecd1c3ed5287cccf87cdbc69abb0451c0b2d371408473bc33; the chart joins image.digest into docker.io/library/traefik@sha256:<digest>; a wrong digest fails.
  • Read:
    • go.mod files: flux2 v2.10.0 uses pkg/kustomize v1.44.0 and kustomize api v0.21.2; kustomize-controller v1.10.1 the same pkg/kustomize with kustomize api v0.21.1; helm-controller v1.7.1 uses helm v4.3.1.
    • flux2 internal/build/build.go: Build skips a rendered Kustomization by name and namespace only, maps sources by key, and has no suspend check; no remote-base flag in cmd/flux.
    • fluxcd/pkg kustomize v1.44.0: GenerateManifest loads .sourceignore below the built path only; ignoreMissingComponents skips a missing component.
    • kustomize api v0.21.1 FileLoader.Load fetches a remote file with an http.Client, outside kustomize-controller's --no-remote-bases filesystem.
    • Kubernetes v1.37.2: ImageVolume is GA and locked on since 1.36, so the node pulls volumes[].image.reference.
    • helm v4.3.1: ValidateReference installs by digest only without a version; template has --kube-version, --api-versions, --is-upgrade, --skip-tests. helm-controller v1.7.1: GetReleaseName and GetReleaseNamespace.
    • mise registry maps helm to aqua:helm/helm; quality v0.9.2 ROOT_BASE_DIRECTORIES includes tests/.
  • Flux's patch fields in the committed schema: a Kustomization's spec.patches[].patch and a HelmRelease's postRenderers[].kustomize.patches[].patch are strings ("an inline StrategicMerge patch or an inline JSON6902 patch").
  • The reference grammar (distribution/reference regexp.go, main): a tag is [\w][\w.-]{0,127}.
  • The tree today: E10 of B1a: the mandatory job images renders the tree as Flux applies it and refuses every rendered image that is neither a listed vendor at a digest nor an app ref #55's matrix renders it: the root flux-system, the GitRepositories flux-system and platform-secrets and the Kustomization platform-secrets, four images, all Flux's own.
  • Where attestations live. The app repositories are public, so actions/attest signs with Sigstore's public-good instance and uploads to GitHub's attestations API (actions/attest README at 1e69f48). The image name and digest of a private package therefore appear in the public transparency log; the source is public anyway.
  • No attested image exists in the organisation today (inferred):
    • No caller: no app repository has a workflow calling image.yml; hopin's workflows are ci.yml and code-scanning.yml.
    • No test image: quality's CI checks image.yml only statically.
    • The registry: GHCR's anonymous token is denied for every app name, as for a private or absent package. The organisation's package listing needs read:packages, which the head's token lacks: not verified.
  • Named gaps. The job's reach ends at what the locked tools render. No layer on the PC judges these gaps yet. The admission item (Only attested app images and digest-pinned listed vendor images start on the PC #50) owns the second layer: the form rule on every admitted Pod and workload once AD1a lands, and the attestation check once AD2 lands. No tag or signer check reaches a ref B1 cannot render.
    • G1, PC-only source. platform-secrets is not rendered, because CI holds no key; whatever it applies is not judged. It stands under the cost the operator accepted with C, „Werte aus deinen geheimen Einstellungen sieht sie trotzdem nicht“, and only while its url is the owner's repository that carries the object's name (S14). It is named in every log. The cluster job's overlay swaps the same source, and a new PC-only source turns both jobs red.

    • G2, Secret substitutions (ruled).

      • A variable from a substituteFrom Secret renders empty or stays literal, so an image built from it is red.
      • With a default and an inline variable beside it, the default renders. CI judges that default while the PC runs the Secret's value.
    • G5, render fidelity.

      • The flux2 CLI builds with kustomize api v0.21.2, and kustomize-controller v1.10.1 with v0.21.1.
    • G6, images outside Pod templates. These are not judged:

      • an image inside a longer string of a non-workload object;
      • an unknown kind's image written without a host and outside an image key;
      • an operator's default image for a custom resource that names none.

      Once AD1a lands, admission judges the Pods they become (Only attested app images and digest-pinned listed vendor images start on the PC #50).

    • G7, CI fixtures. Images that only a CI step runs in the job cluster are no rendered object. They never run on the PC and have no entry in its vendor list. How k3d admits them is AD1a's design (Only attested app images and digest-pinned listed vendor images start on the PC #50).

    • G8, Flux's controllers. They run tag-only in flux-system and pass as the locked flux2's export. Pinning them by digest is the vendor-pin bump item's job (Every vendor pin in platform's manifests has a maintained bump path #47).

    • G10, chart signatures. Neither the render nor Flux verifies the chart's provenance. This is Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44's gap, unchanged.

    • Kept, unchanged: the false red for a ref-shaped non-image string (now only in unknown kinds); forks; the registry tag not compared with its digest; old signers; tag creation.

    • G12, P6 leaves B1. A rendered image is always one string, so B1 no longer enforces that an app ref is written as one string in source. P6 stays hopin runs on the platform from its attested private image, routed by Traefik #46's and the bump path's (P1: every app release yields an attested private image in GHCR #4) convention.

    • Refusals (not gaps: each is red until a caller and a new decision):

      • postBuild.substituteFrom ConfigMaps: the dry run cannot read them.
      • Every HelmRelease, for good (line 2): a chart is rendered into committed manifests by its generator, which Traefik runs in the cluster from manifests rendered from its pinned chart and answers 404 for every host without a route #44 builds.
      • Remote kustomize resources, https:// files and git bases: the render has no network (S1).
      • Changed or repeated self-references, and a render without the root's health-checked controllers (S1).
      • Source fields and ignore files that make Flux's artifact differ from the checkout, and mapped sources other than this repository's default branch and the owner's secrets repository (S14).
  • Invariants kept. No secret in the repository, a log or a test; the job token reaches the registry only through stdin; every check fails closed; nothing touches the PC, a package or a tag.
  • Rollback. Revert the commit; removing a mandatory job is a declared loosening of quality/config.toml. Nothing persistent is created.
  • Deletes. Nothing in the tree. D5's Dependabot route and its spike are not built (P1: every app release yields an attested private image in GHCR #4, comment 6686280549).
  • Size. Against main: B1a about 220 production lines added and 2 removed, in five files; B1b about 60 in three.

Slices

Under ruling C, the slices' own matrices hold the cells. #55's fixture suite runs in the job (no hand-driven fallback), and #56's driven cells are named there.

Verification: CI on the pull request and the landing push; the driven cells; quality hygiene . printing hygiene: clean.

Unknowns

External facts, kept as unresolved evidence until a driven cell settles them with the locked tools (the Codex review's follow-up 4):

  • The locked gh's output: whether it prints buildSignerDigest at the path read with gh 2.103.2, and its --signer-workflow matching. Settled by E1 and E8 with the version mise.lock pins.
  • Attestations without the grant: whether platform's job token reads another public repository's attestations through the API without attestations: read. Settled by E14; a user token did on 06.11.2026 (cli/cli).
  • Private packages: whether GHCR serves a private package of another repository to platform's job token once O8's Read grant exists. Settled by E1. Whether an API sets the grant: not verified.
  • The kustomize skew between the flux2 CLI and kustomize-controller (G5).
  • Dependabot runs: whether a Dependabot-triggered run in platform (its github-actions and opentofu entries) reads the private packages with packages: read. If not, those pull requests turn red once the first app ref lands. Read at the first such pull request after hopin runs on the platform from its attested private image, routed by Traefik #46.
  • Issue states, read 06.11.2026: quality#406 and quality#407 closed; hopin#537 and presentator#851 open.
  • First dispatch: which item owns hopin's first dispatch (O9): hopin#537 excludes it.

Neighbours

version = 1
now = "B1a (#55) landed as 0f4cdaf (PR #57) and B1b (#56) as f284a22 (PR #67, 07.11.2026), so the job images renders the tree, judges vendor refs and checks each app ref's tag, attestation and signer commit; B1c (#60) is closed as superseded by line 2's ruling (07.11.2026): HelmReleases are refused for good and charts are rendered into the repository, and helm-controller leaves Flux in #71 after #44."
next = "B1's follow-ups are #65; then the head judges whether this item can close."
done_when = "On pull requests and pushes to main, the mandatory job images renders the tree with the locked flux2, without network, trusting the render only where it is the tree Flux applies (the committed root unchanged, this repository's default branch, no source field or ignore file that changes Flux's artifact). It refuses every image of every rendered Pod template, image volumes included, that is neither a repository listed in the admission item's vendor ConfigMap, with an optional tag, at a sha256 digest, nor a full app ref whose v-tag resolves, peeled when annotated, to the commit an image.yml attestation from quality's main names for that app's repository. Every HelmRelease, a kind the rule does not know that carries an image, a remote kustomize resource and a substituteFrom ConfigMap are red; platform-secrets and substituteFrom Secrets are named gaps. It names every failing ref, uses only read grants and the locked tools, and the scenario matrix is reported, E1 green against hopin's first attested release and E15 green on an annotated tag."

size = "L"

[[expectation]]
text = "Die Image-Prüfung im Pull Request fängt ehrliche Fehler deiner Agenten; die harte Grenze ist die Prüfung im Server."
asked_on = 2026-11-06
ruling = "C"
ruled_on = 2026-11-06
card_hash = "sha256:79ba123769b367ed8dbce9eaa0a2ce5c1b5d620504c214c3c7079c7cef732993"
via = "chat"
question = "Soll die Image-Prüfung im Pull Request nur ehrliche Fehler fangen, während dein Server die harte Grenze bleibt?"
problem = "Jede Prüfrunde findet neue Tricks, die Prüfung zu umgehen; fertig wird sie so nie."
decision_kind = "data_risk"
area = "Plattform"
topic = "Image-Prüfung"
recommended = "A"
reason = "Drei Prüfrunden hintereinander fanden je neue Schreibweisen, mit denen ein absichtlich getarntes Image durchkäme (Berichte vom 06.11.2026). Der Server prüft dagegen jedes startende Programm selbst, egal wie es beschrieben ist. Ein getarntes eigenes Image müsste zudem erst in dein Paketlager gelangen, in das nur deine Bauabläufe schreiben."

[[expectation.option]]
name = "Hilfe, Grenze im Server"
effect = "Die Prüfung im Pull Request fängt Tippfehler und vergessene Versionen deiner Agenten. Absichtlich getarnte Images hält der Server auf: feste Version und bekannte Hersteller sofort, die Herkunft, sobald das Prüfwerkzeug fertig ist."
notice = "Die Image-Prüfung wird bald fertig, und hopin rückt näher."
cost = "Bis das Prüfwerkzeug fertig ist, könnte ein absichtlich getarntes eigenes Image ohne Herkunftsnachweis starten, wenn es in dein Paketlager gelangt und du den Pull Request zusammenführst."
undo = "reversible"
undo_how = "Später wieder zur harten Grenze machen; die gefundenen Schreibweisen stehen als Liste bereit."
contract = "B1 (#49, #55, #56) becomes a tripwire under the global contract's Reviews: an aid for cooperative agents whose contract is the cases #49 and #55 list, the round-1 extensions of the head's #55 comment of 06.11.2026 included; a gap outside them is a follow-up on #49 and makes no lane material risk. The boundary that stops a non-cooperative author is the admission layer (#50): AD1a's form rule on every admitted Pod and workload, AD2's attestation check once Kyverno 1.20.x GA ships, and the operator's merge. #49 names the found constructs as gaps: case-insensitive kustomize keys, replacement delimiters on sources and targets, decoded options strings, ImageTagTransformer, generator literals, repeated patch-operation keys."

[[expectation.option]]
name = "Harte Grenze, weiter suchen"
effect = "Die Prüfung im Pull Request bleibt selbst eine harte Grenze; jede gefundene Schreibweise wird geschlossen, bevor sie fertig ist."
same_as_today = true
notice = "Die Image-Prüfung und damit hopin warten, bis keine Prüfrunde mehr etwas findet."
cost = "Wie lange das dauert, ist offen; jede Runde kostet Stunden und macht die Prüfung größer."
undo = "reversible"
undo_how = "Später zur Hilfe herabstufen."
contract = "B1 stays a boundary in full (#49); every construct a review finds is closed before B1a lands."

[[expectation.option]]
name = "Neu bauen auf Ergebnis"
effect = "Die Prüfung liest nicht mehr die Beschreibungen, sondern das, was die Werkzeuge daraus erzeugen, und prüft nur dort die Images."
notice = "Die Image-Prüfung kommt später, wird aber kleiner und schwerer zu umgehen."
cost = "Ein neuer Plan und etwa ein Tag Arbeit; Werte aus deinen geheimen Einstellungen sieht sie trotzdem nicht."
undo = "reversible"
undo_how = "Zurück zur heutigen Prüfung."
contract = "B1 is re-planned to judge rendered output: each Flux Kustomization path built with flux build or kustomize, each HelmRelease rendered with helm template and its values in CI, and the image rule applied to the rendered objects only; postBuild substitutions from Secrets stay a named gap; B1a's source-parsing script is replaced."

[[expectation]]
text = "Charts kommen als fertige Dateien ins Repository; Helm-Beschreibungen bleiben dauerhaft verboten."
asked_on = 2026-11-07
ruling = "A"
ruled_on = 2026-11-07
card_hash = "sha256:0e4a0daefe714c7db47687ea99e204ea280f9b5a96ab3e7fa3bfae5184d4ccee"
via = "chat"
question = "Sollen Charts als fertige Dateien ins Repository kommen, statt dass der Server sie selbst zusammenbaut?"
problem = "Fertige Dateien kann die Image-Prüfung vollständig lesen; eine Helm-Beschreibung baut erst der Server zusammen."
decision_kind = "technical"
area = "Plattform"
topic = "Charts als Dateien"
recommended = "A"
reason = "Fertige Dateien im Repository sind ein gängiges Vorgehen bei Flux (Rendered-Manifests-Muster). Die Chart-Prüfung hatte sechs Prüfbefunde, jeder ein Weg, wie die Prüfung etwas anderes sieht als der Server. Traefik wird schon so geplant."

[[expectation.option]]
name = "Charts als fertige Dateien"
effect = "Jeder Chart wird von einem mitgelieferten Befehl in fertige Dateien umgewandelt und im Repository gespeichert. Die Image-Prüfung verbietet Helm-Beschreibungen dauerhaft und prüft die Dateien wie alles andere."
notice = "Du siehst im Pull Request genau, was der Server bekommt; eine Chart-Änderung zeigt sich als Dateiänderung."
cost = "Ein Chart mit Installationsschritten passt nicht hinein; Helms Rückrollen und Versionsverlauf entfallen, Git ersetzt sie."
undo = "reversible"
undo_how = "Die Helm-Prüfung kann später gebaut werden, falls ein Chart sie braucht."
contract = "The mandatory job images refuses every HelmRelease for good, and every chart is rendered by a shipped command into manifests committed to the repository under a regeneration gate. B1c (#60) closes as superseded, and helm-controller leaves Flux in its own item."

[[expectation.option]]
name = "Später Helm-Prüfung bauen"
effect = "Die Image-Prüfung bekommt später die Fähigkeit, Helm-Beschreibungen selbst zu prüfen, bevor die erste auftaucht."
notice = "Der erste Chart wartet auf die Helm-Prüfung."
cost = "Ein weiterer Bauplan mit offener Prüfung; die Prüfung kann vom Server abweichen, wie die sechs Befunde zeigten."
undo = "reversible"
undo_how = "Später auf fertige Dateien umstellen."
contract = "B1c (#60) is built before the first HelmRelease: helm template renders each HelmRelease in the job, as #49 point 1 describes."

Activity

  1. added theissue type on Nov 6, 2026
  2. FlexOr2 commented on Nov 6, 2026

    @FlexOr2
    MemberAuthor

    Fact for P8 (songmaker's head, 06.11.2026): songmaker's release path (main 4a1cc8393) pushes three images under a nested path: ghcr.io/overnightworks/songmaker/<image>:<YYYY.MM.DD.N>, where <image> is web, music-worker or scoring-worker. An acestep worker follows later.

    B1's app-ref form and the admission layer's form (#50) know only ghcr.io/overnightworks/<repo>:<tag>@sha256:<digest>. Both must admit the nested form before songmaker's slice. Each attestor then names songmaker's repository for all of its images.

  3. FlexOr2 commented on Nov 6, 2026

    @FlexOr2
    MemberAuthor

    Journal, 06.11.2026 (fact from songmaker's head): songmaker's Release workflow (songmaker 9a3a8d59b, quality's workflows pinned at 31900fcd) pushes four images, ghcr.io/overnightworks/songmaker/{web,music-worker,scoring-worker,acestep-worker}:<YYYY.MM.DD.N>; acestep-worker is new and builds with its submodule. No release is dispatched yet; the first is the operator's to start. The attestor prefix must cover ghcr.io/overnightworks/songmaker/*, and platform's package Read grant covers all four packages.

  4. FlexOr2 commented on Nov 7, 2026

    @FlexOr2
    MemberAuthor

    Ruling of 07.11.2026 on line 2 (HelmReleases refused for good; every chart rendered by a shipped command into committed manifests under a regeneration gate), the operator in chat, option A. The head had asked whether HelmReleases stay refused for good or B1c (#60) is built later, named the cost (charts with install hooks do not fit) and the undo (B1c can still be built if a chart needs it), and recommended yes. His words, verbatim: „ja ?> wenn es besser ist und es profis so machen würden stimme ich zu! wir wollten ja nur bauen was wir müssen und auch kein theater machen! also es soll klappen logisch sein und dem standard folgenß“. The head's answer before recording: rendered manifests in Git are an established GitOps pattern (the rendered manifests pattern), so his condition holds. Line 1 (ruled C) is unchanged; this is a new line, not a re-rule.

  5. FlexOr2 commented on Nov 7, 2026

    @FlexOr2
    MemberAuthor

    The head revised done_when's last clause on 07.11.2026. It now reads "E1 green against hopin's first attested release and E15 green on an annotated tag".

    Why. hopin's first attested release, v2026.11.06.2, carries a lightweight tag. Its ref points straight at commit 127f26c, so no real hopin tag can exercise E15's peeling. #56 drove E15 on an annotated tag in a local bare clone, and E1 against the real release.

    Done. Every sentence of done_when is proven:

    The follow-ups in #65 change nothing the job refuses today, so #65 moves under #11 and this item closes.

  6. FlexOr2 commented on Nov 7, 2026

    @FlexOr2
    MemberAuthor

    B1a (#55, 0f4cdaf) and B1b (#56, f284a22) landed and every done_when sentence is proven (comment above). B1c (#60) is superseded by line 2. The follow-ups live in #65 under #11.

  7. FlexOr2 commented on Nov 7, 2026

    @FlexOr2
    MemberAuthor

    A gap for this list, from #81's review on 07.11.2026 (PR #85 at 569352b, follow-up). scripts/verify_image_release.sh now judges and counts a host-qualified image in every env map of ci.yml: the workflow's, each job's and each step's. Several image sources stay outside it:

    • env maps it does not read: jobs.<id>.container.env and services.<id>.env;
    • image fields it does not judge: jobs.<id>.container.image, services.<id>.image and uses: docker://… refs.

    Today's ci.yml has none of these, so nothing goes unjudged now. The first of them a workflow adds needs the selector extended in the same pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions