Governed dependency composition¶
Platform teams publish approved dependency payloads once. Application teams publish application payloads. A release pipeline composes their immutable digests into an ordinary Brewlet application image before Kubernetes sees it. There is no deployment-time dependency resolver, second shim pull, workload CRD or hot replacement of libraries.
This page covers the producer and release steps defined in specification ยง16. See Verification and limitations for the runtimes and platforms that have been qualified.
Platform: publish the complete dependency closure¶
Publish the actual dependency files, a complete file lock, a CycloneDX 1.5 SBOM and signed publisher provenance:
brewlet layer-set registry.example.com/platform/dependencies:2026.10 \
--config layerset.json --lock dependencies.lock.json \
--layer dependency=./dependencies:/app/dependencies \
--signing-key "$PUBLISHER_KEY_FILE" \
--builder-identity https://ci.example.com/platform
layer-set computes compressed descriptors and diff IDs. The lock covers every
payload file with its actual hash and explicit package identity; supply
ecosystem-specific PURLs so external scanners can match advisories accurately.
The generic producer never guesses package versions or resolves dependencies.
Use the optional Maven producer for a source-BOM
and scoped Maven graph rather than putting Java knowledge in the composer.
The repository's integration-tests/fixtures/java projects (bom,
dependency and app) show a concrete platform BOM, library and consuming
thin-JAR app.
Application: retain an app-only immutable input¶
Build with the normal external toolchain, then package only the application contribution and its matching explicit launch descriptor:
brewlet build registry.example.com/builds/orders:payload \
--platform linux/arm64 --launch launch.json \
--layer app=./application:/app
brewlet push registry.example.com/builds/orders:payload
This payload can be structurally valid without being runnable alone. Its dependencies are added at release time. Initial composition rejects dependency-role layers; it does not guess which arbitrary bundled files to strip out.
For an app-only Maven publication:
mvn package sh.brewlet:brewlet-maven-plugin:0.8.1:push \
-Dbrewlet.applicationOnly=true \
-Dbrewlet.sourceBom=sh.brewlet.fixture:fixture-bom:2.0.0 \
-Dbrewlet.image=registry.example.com/builds/orders:payload \
-Dbrewlet.platforms=linux/arm64
The producer validates the actual source BOM, requires a thin JAR, excludes
dependency payloads and retains the original scoped graph as the ordinary
application file /app/.brewlet/application-build.json. Release composition
cannot rewrite that build history to pretend the app was compiled against a
new bundle.
Release: compose, verify, test, deploy¶
Choose one explicit Linux platform and one complete managed set. Resolve tags
in the delivery workflow first; bundle accepts only canonical digest-pinned
remote inputs or explicit local LAYOUT@sha256:... inputs:
brewlet bundle registry.example.com/releases/orders:candidate \
--application "$APPLICATION_REF_AT_DIGEST" \
--layer-set "$DEPENDENCY_SET_REF_AT_DIGEST" \
--platform linux/arm64 --policy release-policy.yaml \
--signing-key "$COMPOSER_KEY_FILE" \
--builder-identity https://ci.example.com/release \
--push-result candidate.json
brewlet verify "$CANDIDATE_REF_AT_DIGEST" \
--platform linux/arm64 --policy release-policy.yaml
--store DIRECTORY publishes a local OCI layout instead. Application and
dependency compressed bytes, descriptors, order and diff IDs are preserved.
Launch-layer bytes remain unchanged unless an explicit --launch FILE
replacement is requested; that replacement is validated and recorded.
The config mirror/rootfs list is regenerated for the actual output graph.
Application layers cannot overwrite managed paths, replace their ancestors with files/symlinks, hide them with whiteouts, or make them opaque. This is filesystem integrity, not proof that app code contains no shaded/vendored copies or that it executes only approved code.
The JSON result (printed, and written atomically to the --push-result file)
contains the exact output digest and a verification object with separate
structural, publisherProvenance and compositionSignature results.
brewlet verify prints the same verification object for a published bundle.
testReleaseApproval: not-evaluated means this verifier did not verify a test
or release approval. A signature is not a compatibility test.
Run the configured application/integration tests against this exact candidate digest before updating the ordinary Deployment image. A failed candidate test must stop that delivery pipeline; do not resolve another dependency graph or bypass trust as a fallback.
Accepted and rejected inputs¶
| Input | Accepted | Rejected |
|---|---|---|
| Application | A version-1 application graph with only app and launch layers; or a prior composition whose signature verifies under the current policy | Images with bundled dependency-role layers (they are never guessed or stripped); unsigned or development compositions as replacement sources; ordinary OCI/Docker images without a Brewlet launch layer |
| Dependency set | Exactly one digest-pinned managed set with a complete lock, exactly one CycloneDX SBOM and trusted publisher provenance | Unlocked or generic sets; partial or merged catalogs; revoked or unapproved subjects; missing or multiple SBOMs |
| Platform | Exactly one explicit linux/<arch>[/variant], verified against its index member |
Missing, multiple or non-Linux platforms; implicit index selection |
| References | Canonical REF@sha256:... or local LAYOUT@sha256:... |
Mutable tags |
| Launch | The application's existing launch layer, or an explicit --launch FILE replacement |
Inferred argv or files moved to fit a launch descriptor |
The composer does not detect languages and does not inspect JARs, wheels or packages. Producer-specific checks, such as Maven's thin-JAR validation, run before publication.
Trust policy¶
Policy files accept strict JSON or YAML. Public SPKI PEMs are embedded; private keys stay in CI secret delivery. A production policy requires active publisher and composer keys and explicit identities:
schemaVersion: 1
mode: production
notAfter: "2027-01-01T00:00:00Z"
publisher:
identity: https://ci.example.com/platform
keys:
- publicKey: |
-----BEGIN PUBLIC KEY-----
<publisher SPKI PEM body>
-----END PUBLIC KEY-----
notBefore: "2026-01-01T00:00:00Z"
notAfter: "2027-01-01T00:00:00Z"
composer:
identity: https://ci.example.com/release
keys:
- publicKey: |
-----BEGIN PUBLIC KEY-----
<release SPKI PEM body>
-----END PUBLIC KEY-----
notBefore: "2026-01-01T00:00:00Z"
notAfter: "2027-01-01T00:00:00Z"
The PEM bodies are placeholders: generate/manage real keys outside Brewlet.
Each key can be marked revoked: true; overlapping active keys permit rotation.
Each authority optionally restricts exact subjects or denies
revokedSubjects (SHA-256 digests). Expired policies/keys and revoked subjects
are not unsigned development inputs.
An explicitly chosen development policy may allow unsigned composition.
Its structural evidence is not production-verified and cannot authorize
dependency stripping for a replacement. Present invalid/untrusted signatures
never become an unsigned fallback.
The signed, versioned in-toto/DSSE predicate is an OCI referrer to the final manifest. It binds original and incoming source roots/platform manifests, application layers, selected set/config/lock/SBOM/publisher evidence, original/ input/effective launch layers, output config, parent composition and release identity. Supporting immutable graphs and their referrers travel with the output, so verification does not depend on a mutable source tag.
For cross-registry operations, bundle, verify and inventory accept
--registry-auth FILE, a JSON map from exact https://host[:port] origins to
username/password objects. Alternatively set BREWLET_REGISTRY_USERNAME,
BREWLET_REGISTRY_PASSWORD and BREWLET_REGISTRY_ORIGIN. A source registry
does not receive another origin's credentials.
Dependency-only replacement and rollback¶
Pass the prior signed composition as --application and the approved
replacement set as --layer-set. Brewlet verifies the prior composition before
identifying removable dependency layers. An unsigned annotation alone is not
authority. Original app-layer bytes and build evidence remain intact.
Publish and test the candidate, roll the native Deployment, then inspect actual running identities. Old processes keep their old dependencies until normal replacement. A rollback selects a previous approved final image digest under current trust policy; it is not a trust or revocation bypass.
Operators: export facts for external scanners¶
For the end-to-end response to an advisory, follow the dependency remediation runbook.
brewlet inventory --kubeconfig "$KUBECONFIG" --context production \
--namespace payments --namespace orders \
--policy release-policy.yaml --output dependencies.json
Repeat --workload Kind/namespace/name to narrow native workloads.
--max-age bounds node observation age (at most two minutes). The reader needs
get/list Pods, get native controller owners, get Nodes and get NodeProfiles.
See examples/managed-dependencies/inventory-rbac.yaml;
it creates no binding and grants no Secret, workload-write, host-exec or
inventory-write permission.
The exporter correlates Pod UID/container ID, protected shim-recorded application root/manifest/config identities and fresh worker observations. It follows owner-reference UIDs, not matching labels or desired image tags. A CRI config digest is not relabeled as a manifest digest.
The schema-versioned report contains per-container/workload identities and
deduplicated verified artifact facts, complete package locks and the exact
publisher-bound SBOMs. Missing/ambiguous/stale evidence or registry/trust errors
remain unverified; partial reports are retained and the command exits nonzero.
Explicit scope and capture times remain visible. These diagnostic observations
are never cleanup or host-mutation authority.
There is no Brewlet vulnerability scanner or advisory database. External
automation can trigger on bundle publication, fetch its CycloneDX OCI referrer,
verify digest/provenance bindings, and pass the document to its existing scanner.
It can also read each artifacts entry's sbom from this export, scan once per
SBOM/scanner/database snapshot, and join package findings back through each
container's evidenceKey. Existing scanner tooling owns ecosystem version
matching. Scheduled rescanning can find newly disclosed CVEs in unchanged bytes.
Presence in an observed image does not prove vulnerable-code reachability, actual module loading or absence of embedded copies. Managed-set coverage is not exhaustive application or infrastructure-runtime coverage. An unavailable SBOM or scan result is unknown, never a clean bill of health.
WebAssembly workloads¶
The WebAssembly guide uses the same producer, release policy, signed final image, replacement and inventory path for both Wasmtime and Spin. There is no language detection and no Wasm-specific composer.
A Wasm dependency update replaces a declared external data file that the guest
reads; the compiled .wasm files and the host runtime images stay unchanged.
Libraries compiled into a guest still require rebuilding the guest, and dynamic
component linking is not qualified. Engine or OS fixes are delivered as a
RuntimeImage update.