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: 6 additions & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ jobs:
with:
persist-credentials: false

- name: ⚙️ Setup Go for WebApp input validation
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false

- name: ⚙️ Setup Trivy for nested RGD templates
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
Expand Down
43 changes: 41 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
name: 🧩 Compile WebApp with pinned KRO
needs: [changes]
if: needs.changes.outputs.k8s == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
runs-on: ubuntu-24.04
timeout-minutes: 25
permissions:
contents: read
steps:
Expand All @@ -35,8 +35,27 @@ jobs:
run: |
go install github.com/mikefarah/yq/v4@v4.54.1
printf '%s\n' "$GOBIN" >> "$GITHUB_PATH"
- name: 🔒 Validate confined WebApp source inputs
run: |
shellcheck scripts/validate-webapp-inputs.sh scripts/tests/test-webapp-inputs.sh
bash scripts/tests/test-webapp-inputs.sh
- name: 🧩 Compile the committed WebApp and refusal controls
run: bash scripts/tests/test-webapp-compiler.sh
- name: ⚙️ Install pinned Trivy for compiled workloads
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
cache: true
- name: 🔬 Scan actual compiled identities and defaults
run: |
shellcheck scripts/tests/test-webapp-compiled-scan.sh
bash scripts/tests/test-webapp-compiled-scan.sh
- name: 🔏 Install reviewed public-image verification tools
run: bash .github/scripts/setup-supply-chain-tools.sh
- name: 🧪 Exercise the real controller and restricted public application
run: |
shellcheck scripts/tests/test-webapp-runtime-hosted.sh
bash scripts/tests/test-webapp-runtime-hosted.sh

# Full suites produce no path outputs. Run them independently, but require
# their success before production access and the final merge verdict.
Expand Down Expand Up @@ -130,7 +149,14 @@ jobs:
- '.github/scripts/setup-ksail.sh'
- '.github/scripts/kyverno-version.sh'
- 'scripts/tests/test-webapp-container-security.sh'
- 'scripts/validate-webapp-inputs.sh'
- 'scripts/validate-webapp-inputs/**'
- 'scripts/webapp-inputs.jq'
- 'scripts/tests/test-webapp-inputs.sh'
- 'scripts/tests/test-webapp-inputs-wiring.sh'
- 'scripts/tests/test-webapp-compiler.sh'
- 'scripts/tests/test-webapp-runtime-hosted.sh'
- 'scripts/tests/test-webapp-compiled-scan.sh'
- 'scripts/tests/test-webapp-compiler-wiring.sh'
- 'scripts/tests/webapp-compiler/**'
- 'scripts/guard-cilium-sni/**'
Expand Down Expand Up @@ -3456,6 +3482,19 @@ jobs:
id: webapp-kyverno-version
run: bash .github/scripts/kyverno-version.sh

- name: ⚙️ Setup pinned compiler for resolved WebApp admission
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: scripts/tests/webapp-compiler/go.mod
cache: false

- name: 📌 Install reviewed YAML parser for resolved WebApp admission
env:
GOBIN: ${{ runner.temp }}/webapp-admission-tools
run: |
go install github.com/mikefarah/yq/v4@v4.54.1
printf '%s\n' "$GOBIN" >> "$GITHUB_PATH"

- name: ⚖️ Install Kyverno CLI for WebApp admission
uses: kyverno/action-install-cli@fcee92fca5c883169ef9927acf543e0b5fc58289 # v0.2.0
with:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/dr-rebuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ jobs:
with:
persist-credentials: false

- name: ⚙️ Setup Go for WebApp input validation
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false

