Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ jobs:
shellcheck -x scripts/write-publish-workflow-matchers.sh
shellcheck -x scripts/tests/test-publish-workflow-approved-revisions-guard.sh
shellcheck scripts/tests/test-write-publish-workflow-matchers.sh
shellcheck scripts/tests/test-publish-workflow-canonical-approvals.sh
shellcheck -x scripts/tests/test-publish-workflow-canonical-approvals.sh
bash scripts/tests/test-publish-workflow-approved-revisions-guard.sh
bash scripts/tests/test-write-publish-workflow-matchers.sh
bash scripts/tests/test-publish-workflow-canonical-approvals.sh
Expand Down Expand Up @@ -1220,8 +1220,10 @@ jobs:
# artifact (#3558). Unconditional, like the guards above.
- name: 🔐 Validate every devantler-tech OCIRepository verifies
run: |
shellcheck scripts/guard-oci-repository-verify.sh
shellcheck -x scripts/guard-oci-repository-verify.sh
shellcheck scripts/tests/test-guard-oci-repository-verify.sh
shellcheck scripts/tenant-source-constructors.lib.sh scripts/tests/test-tenant-source-constructors.sh
bash scripts/tests/test-tenant-source-constructors.sh
bash scripts/tests/test-guard-oci-repository-verify.sh
./scripts/guard-oci-repository-verify.sh

Expand Down
27 changes: 27 additions & 0 deletions docs/tenant-source-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ acceptance. Pages and the private portal remain unchanged.

## Observe before activating reconciliation

A generic `Tenant` can bootstrap only its verified source with
`spec.source.reconcileApplication: false`. This requires an exact source pair and
omits both application Kustomizations,
including the SOPS variant, rather than creating suspended children. Set
`spec.source.digest` and `spec.source.publisherRevision` together: the former is
an exact `sha256:` digest and the latter the reviewed maintained publisher's
40-character lowercase commit. Neither setting creates a consumer approval.
Publication evidence, approval generation and live source verification remain
separate requirements.

The production consumer-conservation guard currently rejects every rendered Tenant
instance because its generated source has no concrete per-consumer attribution in
the ordinary matcher inventory. The generic graph's syntax check does not authorize
any publisher revision. First-consumer onboarding must establish that attribution
and canonical approval before extending this closed boundary; Platform #5247 owns
the business-site registration. Neither source-only nor active instances are
production-admitted by this graph change alone.

Omitting the source object preserves the normal semver selector and existing
publisher policy. Omitting `source.reconcileApplication` preserves application reconciliation.
After verification, a source-only Tenant can change false to true. Admission
refuses true to false, including active objects stored before this field
existed: disabling an active tenant requires a separately reviewed lifecycle
procedure, not pruning its workloads through this bootstrap switch.
Once an exact source is selected, removing its pin is refused; activation cannot
silently change the observed publication into a floating version range.

After the reviewed source is deployed, confirm `Ready=True`, `SourceVerified=True`
and a current `observedGeneration`. Its `status.artifact.revision` must equal the
declared manifest digest. `status.artifact.digest` is a different identity: Flux's
Expand Down
10 changes: 9 additions & 1 deletion docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ CI path filters.

## WebApp compiler

Run the committed WebApp graph through the pinned KRO compiler:
Run the committed WebApp and Tenant graphs through the pinned KRO compiler:

```bash
bash scripts/tests/test-webapp-compiler.sh
bash scripts/tests/test-webapp-compiler-wiring.sh
```

Tenant controls compile the complete graph and submit its generated API schema
to the real API server. They cover source-only creation, paired digest and
publisher inputs, unchanged omitted defaults, later application activation and
refusal to disable active tenants, including objects stored under the earlier
schema. These checks do not prove a live controller has reconciled a tenant.
`scripts/tests/test-tenant-source-constructors.sh` separately exercises the actual
publication guards against constrained constructors and mutated graph contexts.

