Managed dependency bundles¶
Managed dependency bundles let a platform team publish an approved, immutable Java runtime classpath while application teams keep using normal Maven BOM, dependency, test, and IDE workflows. Brewlet verifies that an application's resolved runtime graph matches the approved bundle, rejects fat JARs, and composes a runnable image from the verified bundle layer and a thin application JAR.
Kubernetes never resolves Maven dependencies. A JavaApplication or raw
Deployment still references only the complete application image.
The Brewlet specification section 4.5 is the normative bundle and attestation contract. This page focuses on how to publish and consume it.
flowchart LR
Ops[Platform / Ops] -->|approved BOM| Bundle[Immutable dependency bundle]
Dev[Developer / CI] -->|thin JAR + matching graph| Image[Runnable application image]
Bundle -->|verified classpath layer| Image
Image -->|digest-pinned reference| K8s[Kubernetes]
K8s -->|no Maven resolution| Runtime[Brewlet runtime]
Ownership and lifecycle¶
| Stage | Owner | Behavior |
|---|---|---|
| BOM policy | Platform / governance | Publish an approved Maven BOM. |
| Bundle publication | Platform / governance | Import the BOM, declare bundle membership, resolve the runtime closure, and publish the immutable dependency bundle. |
| Development | Application team | Import the same BOM and declare dependencies normally. Maven and the IDE continue to resolve dependencies during development. |
| Application publication | Developer / CI | Verify the resolved runtime graph against the bundle lock and publish a thin-JAR image that reuses the exact managed classpath-layer descriptor. |
| Deployment | Platform / Kubernetes | Reference only the immutable final image from JavaApplication or a raw workload. |
| Runtime | Brewlet shim | Launch with the dependencies already present under /app/lib; no Maven access or startup download occurs. |
Bundle tags are convenient inputs to publication, but deployments should use the immutable final application image digest. Updating a bundle does not automatically roll out applications built from an older bundle.
Before you begin¶
For a Maven workflow, one platform POM can define the approved BOM and publish
its managed dependency bundle. Publish the POM to your Maven repository through
your normal Maven release process; the Brewlet plugin publishes the bundle to
the OCI registry. The application imports the BOM and declares its dependencies
normally. Configure the Brewlet Maven plugin from Maven Central as described in
Building & publishing.
The plugin declarations below let Maven download it automatically; no manual
installation or custom plugin repository is needed.
Keep the concrete BREWLET_VERSION exported by that setup (or supplied by your
platform team) in both projects' build environments. The plugin examples below
read it through Maven's ${env.BREWLET_VERSION} property; for reproducible builds,
pin the resolved version in each POM or CI environment.
Example Maven projects¶
The common setup needs two POMs: one owned by the platform team and one owned by the application team.
The platform POM uses dependencyManagement to define the approved versions
that application projects import and regular dependencies to select what
belongs in the bundle:
<groupId>com.example.platform</groupId>
<artifactId>approved-spring-bom</artifactId>
<version>2026.08</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.10</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>sh.brewlet</groupId>
<artifactId>brewlet-maven-plugin</artifactId>
<version>${env.BREWLET_VERSION}</version>
<configuration>
<dependencyBundleImage>registry.example.com/platform/java-deps/spring-web:2026.08</dependencyBundleImage>
<sourceBom>com.example.platform:approved-spring-bom:2026.08</sourceBom>
</configuration>
</plugin>
</plugins>
</build>
The application imports the same BOM and declares dependencies normally. Brewlet verifies this resolved runtime graph against the selected bundle:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example.platform</groupId>
<artifactId>approved-spring-bom</artifactId>
<version>2026.08</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>sh.brewlet</groupId>
<artifactId>brewlet-maven-plugin</artifactId>
<version>${env.BREWLET_VERSION}</version>
<configuration>
<image>registry.example.com/apps/orders:${project.version}</image>
<dependencyBundle>registry.example.com/platform/java-deps/spring-web:2026.08</dependencyBundle>
<mainClass>com.example.OrdersApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
When a third POM is useful
If your organization already publishes a standalone BOM that should remain
version-management-only, add a small platform-owned bundle module. That
optional module imports the existing BOM, declares the bundle dependencies,
and runs brewlet:dependency-bundle. It is an organizational separation,
not a Brewlet requirement.
Platform workflow: publish a bundle¶
Run this from the platform-owned Maven project that defines the approved BOM and declares which dependencies belong in its bundle:
mvn package brewlet:dependency-bundle \
-Dbrewlet.dependencyBundleImage=registry.example.com/platform/java-deps/spring-web:2026.08 \
-Dbrewlet.sourceBom=com.example.platform:approved-spring-bom:2026.08
One POM can own both definitions: dependencyManagement supplies approved
versions when the POM is deployed to a Maven repository, while regular
dependencies select the bundle's runtime closure. The
brewlet:dependency-bundle goal publishes only the OCI bundle. If your
organization keeps its BOM strictly version-only, run the goal from a small
platform-owned module that imports that BOM and declares the bundle
dependencies instead.
The Brewlet Maven plugin selects the bundle contents from the project's resolved runtime classpath:
- regular
<dependencies>select the direct bundle members;<dependencyManagement>supplies versions but does not add members; compileandruntimedependencies and their resolved transitive closure are included;test,provided, andsystemdependencies are excluded, as are artifact types Maven does not add to the classpath; and- normal Maven conflict mediation, exclusions, and optional-dependency rules apply.
The canonical lock records each dependency's effective scope. An
application's graph must match that scope exactly, so a compile/runtime
difference is rejected even when the artifact bytes are identical. Adjust
bundle membership with the platform POM's regular dependencies and
exclusions.
The goal resolves the runtime dependency closure, creates the canonical
lock and deterministic flat classpath layer, publishes a CycloneDX 1.5 SBOM,
and writes a local OCI layout under
target/brewlet/dependency-bundle-oci.
Optionally sign the bundle provenance with the platform publisher's key and identity:
mvn package brewlet:dependency-bundle \
-Dbrewlet.dependencyBundleImage=registry.example.com/platform/java-deps/spring-web:2026.08 \
-Dbrewlet.sourceBom=com.example.platform:approved-spring-bom:2026.08 \
-Dbrewlet.signingKey=platform-private.pem \
-Dbrewlet.signerIdentity=https://ci.example.com/platform-bundles
Configure compatibleJdks in the plugin when the approved classpath is
restricted to specific JDK feature versions.
Supply an already-built flat classpath tar and its canonical lock:
brewlet dependency-bundle ./deps.tar platform/java-deps/spring-web:2026.08 \
--store ./oci \
--name spring-web \
--version 2026.08 \
--source-bom com.example.platform:approved-spring-bom:2026.08 \
--lock ./dependency-lock.json \
--compatible-jdks 21,25
To sign the bundle, add both flags:
Go CLI boundary
The Go CLI consumes managed bundles from its --store OCI layout and
requires caller-supplied canonical --lock files. brewlet push can upload
ordinary images to a registry, but not images composed with
--dependency-bundle. Use the Maven plugin for BOM import, Maven graph
derivation, and direct registry publication or consumption of managed bundles.
Every valid bundle includes an SBOM. Bundle provenance is optional: omitting both signing options publishes an unsigned bundle, while supplying them publishes a DSSE-signed in-toto bundle-provenance referrer.
Developer workflow: compose an application¶
Build a genuine thin application JAR. Managed mode rejects nested dependency
JARs, including BOOT-INF/lib and WEB-INF/lib.
mvn package brewlet:push \
-Dbrewlet.image=registry.example.com/apps/orders:1.4.2 \
-Dbrewlet.dependencyBundle=registry.example.com/platform/java-deps/spring-web:2026.08 \
-Dbrewlet.mainClass=com.example.OrdersApplication
If the selected bundle has provenance, trust inputs are required:
-Dbrewlet.trustedPublicKey=platform-public.pem \
-Dbrewlet.trustedSignerIdentity=https://ci.example.com/platform-bundles
Application signing is a separate choice and uses a separate identity:
-Dbrewlet.signingKey=app-private.pem \
-Dbrewlet.builderIdentity=https://ci.example.com/application-publisher
brewlet:push resolves a registry reference directly. It may also consume a
local dependency-bundle OCI-layout directory.
brewlet push ./target/orders.jar apps/orders:1.4.2 \
--store ./oci \
--format image \
--dependency-bundle platform/java-deps/spring-web:2026.08 \
--dependency-lock ./application-dependency-lock.json \
--main-class com.example.OrdersApplication
For a signed bundle, add:
--trusted-public-key platform-public.pem \
--trusted-signer-identity https://ci.example.com/platform-bundles
To sign the final application's managed-dependency evidence, add:
Note
The Go CLI requires the application's caller-supplied canonical
--dependency-lock and resolves the named bundle from the same local
--store. Use the Maven plugin when Brewlet should derive and compare the
Maven runtime graph or consume the bundle from a registry.
Brewlet compares the complete resolved graph with the bundle lock, including
coordinates, type, classifier, scope, filename, and JAR digest. Managed mode
then forces classpath launch with the application JAR and lib/*.
Signed and unsigned combinations¶
Bundle signing and application signing are independent:
| Bundle | Application | Publication behavior |
|---|---|---|
| Unsigned | Unsigned | Supported when policy permits unsigned evidence. |
| Unsigned | Signed | Final image carries a signed managed-dependency attestation. |
| Signed | Unsigned | Bundle provenance must verify during application publication; final image has no signed attestation. |
| Signed | Signed | Bundle provenance is verified upstream and the final image receives its own application-builder attestation. |
Brewlet determines signing status from the evidence it discovers. A bundle with no provenance is unsigned. If any provenance is present, Brewlet requires the trusted key and expected signer identity and validates at least one complete candidate. Malformed, untrusted, or incorrectly bound provenance fails verification rather than being treated as unsigned.
The identities represent different roles:
signerIdentityis the platform bundle publisher.builderIdentityis the application publisher that composed the final image.
Generate a separate ECDSA P-256 key pair for each role you choose to sign:
brewlet keygen --private platform-private.pem --public platform-public.pem
brewlet keygen --private app-private.pem --public app-public.pem
Keep private keys in your CI secret store. Public-key distribution and rotation are platform responsibilities.
Production admission verifies the final-image application-builder attestation. Bundle-publisher trust is enforced earlier, while composing the image. See Admission enforcement.
What the final image records¶
The complete kubelet-pullable image:
- reuses the exact verified managed classpath-layer descriptor, preserving its digest, size, annotations, and uncompressed diff ID;
- contains the thin application JAR and launches it with the managed
dependencies under
/app/lib; - records canonical managed-dependency evidence binding the application JAR, bundle, dependency layer, lock, SBOM, source BOM, and thin-JAR verdict; and
- when application signing is enabled, carries a DSSE/in-toto managed-dependency attestation bound to the immutable final image digest and application-builder identity.
The layer is the same standard OCI gzip layer used by the runnable image, so registries and containerd can deduplicate it without repacking. See Layered classpath deployment for the runtime layout and cache behavior.
Responding to a dependency CVE¶
When a bundled library (for example log4j-core) receives a critical CVE:
- Identify affected workloads from what is running. Brewlet does not track
bundles on
JavaApplicationor on pods. Instead, map each running pod's digest-pinned image to its managed-dependency evidence withbrewlet inspect. That evidence names the bundle and SBOM digests. Read the components and purls from the bundle's CycloneDX SBOM referrer. Treat any image you cannot trace to an SBOM as unverified, not clean. - Publish a patched bundle from the updated BOM under a new version or tag. Bundles are immutable, and publishing one does not change running applications.
- Recompose and roll out. Run
brewlet pushagain with the same thin application JAR,--dependency-bundlepointing at the patched bundle, and its lock. No application rebuild is required. The application layer andapplicationJarDigeststay the same, and only the managed classpath layer changes. Then roll each workload to the new image digest with a zero-downtime rolling update (for example,maxUnavailable: 0). Brewlet does not swap classpath layers inside a running JVM, so a new pod on the new digest is the unit of remediation. - Verify by repeating step 1 against the running pods.
E2E tier 19 (integration-tests/e2e/tier19-cve-remediation.sh) runs this
procedure end to end. It uses integration-tests/e2e/cve_sweep.py as the
reference SBOM sweep.
Failure behavior¶
Managed publication fails closed. It does not silently rebuild dependency layers from the application project after an error.
Failures include:
- bundle resolution errors or wrong artifact/config/layer/lock media types and schemas;
- descriptor digest or size mismatches;
- invalid USTAR headers/checksums, truncated blocks or file data, nonzero file padding, missing end markers, or data appended after the archive terminator;
- unsafe tar entries, invalid UTF-8 entry names, unexpected files, or individual JAR digest mismatches;
- missing, duplicate, or invalid SBOM evidence;
- graph differences in coordinates, type, classifier, scope, filename, or content digest;
- nested or fat application JARs;
- an application JDK outside the bundle's compatible-JDK policy; and
- malformed, untrusted, wrong-identity, wrong-subject, or incorrectly bound provenance when provenance is present.
A matching layer digest does not make malformed tar framing valid. Both Go and Maven validate the archive structure independently of the layer and per-JAR SHA-256 checks. Published layers use flat regular-file USTAR records, zero padding to 512-byte boundaries, and two zero end blocks; extra complete zero blocks are allowed. Valid permission, owner, and timestamp values need not equal the canonical publisher defaults. Header checksums are format checks, not a replacement for cryptographic digests or signer verification.
Regenerate a malformed bundle with the approved producer rather than bypassing validation or silently reconstructing dependencies from a different graph.
Use brewlet inspect to inspect a local bundle or final image and optionally
verify its attestation with the configured trusted key and identity. Enforce
signed final images before they run by deploying
Admission enforcement.