- name: ⚙️ Setup Trivy
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
Expand Down
29 changes: 27 additions & 2 deletions .github/workflows/validate-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ concurrency:
jobs:
validate-webapp-compiler:
name: 🧩 Compile WebApp with pinned KRO
runs-on: ubuntu-latest
timeout-minutes: 15
runs-on: ubuntu-24.04
timeout-minutes: 25
permissions:
contents: read
steps:
Expand All @@ -59,8 +59,27 @@ jobs:
run: |
go install github.com/mikefarah/yq/v4@v4.54.1
printf '%s\n' "$GOBIN" >> "$GITHUB_PATH"
- name: 🔒 Validate confined WebApp source inputs
run: |
shellcheck scripts/validate-webapp-inputs.sh scripts/tests/test-webapp-inputs.sh
bash scripts/tests/test-webapp-inputs.sh
- name: 🧩 Compile the committed WebApp and refusal controls
run: bash scripts/tests/test-webapp-compiler.sh
- name: ⚙️ Install pinned Trivy for compiled workloads
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.74.0
cache: true
- name: 🔬 Scan actual compiled identities and defaults
run: |
shellcheck scripts/tests/test-webapp-compiled-scan.sh
bash scripts/tests/test-webapp-compiled-scan.sh
- name: 🔏 Install reviewed public-image verification tools
run: bash .github/scripts/setup-supply-chain-tools.sh
- name: 🧪 Exercise the real controller and restricted public application
run: |
shellcheck scripts/tests/test-webapp-runtime-hosted.sh
bash scripts/tests/test-webapp-runtime-hosted.sh

validate-eks-authorization:
name: 🔐 Validate Production Authorization
Expand Down Expand Up @@ -229,6 +248,12 @@ jobs:
with:
persist-credentials: false

- name: ⚙️ Setup Go for WebApp input validation
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false

