Skip to content

Admission enforcement

Brewlet's optional managed-dependency admission integration admits a pod using runtimeClassName: brewlet only when the Pod image resolves to a digest with a valid, trusted final-image managed-dependency attestation. It combines a Ratify v1.4.5 external verifier plugin with a Gatekeeper policy and verifies Brewlet's native OCI 1.1 referrer in place so the runtime executes that same admitted image.

Preview: evaluate only in a disposable cluster

Source-built live tests cover referrer discovery, external plugin execution and Ratify/Gatekeeper enforcement. The runbook records results and fixture-only cache, registry and TLS settings. These results are not validation of published release artifacts or production certification.

The plugin verifies Brewlet's native evidence directly, reusing Brewlet's own DSSE and predicate verification code instead of requiring evidence to be republished in cosign or notation format.

The Brewlet specification section 4.5 defines the normative attestation contract. The admission/ source directory contains the verifier, deployable resources, and authoritative operational notes.

flowchart LR
    Pod[Pod CREATE or UPDATE\nruntimeClassName: brewlet] --> GK[Gatekeeper]
    GK --> Ratify[Ratify external-data provider]
    Ratify -->|OCI 1.1 Referrers API| Registry[(Registry)]
    Ratify --> Plugin[brewlet-managed-dependencies verifier]
    Plugin -->|DSSE + predicate verification| Result{Trusted evidence?}
    Result -->|yes| Admit[Admit]
    Result -->|no| Deny[Deny]

Ratify/Gatekeeper perform the signature and identity check before admission. The node shim does not introduce a second on-node signature-verification pass; it resolves and executes the same image digest admitted by the policy.

Why Ratify

Brewlet publishes a custom attestation artifact with its own artifact type, in-toto predicate types, and bare-key DSSE profile. Kyverno image verification and Sigstore policy-controller consume cosign or notation formats; they cannot consume Brewlet's native artifact directly without republishing it in one of those formats.

Ratify's external verifier protocol can verify the native Brewlet referrer in place. The integration targets Ratify v1.4.5 and its v1 external verifier plugin protocol.


What is verified

The image attestation is:

  1. an OCI 1.1 referrer with artifact type application/vnd.brewlet.attestation.v1+json;
  2. a DSSE envelope containing an in-toto Statement v1 with predicate type https://brewlet.sh/attestations/managed-dependencies/v1;
  3. signed with ECDSA P-256 over the DSSE pre-authentication encoding, with keyid equal to sha256(SPKI DER); and
  4. bound to the admitted final image digest, thinJar: true, the application JAR, bundle, dependency layer, lock, SBOM, source BOM, and expected application-builder identity.

Bundle provenance uses the separate predicate type https://brewlet.sh/attestations/dependency-bundle/v1. Admission requires the final-image statement; bundle-level provenance establishes trust earlier, while the application image is composed.

Malformed, wrong-key, wrong-identity, wrong-subject, or incorrectly bound candidates are rejected. The image is denied unless at least one candidate satisfies the complete contract; claims are never combined across candidates.


Prerequisites

  • Ratify v1.4.x installed as the ratify-provider Gatekeeper external-data provider.
  • Gatekeeper installed with external data enabled, validation webhook failurePolicy: Fail, and rules covering Pod CREATE/UPDATE and pods/ephemeralcontainers UPDATE.
  • A registry that implements the OCI 1.1 Referrers API.
  • A digest-pinned Brewlet application image with a signed final-image managed-dependency attestation.
  • The application-builder's ECDSA P-256 public key and the exact expected builder identity.

Referrers API is mandatory

Brewlet's custom deterministic fallback tags are not Ratify/oras discovery metadata. If the registry does not expose the OCI 1.1 Referrers API, Ratify cannot discover the attestation and admission denies the workload.

For private registries, configure the oras store's authProvider, such as a k8Secrets provider backed by a Docker-config Secret. Evidence that cannot be authenticated or fetched cannot grant admission.

