diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 455b4149..8a483cbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,6 +103,37 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Guard — injected AuthBridge images must be version-pinned (no floating tags) + # release.yml pins the controller-manager image (below) but NOT the cortex-built + # AuthBridge injection images; they were shipping at :latest (rossoctl/rossoctl#508). + # Positive assertion (tighter than a deny-list, which passes untagged/:dev/etc): + # every injection image must carry a :vX.Y.Z tag — in BOTH the chart values and + # the compiled Go fallbacks (config/defaults.go), which loader.go overlays the + # platform-config ConfigMap on top of, so a no-ConfigMap deploy (kustomize + # `make deploy`, webhook.enable=false) would otherwise still inject :latest. + # This is a release-time backstop; the same check runs per-PR in security-scans.yaml. + run: | + set -euo pipefail + fail=0 + for k in envoyProxy authbridge authbridgeLite proxyInit; do + img=$(yq ".defaults.images.$k" ${{ env.CHARTS_PATH }}/operator/values.yaml) + if [[ ! "$img" =~ :v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::defaults.images.$k not version-pinned in values.yaml: $img" + fail=1 + fi + done + while IFS= read -r img; do + if [[ ! "$img" =~ :v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::compiled default not version-pinned in config/defaults.go: $img" + fail=1 + fi + done < <(grep -oE 'ghcr\.io/rossoctl/cortex/[a-z-]+:[^"]+' operator/internal/webhook/config/defaults.go) + if [ "$fail" -ne 0 ]; then + echo "::error::pin the flagged AuthBridge injection image(s) to a cortex release tag before releasing (rossoctl/rossoctl#508)" + exit 1 + fi + echo "AuthBridge injection images are version-pinned OK (values.yaml + config/defaults.go)" + - name: Package and push Helm chart run: | chartVersion=$(echo "${{ github.ref_name }}" | cut -c 2-) diff --git a/.github/workflows/security-scans.yaml b/.github/workflows/security-scans.yaml index 61f2cdc8..38c83782 100644 --- a/.github/workflows/security-scans.yaml +++ b/.github/workflows/security-scans.yaml @@ -150,6 +150,37 @@ jobs: fi done + - name: Install yq + uses: mikefarah/yq@1b9b4ac5187171d2e5e3129be0cfa827c7f9d53d # v4 + + - name: Guard — AuthBridge injection images must be version-pinned (no floating tags) + # Catches a :latest / floating-tag regression at PR-review time rather than + # mid-release (rossoctl/rossoctl#508). Positive assertion: every injection image + # must carry a :vX.Y.Z tag, in BOTH the chart values and the compiled Go fallbacks + # (config/defaults.go, which the platform-config ConfigMap overlays on top of — a + # no-ConfigMap deploy would otherwise still inject :latest). + run: | + set -euo pipefail + fail=0 + for k in envoyProxy authbridge authbridgeLite proxyInit; do + img=$(yq ".defaults.images.$k" charts/operator/values.yaml) + if [[ ! "$img" =~ :v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error file=charts/operator/values.yaml::defaults.images.$k not version-pinned: $img" + fail=1 + fi + done + while IFS= read -r img; do + if [[ ! "$img" =~ :v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error file=operator/internal/webhook/config/defaults.go::compiled default not version-pinned: $img" + fail=1 + fi + done < <(grep -oE 'ghcr\.io/rossoctl/cortex/[a-z-]+:[^"]+' operator/internal/webhook/config/defaults.go) + if [ "$fail" -ne 0 ]; then + echo "::error::pin the flagged AuthBridge injection image(s) to a cortex release tag (rossoctl/rossoctl#508)" + exit 1 + fi + echo "AuthBridge injection images are version-pinned OK (values.yaml + config/defaults.go)" + # ============================================================================ # Phase B: Container/IaC Security # ============================================================================ diff --git a/charts/operator/values.yaml b/charts/operator/values.yaml index e266c949..7d2551ab 100644 --- a/charts/operator/values.yaml +++ b/charts/operator/values.yaml @@ -246,10 +246,10 @@ featureGates: # proxy-sidecar / lite mode (always-on enforce-redirect egress capture). defaults: images: - envoyProxy: ghcr.io/rossoctl/cortex/authbridge-envoy:latest - authbridge: ghcr.io/rossoctl/cortex/authbridge:latest - authbridgeLite: ghcr.io/rossoctl/cortex/authbridge-lite:latest - proxyInit: ghcr.io/rossoctl/cortex/proxy-init:latest + envoyProxy: ghcr.io/rossoctl/cortex/authbridge-envoy:v0.7.0-alpha.3 + authbridge: ghcr.io/rossoctl/cortex/authbridge:v0.7.0-alpha.3 + authbridgeLite: ghcr.io/rossoctl/cortex/authbridge-lite:v0.7.0-alpha.3 + proxyInit: ghcr.io/rossoctl/cortex/proxy-init:v0.7.0-alpha.3 pullPolicy: IfNotPresent # Proxy settings diff --git a/operator/internal/webhook/config/defaults.go b/operator/internal/webhook/config/defaults.go index 87cb0d9e..79187e12 100644 --- a/operator/internal/webhook/config/defaults.go +++ b/operator/internal/webhook/config/defaults.go @@ -29,20 +29,25 @@ func CompiledDefaults() *PlatformConfig { // Compiled defaults are overridden at runtime by the platform-config // ConfigMap (rossoctl-platform-config). These serve as fallbacks only. Images: ImageConfig{ + // Keep in sync with charts/operator/values.yaml (defaults.images.*). + // These compiled fallbacks are used when the platform-config ConfigMap + // is absent (e.g. kustomize `make deploy`, or webhook.enable=false), so + // they must be version-pinned too — an unpinned fallback would inject + // :latest on exactly the deploy paths the chart-layer pin doesn't cover. // authbridge-envoy: combined image for envoy-sidecar mode // (Envoy + ext_proc authbridge + spiffe-helper bundled). - EnvoyProxy: "ghcr.io/rossoctl/cortex/authbridge-envoy:latest", + EnvoyProxy: "ghcr.io/rossoctl/cortex/authbridge-envoy:v0.7.0-alpha.3", // authbridge: combined image for proxy-sidecar mode (default // deployment shape) — authbridge-proxy + spiffe-helper // bundled, no Envoy, no gRPC. - AuthBridge: "ghcr.io/rossoctl/cortex/authbridge:latest", + AuthBridge: "ghcr.io/rossoctl/cortex/authbridge:v0.7.0-alpha.3", // authbridge-lite: size-optimized variant for the "lite" // mode. Same listener layout as AuthBridge but parsers // (a2a/mcp/inference) are dropped. - AuthBridgeLite: "ghcr.io/rossoctl/cortex/authbridge-lite:latest", + AuthBridgeLite: "ghcr.io/rossoctl/cortex/authbridge-lite:v0.7.0-alpha.3", // proxy-init: iptables init container, used by // envoy-sidecar mode only. - ProxyInit: "ghcr.io/rossoctl/cortex/proxy-init:latest", + ProxyInit: "ghcr.io/rossoctl/cortex/proxy-init:v0.7.0-alpha.3", PullPolicy: corev1.PullIfNotPresent, }, Proxy: ProxyConfig{ diff --git a/operator/test/e2e/e2e_suite_test.go b/operator/test/e2e/e2e_suite_test.go index 4f1e5e66..3d695ca1 100644 --- a/operator/test/e2e/e2e_suite_test.go +++ b/operator/test/e2e/e2e_suite_test.go @@ -57,15 +57,19 @@ var ( signerImage = "ghcr.io/rossoctl/operator/agentcard-signer:e2e-test" // sidecarImages are the AuthBridge sidecar images to pull and load into Kind. - // cortex ships two combined images plus proxy-init: + // cortex ships three combined images plus proxy-init: // * authbridge-envoy: envoy-sidecar mode (Envoy + ext_proc + bundled spiffe-helper) // * authbridge: proxy-sidecar mode (authbridge-proxy + bundled spiffe-helper) + // * authbridge-lite: lite mode (jwt-validation + token-exchange only) // * proxy-init: iptables init container, envoy-sidecar mode only // Spiffe-helper and client-registration are no longer separate images. + // Keep tags in sync with charts/operator/values.yaml (defaults.images.*) so + // e2e exercises the images the chart actually ships, not :latest. sidecarImages = []string{ - "ghcr.io/rossoctl/cortex/authbridge-envoy:latest", - "ghcr.io/rossoctl/cortex/authbridge:latest", - "ghcr.io/rossoctl/cortex/proxy-init:latest", + "ghcr.io/rossoctl/cortex/authbridge-envoy:v0.7.0-alpha.3", + "ghcr.io/rossoctl/cortex/authbridge:v0.7.0-alpha.3", + "ghcr.io/rossoctl/cortex/authbridge-lite:v0.7.0-alpha.3", + "ghcr.io/rossoctl/cortex/proxy-init:v0.7.0-alpha.3", } )