Push and pull OCI artifacts from Java.
Install · Examples · Registries configuration · Trust policy · Javadoc
ORAS (OCI Registry as Storage) lets you store any file in an OCI-conformant registry, not just container images. This SDK brings that to Java applications. It can:
- push and pull files and directories as OCI artifacts
- attach signatures, SBOMs or other artifacts to an existing one through the Referrers API
- copy artifacts between registries and to or from a local OCI image layout (directory or tar)
- reuse your Docker or Podman credentials,
registries.confandpolicy.json, including Sigstore signature verification
Warning
The SDK is in preview. Configuration and APIs may still change between releases.
Releases are published to Maven Central. Requires Java 17 or later.
<dependency>
<groupId>land.oras</groupId>
<artifactId>oras-java-sdk</artifactId>
<version>VERSION_HERE</version>
</dependency>Using Quarkus? The quarkus-oras extension wraps this SDK.
Using SNAPSHOT builds
Every commit on main publishes a SNAPSHOT to the Maven Central snapshots repository. Use them for testing only.
<repositories>
<repository>
<id>central-snapshots</id>
<name>ORAS Maven Central SNAPSHOTS</name>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>Reuse the credentials from ~/.docker/config.json or $XDG_RUNTIME_DIR/containers/auth.json:
Registry registry = Registry.builder().defaults().build();Or pass a username and password:
Registry registry = Registry.builder().defaults("username", "password").build();The examples below use insecure() because they target a local registry over plain HTTP. Drop it for real registries.
Registry registry = Registry.builder().insecure().build();
LocalPath artifact = LocalPath.of(Path.of("my-file.txt"));
Manifest manifest = registry.pushArtifact(ContainerRef.parse("localhost:5000/hello:v1"), artifact);Each file becomes one layer, named after the file through the org.opencontainers.image.title annotation. You can set a media type per file and add annotations to the manifest or to individual files:
Registry registry = Registry.builder().insecure().build();
Annotations annotations = Annotations.ofManifest(Map.of("build-tool", "maven"))
.withFileAnnotations("pom.xml", Map.of("format", "xml"));
Manifest manifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-app:v1"),
ArtifactType.from("application/vnd.maven+type"),
annotations,
LocalPath.of(Path.of("pom.xml"), "application/xml"),
LocalPath.of(Path.of("target/app.jar"), "application/java-archive"));A directory is packed into a tar+gzip layer titled with the directory name. The SDK also sets io.deis.oras.content.unpack=true, so pulling the artifact extracts it again.
Registry registry = Registry.builder().insecure().build();
Manifest manifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-configs:v1"),
LocalPath.of(Path.of("config-dir")));To push it as a zip instead, give the media type:
Manifest manifest = registry.pushArtifact(
ContainerRef.parse("localhost:5000/my-configs:v1"),
LocalPath.of(Path.of("config-dir"), "application/zip"));Files are written using their org.opencontainers.image.title annotation as the file name. The last argument says whether to overwrite existing files.
Registry registry = Registry.builder().insecure().build();
registry.pullArtifact(ContainerRef.parse("localhost:5000/hello:v1"), Path.of("output-dir"), true);Attached artifacts, like a signature or an attestation, point at the original manifest through their subject field. Clients find them with the Referrers API.
Registry registry = Registry.builder().insecure().build();
ContainerRef ref = ContainerRef.parse("localhost:5000/my-app:v1");
Manifest manifest = registry.pushArtifact(ref,
ArtifactType.from("application/vnd.maven+type"),
LocalPath.of(Path.of("pom.xml"), "application/xml"));
Manifest signatureManifest = registry.attachArtifact(
ref,
ArtifactType.from("application/vnd.example.signature"),
LocalPath.of(Path.of("pom.xml.asc")));
Referrers referrers = registry.getReferrers(
ref.withDigest(manifest.getDescriptor().getDigest()), null);When pushArtifact is too high-level, push the blobs and config yourself and assemble the manifest:
Registry registry = Registry.builder().insecure().build();
ContainerRef ref = ContainerRef.parse("localhost:5000/my-app:v1");
Layer layer1 = registry.pushBlob(ref, Files.readAllBytes(Path.of("schema.json")))
.withAnnotations(Map.of(Const.ANNOTATION_TITLE, "schema.json"));
Layer layer2 = registry.pushBlob(ref, Files.readAllBytes(Path.of("data.csv")))
.withAnnotations(Map.of(Const.ANNOTATION_TITLE, "data.csv"));
Config config = registry.pushConfig(ref, Config.empty().withMediaType("application/vnd.example.config+json"));
Manifest manifest = Manifest.empty()
.withConfig(config)
.withLayers(List.of(layer1, layer2));
registry.pushManifest(ref, manifest);Copies the manifest and all its blobs:
Registry source = Registry.builder().defaults("user", "pass").insecure().build();
Registry target = Registry.builder().defaults("user", "pass").build();
ContainerRef from = ContainerRef.parse("localhost:5000/my-app:v1");
ContainerRef to = ContainerRef.parse("registry.example.com/my-app:v1");
CopyUtils.copy(source, from, target, to, CopyUtils.CopyOptions.shallow());OCILayout has the same push and pull API as Registry, but reads and writes an OCI image layout on disk.
LayoutRef ref = LayoutRef.parse("/tmp/my-layout:latest");
OCILayout ociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout")).build();
// Push
ociLayout.pushArtifact(
ref,
ArtifactType.from("application/vnd.example.type"),
Annotations.empty(),
LocalPath.of(Path.of("my-file.txt"), "text/plain"));
// Pull
ociLayout.pullArtifact(ref, Path.of("output-dir"), false);Point it at a .tar path to get a single portable archive instead of a directory. The API doesn't change:
LayoutRef ref = LayoutRef.parse("/tmp/my-layout.tar:latest");
OCILayout ociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout.tar")).build();CopyUtils also works between a layout and a registry:
LayoutRef layoutRef = LayoutRef.parse("/tmp/my-layout:latest");
OCILayout ociLayout = OCILayout.Builder.builder().defaults(Path.of("/tmp/my-layout")).build();
Registry registry = Registry.builder().defaults("user", "pass").build();
ContainerRef target = ContainerRef.parse("registry.example.com/my-app:v1");
CopyUtils.copy(ociLayout, layoutRef, registry, target, CopyUtils.CopyOptions.shallow());Since 0.7.0 the SDK reads registries.conf, the same file Podman, Skopeo and Buildah use. Files are loaded in this order, and later ones override earlier ones:
/etc/containers/registries.conf/etc/containers/registries.conf.d/*.conf(alphabetical)$HOME/.config/containers/registries.conf$HOME/.config/containers/registries.conf.d/*.conf(alphabetical)
Supported settings:
# Short-name resolution (enforcing is the default)
short-name-mode = "enforcing"
unqualified-search-registries = ["docker.io"]
# Rewrite a prefix to another location
[[registry]]
prefix = "docker.io/bitnami"
location = "docker.io/bitnamilegacy"
# Block a registry
[[registry]]
prefix = "gcr.io"
blocked = true
# Use plain HTTP
[[registry]]
location = "localhost:5000"
insecure = true
# Mirrors, tried in order before falling back to the upstream registry
[[registry]]
prefix = "registry.example.com"
location = "registry.example.com"
mirror-by-digest-only = false # true restricts every mirror to digest pulls
[[registry.mirror]]
location = "mirror1.example.com"
insecure = false
pull-from-mirror = "all" # "all" (default) | "tag-only" | "digest-only"
[[registry.mirror]]
location = "mirror2.example.com"
insecure = true
pull-from-mirror = "digest-only"For every operation the SDK first resolves the reference, then applies security checks, then connects:
- Resolve: expand short names (
nginxbecomesdocker.io/library/nginx), applyprefix→locationrewrites and pick a mirror. - Check: apply
blocked,insecureand the trust policy to the resolved reference. - Connect.
The checks run on the host the request will actually reach, not on the name you passed in. Otherwise a mirror or alias could send traffic to a blocked or plain-HTTP host while the check passed on the original name. registries.conf and policy.json are treated as trusted operator configuration. If an attacker can edit them, the host is already compromised.
This has two consequences:
- Policies are scoped to repositories.
policy.jsonmatchesregistry[/namespace/repository]and ignores tags and digests, so you can't write a rule for a single tag or digest. - The trust policy is only checked on pulls. Deletes are checked against
blockedandinsecurebut not verified. To protect a digest from deletion, use registry-side RBAC or tag immutability.
The SDK can verify pulls against a policy.json trust policy, the format Podman, Skopeo and Buildah use. The first file found wins:
$HOME/.config/containers/policy.json/etc/containers/policy.json
Without a policy file, everything is accepted. You can also set the policy in code:
// From a file
Registry registry = Registry.builder()
.defaults()
.withPolicy(Path.of("/etc/containers/policy.json"))
.build();
// Or programmatically
Registry registry = Registry.builder()
.defaults()
.withPolicy(ContainersPolicy.rejectAll())
.build();Every manifest or index pull is checked against the policy. A pull that fails throws an OrasException.
| Requirement type | Supported | Behavior |
|---|---|---|
insecureAcceptAnything |
Yes | Accept without verification. |
reject |
Yes | Always reject. |
sigstoreSigned |
Yes | Accept only images signed with the given Sigstore (cosign) key. |
signedBy (GPG) |
No | Legacy, not implemented. |
Only keyed verification is supported. Give a single Sigstore public key (the cosign.pub from cosign generate-key-pair) with either:
keyPath: path to the PEM public keykeyData: the same key, base64-encoded
Multiple keys (keyPaths, keyDatas) and keyless Fulcio/Rekor verification aren't supported yet.
The SDK finds signatures through the Referrers API, looking for a Sigstore bundle (application/vnd.dev.sigstore.bundle.v0.3+json) attached to the image. It doesn't read a local signature store. The signature is bound to the pulled image by digest. signedIdentity is ignored: the cosign bundle only carries the image digest, so there is no Docker reference to compare against.
{
"default": [{"type": "insecureAcceptAnything"}],
"transports": {
"docker": {
"example.com/my-image": [
{"type": "sigstoreSigned", "keyPath": "/home/me/my-key.pub"}
]
}
}
}See CONTRIBUTING.md. This project follows the CNCF Code of Conduct.