Keep provider timeouts shorter than webhook timeouts; the live fixture uses 20 seconds for the provider and 30 seconds for the validating webhook. The constraint's enforcementAction: deny cannot compensate for webhook failurePolicy: Ignore during transport failures.


Build and deliver the verifier plugin

Build the external verifier:

git clone https://github.com/microsoft/brewlet.git
cd brewlet/admission/ratify-verifier
go build -o brewlet-managed-dependencies .

Choose one delivery model:

Publish the binary as an OCI artifact and configure 20-ratify-verifier.yaml with an immutable source:

spec:
  source:
    artifact: registry.example.com/brewlet/ratify-verifier@sha256:<plugin-digest>

Ratify executes the downloaded plugin without independently verifying it. A mutable tag would let someone replace the verifier with an always-allow binary, so the source must be digest-pinned.

Place the binary at:

/home/nonroot/.ratify/plugins/brewlet-managed-dependencies

Set RATIFY_CONFIG=/home/nonroot/.ratify to select that plugin directory. Use a digest-pinned custom Ratify image from the first deployment and remove spec.source from the Verifier resource. The live scenario exercises this delivery route.

The plugin links Ratify's oras store and its registry-auth dependencies. Build it with a toolchain compatible with the Ratify installation.


Configure and deploy

  1. Edit 20-ratify-verifier.yaml: set the digest-pinned plugin source (or remove it for a baked plugin), provide trustedPublicKey or trustedPublicKeyPath, and set expectedBuilderIdentity.
  2. Configure private-registry authentication in 10-ratify-store.yaml when required.
  3. Review the namespace exclusions and begin 50-gatekeeper-constraint.yaml with enforcementAction: warn or dryrun.
  4. Apply the resources in order:
kubectl apply -f admission/deploy/10-ratify-store.yaml
kubectl apply -f admission/deploy/20-ratify-verifier.yaml
kubectl apply -f admission/deploy/30-ratify-policy.yaml
kubectl apply -f admission/deploy/40-gatekeeper-constrainttemplate.yaml
kubectl apply -f admission/deploy/50-gatekeeper-constraint.yaml

The resources configure:

File Resource Purpose
10-ratify-store.yaml Ratify Store Discovers native referrers through the OCI 1.1 Referrers API and fetches their blobs.
20-ratify-verifier.yaml Ratify Verifier Loads the Brewlet plugin and configures its trusted key and expected application-builder identity.
30-ratify-policy.yaml Ratify Rego Policy Counts success only from the named brewlet-managed-dependencies verifier and admits when at least one attestation verifies.
40-gatekeeper-constrainttemplate.yaml Gatekeeper ConstraintTemplate Sends regular, init, and ephemeral container images from Brewlet-runtime pods to Ratify.
50-gatekeeper-constraint.yaml Gatekeeper Constraint Applies the check to Pod CREATE and UPDATE requests, with explicit namespace exclusions.

Ratify v1.4.5's shipped chart CRD rejects Verifier.spec.type; use spec.name to select the plugin, as the corrected resource does. This defect was exposed by live API deployment and is covered by a manifest regression test.

Observe warnings and test known-good and known-bad images before changing the constraint to enforcementAction: deny.

Use the Rego policy in clusters

Ratify's config-policy can accept success from the first matching or an overlapping verifier without proving that the Brewlet plugin ran. The shipped Rego policy runs matching verifiers and binds success to the verifier named brewlet-managed-dependencies. Do not register a wildcard or overlapping verifier for Brewlet's attestation artifact type.

The cluster policy is a singleton

Ratify permits one cluster Policy named ratify-policy. Applying 30-ratify-policy.yaml replaces an existing policy. In a shared Ratify installation, merge the Brewlet rule into the existing Rego policy instead.


Verify in CI

The non-Kubernetes config.json uses Ratify v1.4.5's inline regoPolicy provider with passthroughEnabled: false. Like the cluster policy, it requires a complete valid candidate verified by brewlet-managed-dependencies, rather than accepting another verifier's success.

Install the plugin at ~/.ratify/plugins/brewlet-managed-dependencies, configure the trusted key path and expected identity in config.json, and use a Referrers-API-capable registry.