The fixture downloads checksum-verified Kubernetes 1.36.2 API-server assets and
starts a disposable, loopback-only API server. It explicitly refuses an existing
cluster. The KRO 0.9.2 CRD applies the actual graph-admission defaults; the Gateway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ spec:
# parent field entirely, but `has()` on a whole optional object is not
# exercised anywhere else in this file.
types:
TenantSource:
digest: 'string | default="" maxLength=71 pattern="^$|^sha256:[0-9a-f]{64}$"'
publisherRevision: 'string | default="" maxLength=40 pattern="^$|^[0-9a-f]{40}$"'
# An active child cannot be removed through this bootstrap control.
reconcileApplication: 'boolean | default=true validation="self || !oldSelf"'
TenantResourceQuota:
enabled: boolean | default=false
# Total across all pods in the namespace (both request AND limit —
Expand Down Expand Up @@ -103,6 +108,14 @@ spec:
# Opt-in own-namespace WebApp reconciliation. Admission also binds the
# parent and generated child ownership; no existing tenant opts in here.
webApp: boolean | default=false
# Digest-addressed bootstrap uses an exact maintained publisher revision.
# This checks syntax, not consumer approval. The mandatory production
# conservation guard refuses all Tenant instances until reviewed consumer
# onboarding adds exact attribution and approval; this graph grants none.
# Keep lifecycle and pinning in one validation scope. Source-only mode
# requires an exact publication; an established pin cannot become a range.
source: >-
TenantSource | default={} validation="((!has(self.digest) || self.digest == '') == (!has(self.publisherRevision) || self.publisherRevision == '')) && (!has(self.reconcileApplication) || self.reconcileApplication || (has(self.digest) && self.digest != ''))" validation="!has(oldSelf.digest) || oldSelf.digest == '' || (has(self.digest) && self.digest != '')"
# Standard multi-tenancy guardrails (#1932 follow-up #2522): a
# namespace-wide ResourceQuota and a per-container default LimitRange.
# Both default to `enabled: false` — no behaviour change for existing
Expand Down Expand Up @@ -279,8 +292,7 @@ spec:
app.kubernetes.io/managed-by: ksail
spec:
interval: 1m
ref:
semver: ">=1.0.0"
ref: '${schema.spec.source.digest == "" ? {"semver": ">=1.0.0"} : {"digest": schema.spec.source.digest}}'
url: oci://ghcr.io/devantler-tech/${schema.spec.name}/manifests
secretRef:
name: ghcr-auth
Expand All @@ -298,7 +310,7 @@ spec:
# identities are not supported at this time"), so any future
# alternation must stay inside one subject regex.
- issuer: '^https://token\.actions\.githubusercontent\.com$'
subject: '^https://github\.com/devantler-tech/(actions/\.github/workflows/publish-app\.yaml@[0-9a-f]{40}|\.github/\.github/workflows/publish-app\.yaml@(0c7b6c3509b73adbc7537e030935bb01987e625b|467f07582436a26b62cf4aa756423512f85ba0c0))$'
subject: '${schema.spec.source.publisherRevision == "" ? "^https://github\\.com/devantler-tech/(actions/\\.github/workflows/publish-app\\.yaml@[0-9a-f]{40}|\\.github/\\.github/workflows/publish-app\\.yaml@(0c7b6c3509b73adbc7537e030935bb01987e625b|467f07582436a26b62cf4aa756423512f85ba0c0))$" : "^https://github\\.com/devantler-tech/\\.github/\\.github/workflows/publish-app\\.yaml@" + schema.spec.source.publisherRevision + "$"}'
Comment thread
devantler marked this conversation as resolved.
# --- Flux Kustomization (one of two variants by `sops`) ---------------
# The impersonating Kustomization that reconciles the tenant's own artifact
# under its namespaced SA (bound to the `tenant-edit` role). Two variants because the SOPS `decryption`
Expand All @@ -307,7 +319,7 @@ spec:
# on a tenant with no `sops-age` Secret would wedge reconciliation.
- id: kustomization
includeWhen:
- ${!schema.spec.sops}
- ${schema.spec.source.reconcileApplication && !schema.spec.sops}
template:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
Expand Down Expand Up @@ -344,7 +356,7 @@ spec:
targetNamespace: ${tenantNamespace.metadata.name}
- id: kustomizationSops
includeWhen:
- ${schema.spec.sops}
- ${schema.spec.source.reconcileApplication && schema.spec.sops}
template:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
Expand Down
11 changes: 11 additions & 0 deletions scripts/guard-oci-repository-verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly REPO_ROOT
# shellcheck source=scripts/tenant-source-constructors.lib.sh
source "$REPO_ROOT/scripts/tenant-source-constructors.lib.sh"
readonly REQUIRED_URL_PREFIX='oci://ghcr.io/devantler-tech/'
# The identity every devantler-tech artifact is signed under: GitHub Actions keyless
# signing, with a subject regex anchored on a devantler-tech GitHub path. Compared as
Expand Down Expand Up @@ -297,6 +299,15 @@ while IFS= read -r root; do
*) continue ;;
esac
in_scope=$((in_scope + 1))
# Only the complete constrained Tenant profile may construct these values.
# Validate its supplied branch too, before judging the unchanged default.
if [[ "$url" == "$TEMPLATED_TENANT_STREAM_URL" && "$ref_type" == '!!str' ]]; then
if ! subject="$(tenant_source_default_subject "$work/render.yaml" "$subject")"; then
fail "$label: invalid Tenant source constructor"
continue
fi
ref_type='!!map'; ref_key_count=1; ref_semver="$REQUIRED_TENANT_STREAM_SEMVER"
fi
case "$url" in
"$WEDDING_STREAM_URL" | "$ASCOACHING_STREAM_URL" | "$TEMPLATED_TENANT_STREAM_URL")
if [ "$ref_type" != "!!map" ] || [ "$ref_key_count" -ne 1 ] || [ "$ref_semver" != "$REQUIRED_TENANT_STREAM_SEMVER" ]; then
Expand Down
20 changes: 19 additions & 1 deletion scripts/guard-shared-publish-workflow-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ readonly REPO_ROOT
# Either publisher family, with an optional group opener (#4502), so a two-family subject
# and a canonical-only one are FOUND and judged rather than left outside the scan. Kept
# textually parallel to FAMILY_SUBJECT_PATTERN in publish-workflow-approved-revisions.lib.sh.
readonly SUBJECT_PATTERN='(subject|subjectRegex|subjectRegExp):[[:space:]]*.?\^?https://github\\?\.com/devantler-tech/[(]?(actions|\\?\.github)/\\?\.github/workflows/publish-(app|manifests)\\?\.yaml@'
readonly SUBJECT_PATTERN='(subject|subjectRegex|subjectRegExp):[[:space:]]*(.?\^?https://github\\?\.com/devantler-tech/[(]?(actions|\\?\.github)/\\?\.github/workflows/publish-(app|manifests)\\?\.yaml@|.?[$][{]schema[.]spec[.]source[.]publisherRevision)'