- name: ⚙️ Setup Trivy for nested RGD templates
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
Expand Down
51 changes: 51 additions & 0 deletions docs/tenant-abstraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,57 @@ customer-facing routing is introduced by this foundation. Private-input
projection (#4854) and the business repository's signed manifest (#102 there)
remain separate prerequisites.

### Confined public runtime inputs

WebApp accepts opt-in literal arguments (at most 32, each at most 256 UTF-8
bytes), a non-root UID and primary GID (1–2147483647), a read-only-root option,
and at most one same-namespace Secret projection. The projection selects 1–8
unique keys and unique single-component destinations. Its directory is fixed
at `/run/webapp-inputs`, with required input, mode `0440` and read-only mounting;
there is no path, namespace, executable, environment or arbitrary volume override.
Arguments reject NUL and Kubernetes `$(...)` expansion; they must contain no
credentials. Secret values remain outside source, images, arguments and receipts.
The WebApp writer chooses the same-namespace Secret reference. This is workload
authority: its chosen image can consume selected keys even without Secret API
`get` permission. Grant WebApp editing only to trusted tenant workload writers;
do not place higher-privilege shared secrets in that tenant namespace. The fixed
directory and selected keys are confinement, not a new secret-authorization boundary.

Omitted inputs preserve empty argument/mount/volume lists, UID/GID 1000 and the
existing writable root. The Pod now explicitly derives its file group from its
primary GID, including that compatibility default. Current pilots have no graph
volumes and the normal platform mutation already supplies group 1000, but this
is not a byte-identical Pod template: existing instances may roll on reconciliation.
Do not generalize that permissions are unchanged for injected or future volumes.
The restricted business image opts into UID/GID 65532 and read-only root, so a
root-owned projected file is readable by exactly its non-root application group.
The public disclosure is an immutable startup snapshot: rotation requires a restart.

The platform source scanner refuses unknown or malformed WebApp spec fields
before sealing committed-instance evidence. The existing strict JSON parser
rejects duplicate members before jq can keep a last value; the maintained YAML
decoder also checks the original source before conversion. Complete inputs are
bounded to 1 MiB, with at most 1024 raw YAML documents. This is the platform's committed
source gate, not proof of a future tenant publisher's input validation. The actual
KRO 0.9.2 compiler/generated API tests admission, defaults, literal arguments and
resolved children. Resolved deployments are separately scanned with Trivy 0.74.0;
the writable-root and low-ID compatibility findings remain explicit rather than
disappearing behind expressions. Both resolved states pass the actual container
security policy, whose missing/false/string controls remain denied.

The required hosted fixture uses a disposable kind node (Kubernetes 1.36.4,
not the compiler fixture's 1.36.2 API server), one pinned KRO controller and the
signed source-owned public business image. It tests controller input transitions,
real process identity, kubelet atomic projection permissions, fictional EN/DA
disclosure and closed private routes. It has no platform credentials, gateway,
production policies, database or identity provider. Passing it is not production
ingress, tenant authorization, portal activation or recovery proof. The script
refuses execution outside an isolated GitHub-hosted Linux runner.
Removing required disclosure changes the template but does not revoke an already
healthy Pod: the zero-unavailable rolling strategy can retain it while replacement
startup fails. The fixture explicitly scales to zero before cleanup; this is not
a new production revocation mechanism.

**Positive** — onboarding a tenant is one small typed CR; the drift is gone; the abstraction is typed
and `kubectl`-native with CRD-schema validation; no Helm/overlay stacking; the isolation posture is
templated verbatim.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
#
# Lands in the `infrastructure` layer alongside the Tenant RGD (a CR of an
# RGD-defined kind cannot share a Flux Kustomization with the controller that
# installs its CRD). Pre-1.0 KRO (v1alpha1): only scalar schema fields with
# defaults (the proven Tenant-RGD subset, which reconciled live); `WebApp`
# instances arrive with the pilot migration, not here.
# installs its CRD). Optional runtime inputs default to no arguments or file
# projections. Their actual generated API and resolved Deployment are exercised
# by scripts/tests/test-webapp-compiler.sh against the pinned KRO release.
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
Expand All @@ -72,6 +72,18 @@ spec:
apiVersion: v1alpha1
kind: WebApp
scope: Namespaced
types:
Argument: >-
string | maxLength=256
validation="size(bytes(self)) <= 256 && !self.contains('\u0000') && !self.contains('$(')"
ProjectionItem:
key: 'string | required=true minLength=1 maxLength=253 pattern="^[-._a-zA-Z0-9]+$"'
path: 'string | required=true minLength=1 maxLength=64 pattern="^[A-Za-z0-9][A-Za-z0-9._-]*$"'
SecretProjection:
secretName: 'string | required=true minLength=1 maxLength=253 pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"'
items: >-
[]ProjectionItem | required=true minItems=1 maxItems=8 listType=atomic
validation="self.all(x, self.filter(y, y.key == x.key).size() == 1 && self.filter(y, y.path == x.path).size() == 1)"
spec:
# App name — used verbatim as the Deployment/Service/HTTPRoute name AND as
# the target namespace (the tenant namespace the Tenant archetype created).
Expand All @@ -98,6 +110,14 @@ spec:
memoryRequest: string | default="16Mi"
cpuLimit: string | default="100m"
memoryLimit: string | default="64Mi"
# Empty/default lists preserve the executable's normal startup. Arguments
# are nonsecret literal values; Kubernetes interpolation is not admitted.
args: '[]Argument | default=[] maxItems=32 listType=atomic'
runAsUser: integer | default=1000 minimum=1 maximum=2147483647
runAsGroup: integer | default=1000 minimum=1 maximum=2147483647
readOnlyRootFilesystem: boolean | default=false
# One same-namespace directory projection; no generic paths or modes.
secretProjections: '[]SecretProjection | default=[] maxItems=1 listType=atomic'
resources:
- id: deployment
template:
Expand Down Expand Up @@ -133,13 +153,17 @@ spec:
# `pod-security.kubernetes.io/enforce: restricted`, so the pod ships
# a compliant security context (Kyverno mutate is additive and
# idempotent over these — it sets the same values, no conflict).
# Matches both pilots (non-root uid/gid 1000).
# Defaults match both pilots. The file group is always the primary
# process group, never an independently granted supplemental group.
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
runAsUser: ${schema.spec.runAsUser}
runAsGroup: ${schema.spec.runAsGroup}
fsGroup: ${schema.spec.runAsGroup}
seccompProfile:
type: RuntimeDefault
volumes: >-
${schema.spec.secretProjections.map(p, {"name": "webapp-inputs", "secret": {"secretName": p.secretName, "defaultMode": 288, "optional": false, "items": p.items.map(i, {"key": i.key, "path": i.path})}})}
# Spread replicas across nodes (soft) so a single node failure can't
# take out the whole app; ScheduleAnyway keeps single-node local/CI
# clusters schedulable — matches both pilots + the operator tier.
Expand All @@ -153,6 +177,9 @@ spec:
containers:
- name: ${schema.spec.name}
image: ${schema.spec.image}
args: ${schema.spec.args}
volumeMounts: >-
${schema.spec.secretProjections.map(p, {"name": "webapp-inputs", "mountPath": "/run/webapp-inputs", "readOnly": true})}
ports:
- containerPort: ${schema.spec.port}
protocol: TCP
Expand Down Expand Up @@ -180,7 +207,7 @@ spec:
# explicitly; Pod-level inheritance is not sufficient.
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
readOnlyRootFilesystem: ${schema.spec.readOnlyRootFilesystem}
capabilities:
drop:
- ALL
Expand Down
15 changes: 8 additions & 7 deletions scripts/rgd-template-static-scan-baseline.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# change; a new finding is fixed, not baselined.
# Measured with Trivy v0.74.0 and the policies embedded in that binary. CI pins the binary on pull
# requests, merge groups, and direct main pushes; the gate rejects other versions and policy updates.
# 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 b196fbaff4273d0ec97cd0276f2e83ca9ce5f77653530864c2f7694c179e0a57
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
Expand All @@ -26,13 +29,11 @@
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0039 LOW CAUSE-SHA256:4fe5cbcec3db4deae608c8886a3d32ac071dcb524452e543555752b464f5cb26
1 k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml.resources/ociRepository.yaml KSV-0040 LOW CAUSE-SHA256:4fe5cbcec3db4deae608c8886a3d32ac071dcb524452e543555752b464f5cb26
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 e5181058d728bcceeb61fce7576c8e61f9dc2f3ea9752b2c30dcbbe2355cc74f
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0013 MEDIUM CAUSE-SHA256:ddddc30da09d767c23469bcc78ffa3e67c4a1361d51cd0d49b49155bc146dae7
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0014 HIGH CAUSE-SHA256:ddddc30da09d767c23469bcc78ffa3e67c4a1361d51cd0d49b49155bc146dae7
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0020 LOW CAUSE-SHA256:ddddc30da09d767c23469bcc78ffa3e67c4a1361d51cd0d49b49155bc146dae7
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0021 LOW CAUSE-SHA256:ddddc30da09d767c23469bcc78ffa3e67c4a1361d51cd0d49b49155bc146dae7
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0039 LOW CAUSE-SHA256:a712a1729d95036da96d81973ff120f2766971f7af6230993576482575b6406d
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0040 LOW CAUSE-SHA256:a712a1729d95036da96d81973ff120f2766971f7af6230993576482575b6406d
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
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0014 HIGH CAUSE-SHA256:c801aedba500c27744aa5111215741ee7a4703a4968fb26c3e42a1fd33423767
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0039 LOW CAUSE-SHA256:3b02ea87d49b53e5ffa05c9030f986e8a0b9a7e5db7b239273e420ed33a2e59b
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/deployment.yaml KSV-0040 LOW CAUSE-SHA256:3b02ea87d49b53e5ffa05c9030f986e8a0b9a7e5db7b239273e420ed33a2e59b
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/httpRoute.yaml KSV-0039 LOW CAUSE-SHA256:b79642b9e85bb986f184b3b83639e109382bdd5d4a318a8390619bf03e16a0cc
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/httpRoute.yaml KSV-0040 LOW CAUSE-SHA256:b79642b9e85bb986f184b3b83639e109382bdd5d4a318a8390619bf03e16a0cc
1 k8s/bases/infrastructure/resource-graph-definitions/webapp/resource-graph-definition.yaml.resources/service.yaml KSV-0039 LOW CAUSE-SHA256:e59d76fdca878919309a3203c94916d7258f052f5ac4012683c70a6307f16e07
Expand Down
Loading
Loading