Ratify v1.4.5 can exit successfully after reporting a denied decision. CI must require JSON isSuccess: true, not just a zero process exit status. With Bash and jq, set IMAGE_DIGEST_REF to the immutable image reference and enforce:

set -o pipefail
ratify verify -s "$IMAGE_DIGEST_REF" -c admission/deploy/config.json |
  jq -e '.isSuccess == true'

Do not use --silent for enforcement. Test with the trusted key and with a different key or unsigned image to confirm the expected pass and fail paths.


Production trust model and limitations

Live fixture boundaries

The live scenario disables Gatekeeper response, Ratify provider and ORAS discovery caches, and directs the ORAS content cache to an immutable empty layout. This makes every candidate fetch real and avoids a concurrent content-cache index failure observed with the pinned Ratify version. It is a fixture configuration, not validation of production cache concurrency or revocation latency. Do not infer a key-rotation or outage guarantee for previously cached images from tests of fresh verification requests.

Regular and init image requests and valid ephemeral-container subresource updates are exercised at the API boundary. This does not establish support for running ordinary-image ephemeral debug containers through Brewlet.

Key distribution and rotation

The trust anchor is a bare ECDSA P-256 public key. Brewlet does not use Fulcio/keyless issuance or a Rekor transparency log for this contract. Key distribution and rotation are out of band.

Both shipped CLI and cluster Rego policies are rotation-tolerant: at least one valid Brewlet attestation is enough to admit an image. During rotation, publish attestations under the old and new keys and move verifier trust deliberately. A remaining invalid old-key attestation neither grants nor blocks admission when another candidate verifies.

Identity is a signed free-form string

expectedBuilderIdentity is compared verbatim with the builderIdentity in the signed predicate. It is not an OIDC- or Fulcio-issued identity. Anyone holding the trusted private key can assert any string, so the key is the actual trust boundary.

Image-level and bundle-level trust

Admission verifies the final-image managed-dependency attestation. That predicate binds the bundle digest and source BOM but does not expose the bundle publisher's signer identity. The policy therefore cannot directly assert that a specific platform team signed the bundle.

Bundle-publisher trust is enforced upstream during application publication: brewlet:push verifies present bundle provenance with trustedPublicKey/trustedSignerIdentity before composing the image. Admission then trusts the application builder that signed the final image. See Managed dependency bundles.

Digest-pinned images are required

Images must use repo@sha256:.... Ratify passes the original image reference to the plugin; a tag is not resolved to a digest in that hand-off, so the plugin cannot bind the statement subject and denies tag-based images. Enforce digest pinning in CI or GitOps.

Scope and excluded namespaces

The Gatekeeper template covers every container, init container, and ephemeral container on pods with runtimeClassName: brewlet, including raw workloads and pods generated from JavaApplication. Non-Brewlet pods are not checked.

Namespaces excluded to prevent infrastructure bootstrap deadlocks are also outside this control. Keep Brewlet workloads out of excluded namespaces and restrict who can create pods there.


Fail-closed behavior

The integration denies images with no complete valid candidate, including when the only available evidence has any of these problems:

  • missing Brewlet attestation referrers;
  • missing trusted key or a signature from the wrong key;
  • missing or mismatched expected builder identity;
  • tampered DSSE payloads or signatures;
  • a subject or finalImageDigest bound to another image;
  • the bundle-provenance predicate where the final-image predicate is required;
  • thinJar values other than true or malformed predicate digests;
  • an attestation manifest that does not contain exactly one DSSE layer;
  • registry discovery, authentication, manifest, or blob-fetch failures;
  • tag-based or otherwise unresolved image subjects; and
  • success reported only by another or overlapping verifier.

A rejected extra candidate does not block a complete valid candidate.

In a disposable evaluation cluster, exercise these failure paths with the constraint in deny mode and confirm actual Kubernetes admission outcomes. Successful local evaluation does not by itself establish production readiness. See the live-validation runbook for the implemented regression scenarios, recorded results and fixture limitations.