# The workflow identity of either family with no key name, no organisation prefix and no
# line structure, for the independent discovery below. The canonical half of a two-family
Expand Down Expand Up @@ -735,6 +735,24 @@ EOF
continue
fi

# shellcheck disable=SC2016 # Match the literal CEL expression opener.
if [[ "$subject" == '${'* ]]; then
if [[ ! -r "$REPO_ROOT/scripts/tenant-source-constructors.lib.sh" ]]; then
printf '%s: invalid Tenant source constructor: reader is missing\n' "$location" >&2
status=1
continue
fi
# shellcheck source=scripts/tenant-source-constructors.lib.sh
source "$REPO_ROOT/scripts/tenant-source-constructors.lib.sh"
if ! subject="$(tenant_source_default_subject "${location%:*}" "$subject")"; then
printf '%s: invalid Tenant source constructor\n' "$location" >&2
status=1
continue
fi
judge_family_subject "$location" "$subject" || status=1
continue
fi

# A TWO-FAMILY SUBJECT IS READ BY ITS OWN STRICT PARSE (#4502). It carries two `@` by
# design, so it is dispatched before the single-identity rule below rather than by
# relaxing that rule: every other subject is still held to exactly one.
Expand Down
6 changes: 3 additions & 3 deletions scripts/rgd-template-static-scan-baseline.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# The parameterized WebApp identities are additionally scanned after actual KRO resolution by
# test-webapp-compiled-scan.sh: omitted defaults still report writable root and UID/GID <= 10000.
# Disappearing low-ID rows below are not clearance for those compatibility defaults.
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml RGD-CONTENT SHA256 4b4f077ffee9276ede0339daf8e1a5e2f8adeb7d8c8705aabc3e872393c855b0
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml RGD-CONTENT SHA256 ce3d2ed0939787279638da98c3d8fe13fb7fd1bee4d6639bc6d7e699053bb308
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/defaultDeny.yaml KSV-0039 LOW CAUSE-SHA256:6246c5e7ede528806f1f84a2b466b1a22c061b7d7f2ad38e18dc4c722ea7b32b
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/defaultDeny.yaml KSV-0040 LOW CAUSE-SHA256:6246c5e7ede528806f1f84a2b466b1a22c061b7d7f2ad38e18dc4c722ea7b32b
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ghcrAuth.yaml KSV-0039 LOW CAUSE-SHA256:f3c7ced815c76aaba6407cf691461a7e46b0037f888f32d760286b0fde1bd286
Expand All @@ -26,8 +26,8 @@
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/kustomizationSops.yaml KSV-0040 LOW CAUSE-SHA256:59d21ae5decd83ec9c1e66036a71db7aa29ca574b4e3ef0fb803a8b45d7fac61
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/limitRange.yaml KSV-0039 LOW CAUSE-SHA256:c37b17db55af250264e52c9089e25decadde834c9ebae540db73774a4f2b5702
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/limitRange.yaml KSV-0040 LOW CAUSE-SHA256:c37b17db55af250264e52c9089e25decadde834c9ebae540db73774a4f2b5702
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0039 LOW CAUSE-SHA256:09f3a16081e9973d851ad50ebed089365dc8d9832c2d4f7ecffbe3199d93cff7
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0040 LOW CAUSE-SHA256:09f3a16081e9973d851ad50ebed089365dc8d9832c2d4f7ecffbe3199d93cff7
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0039 LOW CAUSE-SHA256:c0b63be11d05b36a13e5e0d4305b8dacc1a7c02e7e7c1308122ccd2f650343c2
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0040 LOW CAUSE-SHA256:c0b63be11d05b36a13e5e0d4305b8dacc1a7c02e7e7c1308122ccd2f650343c2
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/resourceQuota.yaml KSV-0039 LOW CAUSE-SHA256:9e7f4d1bd690474236d5c0a694dce8767ed63f3b41bd94a1741ff5a8d072a8a4
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml RGD-CONTENT SHA256 d72726a713827e5fb8091793ea3411f3bfb4dd5136488c11ba6e5ae9dd23a9a1
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0013 MEDIUM CAUSE-SHA256:c801aedba500c27744aa5111215741ee7a4703a4968fb26c3e42a1fd33423767
Expand Down
43 changes: 43 additions & 0 deletions scripts/tenant-source-constructors.lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# The only dynamic publisher/selector profile admitted by the generic Tenant.
# This reads both exact constructors and their complete schema input domain;
# it never evaluates arbitrary CEL or grants a consumer publisher approval.
tenant_source_default_subject() (
set -o pipefail
local file="$1" observed="${2:-}" ref expected_subject default_subject source_type digest_type publisher_type lifecycle
# shellcheck disable=SC2016 # Literal CEL, never shell expansion.
ref='${schema.spec.source.digest == "" ? {"semver": ">=1.0.0"} : {"digest": schema.spec.source.digest}}'
# shellcheck disable=SC2016 # Literal CEL, never shell expansion.
expected_subject='${schema.spec.source.publisherRevision == "" ? "^https://github\\.com/devantler-tech/(actions/\\.github/workflows/publish-app\\.yaml@[0-9a-f]{40}|\\.github/\\.github/workflows/publish-app\\.yaml@(0c7b6c3509b73adbc7537e030935bb01987e625b|467f07582436a26b62cf4aa756423512f85ba0c0))$" : "^https://github\\.com/devantler-tech/\\.github/\\.github/workflows/publish-app\\.yaml@" + schema.spec.source.publisherRevision + "$"}'
default_subject='^https://github\.com/devantler-tech/(actions/\.github/workflows/publish-app\.yaml@[0-9a-f]{40}|\.github/\.github/workflows/publish-app\.yaml@(0c7b6c3509b73adbc7537e030935bb01987e625b|467f07582436a26b62cf4aa756423512f85ba0c0))$'
source_type="TenantSource | default={} validation=\"((!has(self.digest) || self.digest == '') == (!has(self.publisherRevision) || self.publisherRevision == '')) && (!has(self.reconcileApplication) || self.reconcileApplication || (has(self.digest) && self.digest != ''))\" validation=\"!has(oldSelf.digest) || oldSelf.digest == '' || (has(self.digest) && self.digest != '')\""
digest_type='string | default="" maxLength=71 pattern="^$|^sha256:[0-9a-f]{64}$"'
publisher_type='string | default="" maxLength=40 pattern="^$|^[0-9a-f]{40}$"'
lifecycle='boolean | default=true validation="self || !oldSelf"'
if [[ -n "$observed" && "$observed" != "$expected_subject" ]]; then
printf 'invalid Tenant source constructor: unexpected subject\n' >&2
return 1
fi
if ! yq -o=json -I=0 '.' "$file" | jq -se \
--arg ref "$ref" --arg subject "$expected_subject" --arg source_type "$source_type" \
--arg digest_type "$digest_type" --arg publisher_type "$publisher_type" --arg lifecycle "$lifecycle" '
[ .[] | select(.apiVersion == "kro.run/v1alpha1" and .kind == "ResourceGraphDefinition" and .metadata.name == "tenant.kro.run") ] as $graphs
| ($graphs | length) == 1
and ($graphs[0].spec.schema | .apiVersion == "v1alpha1" and .kind == "Tenant" and .scope == "Namespaced"
and .types.TenantSource == {digest: $digest_type, publisherRevision: $publisher_type, reconcileApplication: $lifecycle}
and .spec.source == $source_type and (.spec | has("reconcileApplication") | not))
and ([$graphs[0].spec.resources[] | select(.id == "ociRepository")] | length) == 1
and ([$graphs[0].spec.resources[] | select(.id == "ociRepository")][0].template |
.apiVersion == "source.toolkit.fluxcd.io/v1" and .kind == "OCIRepository"
and .metadata.name == "${schema.spec.name}" and .metadata.namespace == "${tenantNamespace.metadata.name}"
and .spec.url == "oci://ghcr.io/devantler-tech/${schema.spec.name}/manifests"
and .spec.ref == $ref and .spec.verify == {provider: "cosign", matchOIDCIdentity: [{
issuer: "^https://token\\.actions\\.githubusercontent\\.com$", subject: $subject
}]})
and ([.[] | .. | objects | select(has("subject")) | select(.subject == $subject)] | length) == 1
' >/dev/null; then
printf 'invalid Tenant source constructor: graph context, paired domain or constructor differs\n' >&2
return 1
fi
printf '%s\n' "$default_subject"
)
Loading
Loading