16 KiB
MinIO On-Disk Format Interoperability
Use this when: deciding whether a MinIO drive set, bucket-metadata blob, or SSE object can be read or imported by a given RustFS build, or before touching any constant or codec listed under Version Anchors.
Source of truth: crates/filemeta/src/filemeta.rs, crates/filemeta/src/filemeta/codec.rs, crates/ecstore/src/bucket/metadata.rs, crates/ecstore/src/bucket/migration.rs, rustfs/src/storage/sse.rs, rustfs/Cargo.toml [features], .github/workflows/ci.yml, .github/workflows/minio-interop.yml.
This is an interop contract, not a plan. Migration is one-way (MinIO to RustFS). Erasure-coding internals are owned by erasure-coding.md; this document owns the interop claim, the fixture evidence, and the out-of-scope list.
Scope Matrix By Build Variant
Build variants are the rustfs crate features in rustfs/Cargo.toml: default, full, and rio-v2 (which enables rustfs-ecstore/rio-v2 and pulls in crates/rio-v2). rio-v2 is absent from both default and full.
| MinIO artifact | default / full build |
rio-v2 build |
Notes |
|---|---|---|---|
Unencrypted xl.meta (meta_ver 1-3, inline, multipart, versioned, delete marker) |
Read | Read | Part A. Normalized to meta_ver 3 on rewrite. |
Transitioned (tiered) xl.meta |
Not fixture-proven | Not fixture-proven | Out of scope; see erasure-coding.md for the tolerant transitioned-versionID read rule. |
.metadata.bin bucket config |
Read and imported | Read and imported | Part B. Importer reads a .minio.sys layout end to end. |
IAM config under config/iam/ |
Imported | Imported | try_migrate_iam_config; legacy field aliases normalized. |
| SSE-S3 / SSE-KMS objects, MinIO builtin static KMS | Fail closed, diagnosed | Read | Part C. Requires the shared master key. |
| SSE-C objects | Fail closed, diagnosed | Read | Part C. Customer key supplied per request. |
| Any SSE object, MinIO backed by KES / KMS plugin / MinKMS | Fail closed | Fail closed | Not planned; the DEK is sealed by the KES service. |
| RustFS-written drive set read by a live MinIO binary | Unsupported | Unsupported | Set-level divergence: MinIO looks for .minio.sys, RustFS writes .rustfs.sys. |
| RustFS-written SSE objects read by MinIO | Unsupported | Unsupported | Part C, reverse direction. |
Version Anchors
These constants are compatibility anchors. Bumping any of them requires a read-compat path for the prior value and a migration story, exactly as the meta_ver 2 to 3 read path provides. Values live in code; do not copy them elsewhere.
| Anchor | Symbol | File | Rule |
|---|---|---|---|
xl.meta magic |
XL_FILE_HEADER |
crates/filemeta/src/filemeta.rs |
Must equal MinIO's XL2 magic. |
| Container major / minor | XL_FILE_VERSION_MAJOR, XL_FILE_VERSION_MINOR |
crates/filemeta/src/filemeta.rs |
check_xl2_v1 (crates/filemeta/src/filemeta/codec.rs) rejects major > XL_FILE_VERSION_MAJOR. |
| Header version | XL_HEADER_VERSION |
crates/filemeta/src/filemeta.rs |
decode_xl_headers rejects header_ver > XL_HEADER_VERSION. |
| Metadata version | XL_META_VERSION |
crates/filemeta/src/filemeta.rs |
Written by FileMeta::new; decode_xl_headers rejects meta_ver > XL_META_VERSION (accept-older, reject-newer). |
| Bucket metadata header | BUCKET_METADATA_FORMAT, BUCKET_METADATA_VERSION |
crates/ecstore/src/bucket/metadata.rs |
Checked by check_header; both match MinIO's bucketMetadataFormat / bucketMetadataVersion. |
| Erasure algorithm string | ERASURE_ALGORITHM |
crates/ecstore/src/object_api/mod.rs |
rs-vandermonde; enum ErasureAlgo in crates/filemeta/src/fileinfo.rs. |
| Meta bucket names | RUSTFS_META_BUCKET, MIGRATING_META_BUCKET, BUCKET_META_PREFIX |
crates/ecstore/src/disk/mod.rs |
.rustfs.sys is the live meta bucket; .minio.sys is the importer source. |
Part A — xl.meta Object Format
| Aspect | Contract | Where |
|---|---|---|
| Version probe | read_format_versions returns (major, minor, header_ver, meta_ver) without a full parse |
crates/filemeta/src/filemeta/codec.rs |
| Read compatibility | Accepts meta_ver 1-3 including legacy meta_ver 2 with legacy checksums (uses_legacy_checksum); load_or_convert normalizes on rewrite |
crates/filemeta/src/filemeta.rs, crates/ecstore/src/set_disk/read.rs |
| Container layout | 8-byte header, bin-length-prefixed msgpack header block, CRC trailer, optional inline data (MinIO XL2 v1 shape) | crates/filemeta/src/filemeta/codec.rs |
| Erasure coding | Reed-Solomon Vandermonde, rs-vandermonde identifier, codec crate rustfs-erasure-codec |
Cargo.toml, erasure-coding.md |
| Bitrot | HighwayHash256S default; HighwayHash256SLegacy (fixed key) for older shards |
crates/ecstore/src/io_support/bitrot.rs, crates/ecstore/tests/legacy_bitrot_read_test.rs |
| Inline data | Inline block after the CRC trailer; null / version-id keying via data_key_for_version; physical_data_dir accounting |
crates/filemeta/src/filemeta.rs, crates/filemeta/src/filemeta/inline_data.rs |
MinIO stores an inlined object body as [HighwayHash256 (32 B)][body]. Feeding the raw inline shard through RustFS's BitrotReader with HighwayHash256S verifies the checksum and yields the exact payload; the bitrot prefix is not a format incompatibility.
Part B — Bucket Metadata (.metadata.bin)
| Aspect | Contract | Where |
|---|---|---|
| Path | buckets/{bucket}/.metadata.bin under the meta bucket (BUCKET_METADATA_FILE, save_file_path) |
crates/ecstore/src/bucket/metadata.rs |
| Header | 4 bytes: format: u16 LE + version: u16 LE, stripped before unmarshal |
check_header in crates/ecstore/src/bucket/metadata.rs |
| Body | MessagePack-encoded BucketMetadata; field names map one-to-one onto MinIO's bucketMetadata (PascalCase on the wire) |
BucketMetadata in crates/ecstore/src/bucket/metadata.rs |
| Per-config encoding | XML for S3-XML configs, JSON for policy / quota / targets / ACL; the per-config filename constants (policy.json, lifecycle.xml, ...) are update_config field-selector keys, not separate files |
update_config, parse_all_configs in crates/ecstore/src/bucket/metadata.rs |
| RustFS-only fields | bucket_targets_config_meta_json, table_bucket_config_json; a MinIO reader ignores unknown msgpack fields |
crates/ecstore/src/bucket/metadata.rs |
| Partial interop | bucket_targets meta side-channel is RustFS-specific; bucket_acl round-trips as a blob but only canned ACLs are enforced (see minio-rustfs-router-compatibility.md) |
Importer
crates/ecstore/src/bucket/migration.rs is a one-way, idempotent importer from a MIGRATING_META_BUCKET (.minio.sys) layout into .rustfs.sys, run at startup from rustfs/src/startup_bucket_metadata.rs:
| Function | Imports |
|---|---|
try_migrate_bucket_metadata |
buckets/{bucket}/.metadata.bin plus the replication resync blob (normalize_bucket_meta_blob via ReplicationMigrationBridge) |
try_migrate_iam_config |
config/iam/ records; normalize_iam_config_blob rewrites legacy timestamp and policy-mapping aliases |
Part C — Server-Side Encryption (SSE)
The xl.meta around a MinIO SSE object parses in every build, so such objects list, HEAD, and report plausible sizes; only payload readability depends on the build. KMS wire protocols (AWS awsJson1_1 client in crates/kms/src/backends/aws.rs, MinIO KES) are non-targets.
| Object class | default / full |
rio-v2 |
Requirement |
|---|---|---|---|
| SSE-S3 / SSE-KMS, MinIO builtin static KMS, single- and multipart | Fail closed, diagnosed | Read | RUSTFS_SSE_S3_MASTER_KEY (base64, 32 bytes) equal to the source MinIO's static secret. |
| SSE-C, MinIO-written | Fail closed, diagnosed | Read | Client supplies the customer key per request; MinIO stores no key MD5, so the AEAD unseal is the key proof. |
| Any SSE, MinIO backed by KES / KMS plugin / MinKMS | Fail closed | Fail closed | Not planned. Re-encrypt or decrypt on the MinIO side first. |
| Bucket default-encryption configuration | Round-trips | Round-trips | A config blob; it does not make existing ciphertext readable. |
Seams
The cryptography (DARE v2 stream format, object-key derivation, sealing) was never the gap. Three metadata seams above it rejected MinIO objects; all are closed in rio-v2 builds. Symbols are in rustfs/src/storage/sse.rs unless noted.
| Seam | Resolution |
|---|---|
Managed-SSE detection required the persisted public x-amz-server-side-encryption key, which MinIO synthesizes at response time |
infer_minio_managed_sse_type infers the scheme from which MinIO sealed-key slot is present; the slot also selects the sealing-key domain, so a wrong inference cannot derive a wrong key. |
| MinIO's wrapped-DEK ciphertext was accepted by no envelope parser | decrypt_minio_kms_data_key implements MinIO's builtin-KMS sealing for both the raw sealed‖iv‖nonce layout and the legacy {"aead": ...} JSON. Routing is by byte shape: LocalSseDekEnvelope (deny_unknown_fields) is recognized positively, everything else goes to the MinIO decoder. |
| SSE-C detection keyed on the stored customer-algorithm header, which MinIO also never persists, and demanded a stored key MD5 | stored_ssec_metadata accepts MinIO's SSE-C sealed-key slot (rio-v2 only); verify_ssec_key_match tolerates a missing stored MD5 for exactly that shape. |
| Multipart classification used an ETag-length heuristic (MinIO stores encrypted ETags) | Trusts MinIO's own X-Minio-Internal-Encrypted-Multipart marker (crates/utils/src/http/header_compat.rs). |
How default builds fail
is_object_encryption_marker (crates/utils/src/http/header_compat.rs) matches the whole x-minio-internal-server-side-encryption- prefix, so ObjectInfo::is_encrypted() is true and the read plan refuses to construct a reader without decryption material. The refusal is a typed error that names the MinIO-compatible sealed format and the rio-v2 read path it requires, surfaced as S3 InvalidObjectState (non-retryable). Ciphertext is never served as plaintext.
Reverse direction
Under rio-v2 RustFS writes its own DEK envelope into MinIO's sealed-key metadata slots labelled with MinIO's seal algorithm, so the metadata is MinIO-shaped while the key bytes are not MinIO-openable. Default builds do not populate those slots. Treat RustFS-written SSE objects as readable only by RustFS. Known unverified edge: MinIO seals ETags on SSE objects; RustFS does not unseal them, so ETag display and If-Match on migrated SSE objects are not guaranteed to match MinIO.
Migration options for encrypted objects
- Static-KMS source: run the migration through a
rio-v2build with the shared master key, serving in place or copying into a default-build cluster (the copy re-encrypts under RustFS's own KMS). - KES / MinKMS source, or no special-purpose build wanted: decrypt on the MinIO side (rewrite as plaintext, or copy out through MinIO's S3 endpoint) and let RustFS encrypt on ingest.
- Leave encrypted objects on MinIO and migrate only unencrypted data.
Inventory the source first: bucket default encryption means objects can be encrypted without the uploader asking, so "we never set SSE headers" is not evidence that a bucket has no encrypted objects.
rio-v2 variant lifecycle
rio-v2 is a dormant, special-purpose migration variant tracked under rustfs/backlog#1835.
| Fact | Value |
|---|---|
| Shipping status | Ships in no default build: absent from default and full in rustfs/Cargo.toml; released binaries and container images never include it. Enable with --features rio-v2. |
| Pull-request coverage | test-and-lint-rio-v2 in .github/workflows/ci.yml: clippy plus cargo nextest for rustfs and rustfs-ecstore with --features rio-v2. This is the cfg-seam guard; it keeps the feature compiling and its unit suite green on every pull request. |
| Full-suite lane | build-rustfs-debug-binary-rio-v2 and e2e-tests-rio-v2 in ci.yml run only on the weekly schedule and manual dispatch (cache-warm.yml keeps the ci-feat-rio cache warm so the scheduled build fits its timeout). |
| Interop evidence | .github/workflows/minio-interop.yml (nightly plus manual) regenerates real MinIO backend trees via crates/rio-v2/tests/minio_fixture_lab/ and runs the #[ignore] reader tests in rustfs/src/storage/minio_generated_read_test.rs with --features rio-v2. Its freshness is tracked in .github/scheduled-validations.json. |
| Promote-or-delete condition | The variant stays dormant until one of two things happens. Promote: a release commits to shipping MinIO SSE migration as a supported capability; then rio-v2 joins default/full, the scheduled lanes run on every pull request, and this section is rewritten. Delete: no release commits to it and the scheduled lanes are not kept green; then the feature flag, crates/rio-v2, the cfg seams in rustfs/src/storage/sse.rs, the three ci.yml jobs, the cache-warm.yml warm step, minio-interop.yml, and its scheduled-validations.json entry are removed in one change. Either outcome must update ARCHITECTURE.md and the ci.yml job comments that cite this section. |
Fixture Evidence
Fixtures were captured from a real MinIO single-drive instance and live under crates/filemeta/tests/fixtures/minio/ and crates/ecstore/tests/fixtures/minio/. These tests run in the normal cargo test / nextest lanes.
| Test | File | Proves |
|---|---|---|
parses_real_minio_object_xlmeta |
crates/filemeta/src/filemeta.rs |
Inline, two-version plus delete-marker, and multipart xl.meta parse to the expected FileInfo. |
parses_real_minio_bucket_metadata_blob_without_loss |
crates/ecstore/src/bucket/metadata.rs |
The msgpack blob decodes via MinIO's field names and parse_all_configs loads every config in the corpus, including MinIO's lifecycle <ExpiryUpdatedAt> and replication DeleteMarkerReplication / ExistingObjectReplication extensions. |
reads_minio_inline_bucket_metadata_via_bitrot |
crates/ecstore/src/bucket/metadata.rs |
The inline shard's HighwayHash prefix verifies under HighwayHash256S and yields the exact .metadata.bin blob. |
migrates_real_minio_bucket_metadata_end_to_end |
crates/ecstore/src/bucket/migration.rs |
A real .metadata.bin seeded under a .minio.sys layout is imported by try_migrate_bucket_metadata into .rustfs.sys byte-identical, through the object layer on a 4-drive ECStore. |
test_issue_2265_legacy_meta_v2_object_compatibility, test_issue_2288_legacy_xlmeta_compatibility |
crates/filemeta/src/filemeta.rs |
Legacy meta_ver 2 objects with legacy checksums still read. |
minio_generated_read_test.rs (#[ignore], rio-v2) |
rustfs/src/storage/minio_generated_read_test.rs |
Byte-identical plaintext reconstruction of MinIO SSE-S3 / SSE-KMS fixtures; driven by minio-interop.yml. |
Not fixture-proven: transitioned xl.meta; CORS, public-access-block, and bucket-ACL configs (the SNSD corpus did not exercise them); bucket-targets credentials (MinIO stores them KMS-encrypted); the SSE-C fixture-lab lane (customer-key handout is not wired; SSE-C coverage is unit-level).
Out Of Scope
- A live MinIO binary serving a RustFS-written drive set (set-level
.minio.sysvs.rustfs.sysdivergence). A bidirectional round-trip would require RustFS to optionally write the.minio.sysset layout, which is a separate feature. - RustFS-written SSE objects readable by MinIO.
- KES / MinKMS / KMS-plugin-sealed MinIO objects.
- Objects sealed with pre-DARE-v2-HMAC MinIO seal algorithms;
parse_minio_managed_sealed_keyrejects unknown algorithms and the read fails closed. - AWS KMS and KES wire-protocol compatibility.
Guardrails
- Any change to
crates/filemetaorcrates/ecstore/src/bucketmetadata encoding is a storage-format change and follows the migration and readiness contracts in README.md and the ecstore layout boundary rules. - Do not bump a Version Anchor without a read path for the prior value; see erasure-coding.md for the accept-older, reject-newer rule.
.github/workflows/ci.yml,.github/workflows/cache-warm.yml, andARCHITECTURE.mdcite the rio-v2 variant lifecycle heading; keep it when editing this file.