-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture and OCI Layout
cmd/oci-builder (backed by internal/oci) does exactly one thing: turn
an already-compiled Linux executable, plus whatever -extra-files it
needs, into an OCI Image Layout.
Each application/platform is built and verified independently. The composition step then creates a new layout without rewriting any config, manifest, or layer:
secure-oci compose --output catalog \
api-amd64 api-arm64 worker-amd64
# equivalent low-level command:
oci-builder compose -output catalog \
api-amd64 api-arm64 worker-amd64The resulting index.json may contain:
- the same image/tag for
linux/amd64andlinux/arm64; - several tags referencing identical content-addressed blobs;
- several applications and therefore several manifests for one platform.
Descriptors are sorted by image reference, platform, and digest for deterministic output. Every input is strictly verified, blobs are copied once per digest with a fixed 1 MiB buffer, and the combined layout is verified again before an atomic rename installs it. A duplicate image-reference and platform pair is rejected because a consumer could not select it unambiguously.
Composition does not emulate an OCI registry namespace. The
org.opencontainers.image.ref.name descriptor annotation carries the local
image/tag selection metadata. Consumers that only support a single manifest
must select or export one descriptor; registry-aware tools such as Skopeo can
copy the full multi-manifest layout.
The pipeline, in order:
-
Validate the input.
Options.Binarymust be a regular, executable file.Options.Outputmust not already exist — the builder never overwrites anything, to prevent an attacker-controlled directory from silently replacing a previous, trusted layout. -
Build the layer. The binary, every extra file, and a fixed set of
directories are packed into a
tarstream with fixed metadata (see below), then gzip-compressed at best-compression level. -
Build the config. A JSON OCI image config: architecture, OS, the
single layer's
diff_id(the uncompressed layer's SHA-256),User,Entrypoint, and any labels. - Build the manifest and index. Standard OCI schema-2 documents referencing the config and layer blobs by digest.
-
Write atomically. Everything is written to a fresh temporary
directory (
os.MkdirTempnext to the destination) and onlyos.Renamed into place at the very end — a crash or interrupt mid-build never leaves a partial, half-written layout at the destination path.
flowchart TD
A["Options.Binary + ExtraFiles"] --> V{"regular file?<br/>executable?<br/>Output doesn't exist?"}
V -- no --> E["error - nothing written"]
V -- yes --> L["tar + gzip the layer<br/>(fixed mtimes, sorted entries)"]
L --> C["OCI config JSON<br/>(User, Entrypoint, Labels, diff_id)"]
C --> M["manifest.json"]
M --> I["index.json"]
L & C & M & I --> T["write to a temp dir"]
T --> R["os.Rename into place"]
R --> D["digest returned"]
oci-image/
oci-layout # {"imageLayoutVersion":"1.0.0"}
index.json # platform descriptor + image reference annotation
blobs/sha256/
<config digest>
<manifest digest>
<compressed layer digest>
Exactly one manifest, one layer, one config — always. The builder has no notion of multi-layer images; that's a deliberate simplification, not a missing feature (see Project scope).
| Path | Mode | Purpose |
|---|---|---|
/app/service (configurable via -entrypoint) |
0555 |
the binary itself |
every -extra-file destination, if any were given |
0555 |
e.g. a shared library or the ELF interpreter - see CLI Reference |
/etc/ssl/certs/ |
0755 |
mount point for a CA bundle at runtime (nothing is placed here at build time) |
/tmp/ |
01777 (sticky) |
scratch space |
/var/tmp/ |
01777 (sticky) |
scratch space |
| every parent directory of the entrypoint and of every extra file | 0755 |
e.g. /app/, /lib/x86_64-linux-gnu/
|
Nothing else beyond what you explicitly asked for - the filesystem
equivalent of packing light. No shell, no package manager, no source
tree, no .git, no build cache - and no libc either, unless the binary
needs one and you supplied it as an -extra-file.
-
architecture:amd64orarm64(validated — no other value is accepted). -
os: alwayslinux. -
config.User: always"65532:65532"— a fixed, high, conventionally "nobody"-range non-root UID/GID (the same one distroless images use). -
config.Entrypoint: a one-element array, the absolute path passed via-entrypoint(default/app/service). -
config.Labels: whatever-label key=valuepairs were passed, sorted and deduplication-checked byLabelsFromPairs(see CLI Reference). -
rootfs.diff_ids: the single layer's uncompressed SHA-256.
Given the same input binary bytes and the same CLI options, two separate
builds — on different machines, at different times — produce byte-for-byte
identical tar, gzip, and JSON output. This isn't a general claim about
"reproducible builds" in the abstract; it's a specific, testable property
enforced by:
-
Fixed tar timestamps. Every tar header's
ModTimeistime.Unix(0, 0), never the build machine's clock. -
Fixed gzip metadata. The gzip header's
ModTimeistime.Unix(0, 0)andOSis fixed at255(unknown), so gzip doesn't leak the build host's OS byte. -
Fixed
createdfield. The OCI config'screatedtimestamp defaults to1970-01-01T00:00:00Zunless explicitly overridden with-created. - Sorted, deduplicated directory/label ordering. Both the layer's directory list and the label map are sorted before writing, so map iteration order (which Go deliberately randomizes) never leaks into the output.
ci-reproducibility.yml (see Testing and CI/CD) proves
this empirically on every push: it builds the executable and layout twice,
in two separate isolated jobs, and does a byte-for-byte cmp.
The builder never decides how its output will be run. That's
intentional: internal/oci and cmd/oci-builder produce one artifact —
the layout — and three completely independent consumers exist on top of
it:
-
Dockerfile— turns a layout into a container image, for normal container-runtime use. -
scripts/microvm— boots a layout directly under QEMU/KVM, for maximum isolation or for binaries that can't be containerized. -
ci-release.yml— turns a layout into a signed, published GHCR image (see Testing and CI/CD).
None of these consumers can influence what the builder produces, and the builder has zero knowledge of any of them. That's what makes "build once, run it three different ways" possible without three different build pipelines.
© 2026 CYPT71
platform-factory
Core
- Architecture and OCI Layout
- Next-generation Architecture
- Architecture Decision Records
- Security Model
- Threat Model and Residual Risks
- Independent Security Review Process
- CLI Reference
- Project Configuration and Dependency Freezing
- mTLS Configuration
- Meine Graal
CI/CD
Running an image
- Production Adoption Guide
- Dockerfile Consumer
- Local Dev (Podman/macOS)
- MicroVM Support
- MicroVM Administration
- Large-image streaming
Operating