Changelog¶
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
0.34.1 — 2026-10-30¶
v0.34.0 was tagged but never published: the scans of its release run found the advisories under Security, so no image, binary or GitHub release exists for it. 0.34.1 is the first release with the changes below.
Added¶
- A benchmark suite that compares Arca with MinIO:
bin/perf-test suite. It runs the same warp scenarios against both servers on the same host and storage: small PUT, GET and HEAD at several concurrencies, 64 MiB PUT and GET, ranged GET, multipart upload, a mixed workload, DeleteObjects, ListObjectsV2 and a versioned workload. Each server runs in three configurations: plain, SSE-S3 and compression. The suite also times ListObjectsV2 walks over a seeded key tree, with and without a delimiter, on both servers, and runs the SDK-side boto3 suite. For Arca it adds the offline maintenance commands (fsck,fsck --verify-checksums,gc,compress-existing,recover) on a seeded store, with each command's peak memory. - Isolation. Every server starts fresh, in a compose project of its own with no fixed host port, so the suite never touches a running
bin/arca start. - Output. Everything goes into one run directory under
perf-results/, which is git-ignored. It holds the raw warp benchdata and logs, the JSON results, the server metrics and logs, asummary.json, andreport.html, which links every raw file. - Options.
--profile quick|full,--targets,--variants,--scenarios,--storage volume|tmpfsand--release <tag>(benchmark a published image). - Comparison with a baseline.
--baseline <run-dir>compares the report with an earlier run, andbin/perf-test report <run-dir> [--baseline <run-dir>]regenerates the report of a finished run in a plain Python container, with no server and no compose project. For each scenario and server of the run, the report adds the baseline's figure as a third bar between Arca and MinIO, with the change since then. When the run did not measure MinIO, as in a targeted run of Arca only, it takes MinIO's figures from the baseline. The listing, maintenance and boto3 tables gain the baseline's figures and the change too. The report warns when the two runs were not made the same way (host, Docker resources, profile, duration, storage, MinIO image, dataset sizes). - Documentation and tests. See the new Performance Testing page under Operations.
bin/test perf-reporttests the report generator and the runner;bin/test scriptstests the orchestration. - Licensing. warp and MinIO run as separate test-only containers: neither is a dependency of Arca. The default MinIO image is Chainguard's build of upstream MinIO, because MinIO no longer publishes server images on Docker Hub.
bin/perf-testreports the aggregate request rate. Itsops_per_secdivides the operations by the sum of their latencies, which is the rate of a single client, not the rate the server sustained under concurrency. A newwall_ops_per_secdivides the operations by the elapsed time of the run.ops_per_seckeeps its old meaning, so old baselines still compare.perf_test.py --json-out <path>writes the JSON results to the given path.
Changed¶
- The console dashboard recognises TLS terminated by a reverse proxy. Behind a proxy that holds the certificates, Arca listens on plain HTTP and the Transport field showed "HTTP", although the browser reached Arca over HTTPS. When the console's Arca endpoint is
https://and Arca has no TLS of its own, the field now shows "TLS · proxy" in green, with a tooltip saying the last hop, from the proxy to Arca, is plain HTTP. - A GET of a whole SSE-S3 object is several times faster. The decrypting read pulled the file in 4 KiB reads, each a round trip to tokio's blocking pool (16 per 64 KiB chunk), where a plain GET reads 64 KiB at a time. It now reads one whole on-disk chunk at a time and decrypts each chunk in place, without copying it twice. GET of a 64 MiB SSE-S3 object, interleaved A/B on the suite's reference host: 12.0 to 67.5 objects/s with 4 clients, 14.9 to 159.5 with 16 (MinIO in the reference suite run: 45 and 117). Ranged GETs, which already read their range in one go, are unchanged.
- A GET of a whole compressed object costs half as much CPU. It opened the blob twice, once for the footer's chunk count and once for the chunks, which doubled the file operations, each a round trip to tokio's blocking pool; and zstd created and freed a decompression context for every chunk, which cost more than decoding a small chunk. A whole-object GET now reads the blob once: the object size in the sidecar says where the chunks end, and the footer is checked against the chunks as the read reaches it, so a truncated or corrupted blob is still an error. zstd keeps one decompression context per thread. GET of a 1 KiB object in a zstd bucket, interleaved A/B on the suite's reference host: 19,700 to 29,800 objects/s with 16 clients, 20,400 to 43,300 with 64, the same as an uncompressed bucket (30,200 and 44,800). GET of a 64 MiB compressed object is unchanged, and so are ranged GETs.
- A compressed PUT spends less CPU on zstd. Every chunk created a zstd compression context, sized it for a stream of unknown length (most of the cost: zeroing tables far larger than a small chunk) and freed it, about 18% of the server's CPU on 1 KiB PUTs where the compression itself took under 1%. zstd now keeps one compression context per thread and encodes each chunk with its size known up front; the frames are standard zstd frames, and blobs written before read as they did. PUT 1 KiB to a zstd bucket with
[storage] fsync = false, interleaved A/B on the suite's reference host: 5,383 to 5,890 objects/s with 16 clients, 5,200 to 5,506 with 64 (uncompressed: 6,372 and 5,676). With the defaultfsync = truethe disk's fsync is the limit and the rate is unchanged; so is PUT of a 64 MiB object. - Small requests are several times faster. On the suite's reference host (14 CPUs, Docker volume storage), GET of a 1 KiB object with 16 clients went from 2,660 to 31,794 objects/s (MinIO: 33,350) and HEAD from 3,214 to 44,234 (MinIO: 41,915). The time went into per-request overhead, not into storage:
- The shared server state is one pointer. Every middleware and handler cloned an 840-byte struct of about 30 reference-counted fields on every request.
- mimalloc is the global allocator. The static musl binary used musl's allocator, which serialises every allocation on one global lock. mimalloc (MIT) keeps per-thread heaps;
THIRD-PARTY-NOTICESattributes the crate and its vendored C sources. - SQLite reuses its prepared statements. Every call compiled its SQL again; each connection now caches up to 256 statements, and listing page sizes are bound parameters, so one statement serves every page size.
- Identity lookups are cached for 5 seconds. Every authenticated request read the credential, its user and the user's policies from the database, and the audit log read the credential again. Any change to credentials, users, teams or grants empties the cache at once, on every cluster node it replicates to, so a revoked credential is refused on the next request. Only a change written by another process, such as the offline
arca credentialandarca usercommands run against a live server, waits for the entries to expire. - A GET reads the blob's sidecar once, instead of once per layer of the blob stack (compression, encryption, file store, cluster read repair), and once more per inner range read of a compressed object.
- A GET or HEAD counts the object's tags in the object query instead of a second query, for
x-amz-tagging-count. - Unversioned PUTs write less. An unversioned PUT replaces every row of its key, so the new row is already the latest one: the
is_latestfix-up that rewrote it twice is skipped, on both backends. The fix-up also rewrote every version of the key on each versioned PUT; it now touches only the rows that change. - PostgreSQL writes to different keys no longer wait for each other on a single node. Every object write took the same counter row as its first statement and held it until commit, so the whole database wrote one object at a time: 790 PUTs/s with 16 clients and the same with 64. That counter orders the changes a cluster node hands to its peers, which a single node does not need. A single node now locks only the key it writes (a transaction-level advisory lock, so conditional writes stay exact) and numbers its rows from a sequence (PostgreSQL migration 0018, applied at startup). PUT 1 KiB: 798 to 1,808 objects/s with 16 clients, 792 to 2,340 with 64; the commit's WAL flush is now what PUTs wait for. DeleteObjects applies its whole request in one transaction here too: 660 to 15,235 objects/s. Cluster mode is unchanged. When the store opens, both numbering schemes are raised above every row, so switching between single node and cluster stays safe.
fsck,gc,encrypt-existinganddecrypt-existingnow open the database in the node's mode ([cluster] enabled), as the server and the other commands already did, so a command running next to a cluster node takes the same lock as the server. See decision H13 in the HA design decisions. - A PUT with tags is one transaction. PutObject with
x-amz-tagging, CopyObject (its tags resolved before the write, from the request underREPLACEor from the source underCOPY) and CompleteMultipartUpload (the tags given at CreateMultipartUpload) wrote the object row, then its tags in a second transaction: a second commit, a second fsync. They are now written together, on both backends. PUT 1 KiB with tags, durable SQLite, 16 clients: 486 to 892 objects/s, as fast as without tags. The version is never visible without its tags, also to notifications and outbound replication. A failed tag write now fails the request with 500, where it was logged and answered 200 with the object left untagged. In a cluster the tags still replicate to peers as a separate best-effort message (TD-033). - Concurrent writes on SQLite share one commit (group commit). The single SQLite writer committed every write on its own, and with
[storage] fsync = trueevery commit is an fsync of the WAL, so small writes were capped at one per fsync of the database disk whatever the number of clients (TD-048). The request-path writes (PUT, CopyObject, DELETE, DeleteObjects, the multipart writes, object tags, replication journal entries and notification events) now queue while a commit is in flight and the queued ones commit together in one transaction, one fsync for the group. Each write runs under its own savepoint, so a refused precondition or an Object Lock refusal still fails that request alone, and each request is answered only once the transaction holding it is durable. There is no waiting for a group to fill: a lone write commits at once, as before. Durable SQLite, on the suite's reference host: PUT 1 KiB with 16 clients 924 to 1,509 objects/s, with 64 clients 888 to 2,293, PUT 64 KiB with 16 clients 805 to 1,139, DeleteObjects 4,522 to 8,750; a single client is unchanged. The gain grows with the disk's fsync latency. - DeleteObjects applies the whole request in one transaction on SQLite, one commit (one fsync) instead of one per key, with a savepoint per key, so a key that fails (a precondition, an Object Lock) fails alone: 966 to 4,781 objects/s. A cluster still deletes key by key, because each key is replicated with its own quorum.
Fixed¶
- A SQLite write under heavy read load no longer fails with "database is locked". The single writer opened its transactions as
DEFERRED, and most of them read before they write, so they asked for the WAL write lock as read transactions, a case where SQLite returnsSQLITE_BUSYat once instead of waitingbusy_timeout(5 s). A reader holds that lock for an instant when it reads a wal-index header the writer is updating, so under enough concurrent reads a write occasionally failed with 500 (seen as 7 failed PUTs out of 260,000 in a perf suite seed). The writer now opens its transactions asIMMEDIATE, which takes the lock atBEGIN, wherebusy_timeoutapplies; with a single writer this costs nothing. - A copy of an object onto itself keeps its tags. CopyObject with the default
COPYtagging directive read the source's tags after writing the destination; when both are the same unversioned object (a copy that changes the metadata), the write had already removed the old version's tags, so the copy came out untagged. The source's tags are now read before the write. AREPLACEcopy with an invalidx-amz-taggingis now refused withInvalidTagbefore anything is written, as PutObject does; it used to succeed and leave the copy untagged. - Prefix filters on SQLite match case exactly. ListObjects, ListObjectVersions, ListMultipartUploads and the lifecycle rules selected keys with SQL
LIKE 'prefix%', which SQLite compares ignoring ASCII case: a listing with prefixphotos/also returnedPhotos/andPHOTOS/keys, and a lifecycle rule forlogs/also expiredLogs/objects. The filter is now a key range (key >= prefix AND key < next), which matches bytes as S3 does and is an index seek instead of a scan. PostgreSQL'sLIKEwas already case-sensitive. - PostgreSQL lists in S3 byte order whatever the database locale. On a database initialised with a glibc locale (
en_US.UTF-8, the default of the Debian-basedpostgresimage and of most distribution packages) keys, version ids, upload ids and bucket names sorted by that locale, which ignores case and punctuation at first:a_ccame beforeA/xandB. Listings were out of S3 order, and ListObjects with a delimiter, which compares keys in Arca's own code, could skip or repeat entries across pages. PostgreSQL migration 0017 (applied at startup) setsCOLLATE "C"on those columns. On such a database the listing order changes to the S3 one. The migration rebuilds the indexes of theobjects,multipart_uploadsandbucketstables, so the first start takes longer on a large database.bin/test postgresnow runs on the Debian image, where this kind of bug shows up; the Alpine image collates by bytes and hid it. - Lifecycle rules delete only what S3 deletes, and all of it.
- Every match is processed. Each rule handled the first 100 matches per cycle (one cycle per hour by default) with no cursor: a bucket with many expired objects took weeks to catch up, and 100 locked noncurrent versions at the start of the bucket stopped the rule for good. The evaluator now pages through every match each cycle.
AbortIncompleteMultipartUploadhonours the rule's prefix. It aborted the stale uploads of the whole bucket, also those outside the rule's scope.NoncurrentDayscounts from when the version became noncurrent, the time the next newer version or delete marker was written, not from the version's own creation: a version created a year ago and replaced yesterday was deleted at once, where S3 keeps it forNoncurrentDaysmore.NewerNoncurrentVersionsis enforced. It was stored and returned but ignored, so the newest noncurrent versions the rule keeps were deleted too.- The tag filter applies to noncurrent versions. NoncurrentVersionExpiration ignored it and expired the versions without the tag as well.
Each of these deleted data that S3 keeps; after the fix a rule may delete less than before, never more. Two gaps remain, both on the side of deleting less: expired object delete markers and noncurrent delete markers are never removed (TD-056).
- ListObjectVersions honours delimiter. It was read and ignored, so a delimited listing returned every version of every key and no CommonPrefixes. The versions of the keys that have the delimiter after the prefix now roll up into one common prefix, which counts once towards max-keys, the response echoes Delimiter, and a page that ends on a common prefix returns it as NextKeyMarker with no NextVersionIdMarker, as S3 does.
- The per-IP rate limit tells clients apart. The server never passed the TCP peer address to the request handlers, so every client without an X-Forwarded-For header shared one limiter bucket: with rate_limit_per_ip_per_second set, the whole server was limited as if it had a single client. The audit log also recorded no source address for direct clients; it now records the peer address. X-Forwarded-For is still trusted from any client, so a client that reaches Arca directly can evade the per-IP limit (TD-055).
- A delimited listing reads only what its page returns. ListObjects (V1 and V2) and ListObjectVersions with a delimiter read every key, or every version, under each common prefix before moving to the next one: a listing at the root of a bucket read the whole bucket, however few prefixes it returned, and one request could keep the database busy for as long as the bucket was large. Once a listing reaches a common prefix, its next query now starts past every key under it, so a page costs at most one query of 100 rows per item it returns. The three listings share the same grouping code.
- DeleteBucket no longer counts the bucket's objects to see whether it is empty (nor does enabling Object Lock on an existing bucket). The check was COUNT(*) ... LIMIT 1, which counts every row before applying the limit; it is now an EXISTS that stops at the first row.
- The documented default of server.runtime.max_blocking_threads was wrong. The configuration guide and the code comments said 0 meant num_cpus; it leaves tokio's own default, a pool of up to 512 threads that grows on demand, which is what the server has always run with. The guide now says so, and advises against a low cap: every file operation runs in that pool.
Security¶
- musl CVE-2026-107891 in both images. The console's Alpine base shipped musl 1.2.6-r2; it moves from Alpine 3.24.1 to 3.24.3, which ships musl 1.2.6-r3, openssl 3.5.9-r0 and zlib 1.3.2-r1, so the console Dockerfile no longer needs its openssl and zlib constraints and has none. The
arcabinary links musl statically, where the image scan cannot see it because the scratch image has no package database, and the Rust builder image still ships 1.2.6-r2: the builder now requiresmusl-dev >= 1.2.6-r3. Earlier binaries and images embed musl 1.2.6-r2. - rustls 0.23.46 (
Cargo.lockonly, a compatible point release) for GHSA-c8m5-c952-6cm4 (MEDIUM: theDebugoutput of a session exposed TLS secrets) and three LOW advisories: GHSA-h2v8-394m-p7vv (Brotli certificate decompression could allocate an attacker-chosen amount of memory), GHSA-vp88-798v-h67f (an ECH rejection could expose buffered application data) and GHSA-w44r-93j7-qv28 (an unauthenticated ServerHello could poison the per-name key-exchange record).
0.34.0 — 2026-10-30 [YANKED]¶
Tagged but never published; its changes ship in 0.34.1.
0.33.0 — 2026-10-17¶
Added¶
- Run the published images without building. The images on ghcr.io are public, so
bin/arca start --release <tag>,bin/console start --release <tag>andbin/cluster up --release <tag>pulllatest,X,X.YorX.Y.Z(no login, no Rust toolchain, no build) instead of building the working tree. The compose files read the tag fromARCA_RELEASE;bin/arcasaves it in.arca-envso the console andbin/arca stopfollow it.--releasecannot be combined with--buildor--dev, andbin/buildnever tags a local build with a release tag. - The publish workflow verifies the images are public. After tagging, every tag of both images is fetched from the registry API with an anonymous token (no docker login, plugins or builders involved) and must list
linux/amd64andlinux/arm64; a package that went private fails the release.bin/build --pushruns the same check, after both images are pushed. - A "Choosing a tag" guide in the installation documentation:
latestfor the newest release,X.Yfor fixes only,Xfrom 1.0,X.Y.Zfor business-critical production. The Kubernetes manifests and the deployment guide now uselatestand point at it.
Changed¶
bin/console startandbin/cluster upfollow the same rule (run the existing image, else pulllatest,--devbuilds), and the console warns when it runs a different version from the server.bin/clusterdoes not remember--release.bin/arca startno longer builds a production image implicitly. It used to let Compose build when the image was missing; it now pulls the publishedlatestinstead, asdocker rundoes with a missing image (--devhas no published image, so a missing one is built).bin/test integrationhas no known failures left. The cluster, replication and notification-delivery tests need names (arca-1,arca-replica,webhook-receiver) that exist only in the compose project of their own mode, so against the developer's server 61 of them failed (and 7 cluster tests passed against a single node standing in for the load balancer). They now skip unless their mode set the variable that points at those services, and run inbin/test cluster,bin/test replicationandbin/test notifications.bin/test clusterjoined the other self-contained modes: its own compose project, ephemeral loopback ports (it no longer collides with a running server on port 9000), andbin/cluster partition/healfollow it throughARCA_CLUSTER_PROJECT. Newbin/test fullruns unit, integration, notifications, replication and cluster as separate stages and names the ones that failed.- The console image now listens on ports 8080 (HTTP) and 8443 (HTTPS) instead of 80 and 443: remap your port mappings and proxies (TD-027). HTTP is now on 8080 (was 80) and HTTPS on 8443 (was 443), the nginx binary no longer carries the
cap_net_bind_servicefile capability, and the container user is renumbered from100:101to10001:10001, so the container runs with every capability dropped and the Kubernetes manifest adds none back. This is incompatible for anyone running the published console image with their own setup: a port mapping-p X:80/X:443must become-p X:8080/X:8443, a reverse proxy pointing atconsole:80must point atconsole:8080, a pod that setsrunAsUser: 100andrunAsGroup: 101must use10001for both, and a manifest that declarescontainerPort: 80must declare 8080. The bundled Compose files (host ports 9080 and 9443 are unchanged),deploy/kubernetes/arca-console.yaml(Service still on port 80) and the documentation are already updated. TLS key access is unaffected: the console still reads the key through Arca's service GID 65532.trivy fsnow reports no misconfiguration or vulnerability; KSV-0125 (ghcr.io is not in the check's registry allow-list) and KSV-0013 on the cluster manifest (latestis its documented evaluation default) are accepted in.trivyignorewith their reasons. - The console is documented like the server. Every way of running Arca now has its console counterpart with the same structure: the published image with
bin/console start -d --release latestor a plaindocker run(no repository needed,ARCA_ENDPOINT, ports 8080/8443 inside the container, 9080/9443 on the host), the HTTPS variant, the Compose service and how to stop it. The documentation also told readers to runbin/console start --tls, a flag that does not exist: the console inherits TLS from the Arca server it follows.
Fixed¶
- A completed multipart upload gets an object checksum, and keeps its part layout (TD-046). CompleteMultipartUpload stored no checksum and compared nothing: not the part checksums of the XML, not a whole-object
x-amz-checksum-*header. CreateMultipartUpload now takesx-amz-checksum-algorithmandx-amz-checksum-type(COMPOSITEorFULL_OBJECT;CRC64NVME, the AWS CLI default, only has the full-object form,SHA1andSHA256only the composite one; contradictory or unknown values answer400 InvalidRequest). UploadPart computes the checksum when the client declares none and refuses another algorithm's. Complete builds the checksum from the part checksums, without reading the object (a CRC combination forFULL_OBJECT, checked against the Ceph s3-tests vectors), and refuses with400 BadDigesta whole-object header or a part checksum that does not match, and with400 InvalidRequesta wrongx-amz-mp-object-sizeorx-amz-checksum-type, all before anything is assembled. The checksum, withx-amz-checksum-type, is returned by Complete, HeadObject and GetObject (checksum mode) and GetObjectAttributes. Every object completed from parts also keeps the layout of its parts, whichGETandHEADwith?partNumber=N(206,x-amz-mp-parts-count, the part's checksum) and theObjectPartsof GetObjectAttributes (paginated, per-part checksums) now serve; it is read on demand, so a GET, HEAD, listing or PUT costs the same whatever the number of parts. GetObjectAttributes also named the part countTotalPartsCountinstead ofPartsCount, so SDKs never read it. CopyObject keeps aFULL_OBJECTchecksum and drops aCOMPOSITEone (the copy has no parts). Uploads created without a checksum algorithm still get no object checksum, and objects completed before this release have no part layout (?partNumberanswers400 InvalidPartfor them). Adds theobjects.partsandmultipart_uploads.checksum_typecolumns (SQLite migration 28, PostgreSQL 0016, applied automatically). Measured against the previous release on the project's perf suite, no request type is slower (all within 1%). - The chunks and the trailer of signed aws-chunked uploads are verified (TD-045). For
STREAMING-AWS4-HMAC-SHA256-PAYLOADbodies (and the-TRAILERvariant) thechunk-signature=of each chunk and thex-amz-trailer-signaturewere read and thrown away, so only the headers were authenticated: a client that sends no digest, such asmcover plain HTTP, had its body checked by nothing, and anyone able to alter the stream could change what was stored. PutObject and UploadPart now verify the signature chain as the body streams (each chunk signs its data and the previous signature, the first chain on the request signature). A modified byte, a bad or missing signature, a reordered or replayed chunk, a body cut anywhere (the signed final chunk is required) or a signed-chunk body under a presigned URL answers403 SignatureDoesNotMatchbefore the blob is committed: nothing is stored and an existing object is left untouched. Clients that sign chunks (MinIO clients over plain HTTP) keep working; boto3 and the AWS CLI useSTREAMING-UNSIGNED-PAYLOAD-TRAILER, which has no chunk signatures and still relies on the declared checksum (TD-034). Checked against the AWS chunked-upload example vectors, an independent signer in the new integration tests andmc. Verifying the signatures hashes every byte of a signed-chunk upload: measured on the project's perf setup, such an upload (mcover plain HTTP) runs about 16% slower, 392 against 465 MB/s on a 128 MiB body; every other upload and request is unaffected. - Objects, delete markers, buckets and multipart uploads record the user who created them (TD-042). PutObject, CopyObject (the copier, not the source owner) and CompleteMultipartUpload (the user who completes it) stored
rootas owner whatever the credential; delete markers, buckets and multipart uploads did the same.Ownerin ListObjects (V1 now returns it too, as AWS does), ListObjectsV2 withfetch-ownerand ListObjectVersions, andOwner/Initiatorin ListParts and ListMultipartUploads now show that user's name. TheuserIdentity.principalIdof notification events carries it as well (it was empty). Objects, buckets and uploads created before this release keeproot: the real writer was never recorded and cannot be recovered. Lifecycle delete markers andarca recoverstill useroot. Multipart uploads gain aninitiatorcolumn (SQLite migration 27, PostgreSQL 0015, applied automatically). TheMetadataStoretrait gainscreate_bucket_ownedand amarker_ownerargument ondelete_object_if. In a cluster, nodes on an older version keep writingrootfor the writes they serve. - The Kubernetes console manifest and the tooling Dockerfiles are tightened for the trivy misconfiguration scan (TD-027, TD-028).
deploy/kubernetes/arca-console.yamlpins the console image to the release tag, runs with a read-only root filesystem (emptyDirvolumes for nginx's writable paths, an init container seedingindex.htmlandhttp.d) and drops every capability (possible thanks to the unprivileged ports under Changed). The screenshots image runs as the unprivilegedpwuser, and the screenshots, perf, test and s3-tests images declareHEALTHCHECK NONE. The pinned tag is now a version location to bump at every release. The port and UID/GID findings are closed by the unprivileged-port change under Changed. - An abort racing CompleteMultipartUpload no longer destroys the completed object's data (TD-053). Complete committed the object row and only afterwards consumed the upload, so an
AbortMultipartUpload, a lifecycleAbortIncompleteMultipartUploadrule orarca fsck --repair --abort-uploads-older-thanlanding in between deleted the parts the new object was made of: Complete answered 200 and the object could not be read (InternalError). The object row and the consumption of the upload are now one transaction in SQLite and PostgreSQL, so whichever commits first wins: an abort that wins makes the Complete answerNoSuchUploadand write nothing, and a Complete that wins leaves the abort with no parts to delete. A refusedIf-Match/If-None-Matchstill leaves the upload intact for a retry. On an HA cluster the guarantee holds per node. - A
version-id-markerdeleted between two pages no longer skips the rest of its key (TD-054). ListObjectVersions looked the marker version up to find where to resume, and when it was gone resumed after the whole key. The internal full listing (re-encryption scan,arca fsck) now pages by the position of the last row it read, so it cannot skip a version even under concurrent deletes (for example lifecycle noncurrent expiry). On S3, aversion-id-markerthat does not exist (never did, or deleted by a client purging the bucket page by page) restarts the listing from the beginning of thekey-marker's key and answers200: a version of that key can be listed twice, none is skipped. UploadPartCopyhonours the copy-source conditional headers (TD-035).x-amz-copy-source-if-match,-if-none-match,-if-modified-sinceand-if-unmodified-sincewere evaluated byCopyObjectonly, so a part was copied even when the condition failed. They now answer412 PreconditionFailedand store no part.- Virtual-hosted-style addressing works (TD-037). With
[server] domainset,bucket.<domain>/keywas routed as bucketkey(andbucket.<domain>/as ListBuckets) because the rewrite ran after routing. It now runs before routing, in the same layer that saves the signed URI, so the signature still verifies; the bucket root maps to the bucket, and a virtual-hosteddir/stays a directory-marker key. The host is read fromHostor, on HTTP/2, the URI authority. New self-containedbin/test virtual-host. - Blob ids from cluster peers are validated (TD-041). An id inside a replicated object row, a composite part of a received sidecar or an anti-entropy manifest row was used as a file name unchecked, so a hostile or corrupt peer could make a node address paths outside its blob tree or panic on a short id. Such ids are now refused: the cluster endpoints answer 400, anti-entropy skips the row, and the blob store itself rejects any id that is not a UUID. Files under
blobs/whose name is not a blob id (a stray file, a copied backup) are no longer treated as blobs:arca gcused to delete them as orphans, now it leaves them alone,arca fscklists them asFOREIGN_FILEwith their size, andarca gc --reclaim --delete-foreign-filesremoves them on request. - Config-drift detection compares
cluster_size(TD-036). The fingerprint nodes exchange hashed the write quorum but notcluster_size, so a resize between 2 and 3 (or 4 and 5) left a node with a stale size unflagged. A second fingerprint now includes it (quorum mode only: inavailablemode the setting has no effect). The ping sends both, and a node compares the new one only with a peer that sends it, so a rolling upgrade from an older version shows no drift and loses no write quorum;cluster_sizedrift is caught between upgraded nodes.
0.32.1 — 2026-10-15¶
Added¶
arca gcreports the space it frees: the dry run says how much--reclaimwould free, and--reclaimhow much it freed (Reclaimed 0 orphan blob(s) and 118 stale temp file(s), freeing 419.0 MiB.). An orphan counts with its sidecar, which is deleted with it; only the files actually deleted count toward the freed space.
Changed¶
- Plain HTTP is served by the same accept loop as HTTPS (
serve.rs) instead ofaxum::serve, so the two listeners behave alike: the bounded shutdown above, the[server.http]HTTP/2 settings (which only HTTPS applied), hyper's 30-second limit for receiving a request's headers, which also closes a keep-alive connection left idle for 30 seconds (only HTTPS had it; clients reconnect on their own), and, when accepting fails for a reason that is not about one connection (EMFILE, out of file descriptors), a 1-second pause before retrying instead of a busy loop (which only plain HTTP had).
Fixed¶
- Shutdown is bounded: a stopping server no longer hangs until it is killed. When the drain period ended, the HTTPS listener waited for every open connection to end by itself without telling any of them to close, so a single idle keep-alive connection kept the server running: in production, 137 idle client connections held a stopping server until systemd SIGKILLed it at
TimeoutStopSec, skipping the rest of the shutdown. Plain HTTP closed idle connections but waited for running requests with no limit, so one stalled upload did the same. Every connection is now told to close when the drain ends: an idle one closes at once, a busy one after its current request (HTTP/2 sendsGOAWAY), and those still running 10 seconds later are dropped, so a stop takes at mostdrain_timeout_seconds+ 10 seconds. The deployment guide also wrongly advised adrain_timeout_secondsat least as long as the Kubernetes termination grace period: the grace period must be the longer of the two. - The HTTPS listener no longer keeps every closed connection until shutdown. The task of each connection stayed in the listener's set after the connection closed, so memory grew with every connection accepted since the server started (and the connection count logged at shutdown was inflated by all of them). Closed connections are now dropped as new ones are accepted.
0.32.0 — 2026-10-15¶
Added¶
arca fsck --repair: fixes what the check finds, asking before each fix (y,n,afor the rest of the category,qto stop;--yesanswers yes to all). Stale temp files are deleted without asking; orphaned blobs and sidecars are moved to<data_dir>/lost+found/<timestamp>/(or deleted with--delete); the row of a version whose data is gone is deleted; a sidecar that disagrees with its row is rewritten from it, keeping its encryption, compression and part list; with--abort-uploads-older-than <days>, old unfinished uploads are aborted. It may run against a live server: files younger than the grace period are never touched (on a cluster node the grace is at least the tombstone grace), every item is checked again right before its fix (a row is deleted only if it still points at the missing blob, checked inside the delete's transaction), and the referenced set is recomputed before orphans are moved. Database fixes run first and are followed by a rescan, so the files they leave behind are reclaimed in the same run. Ctrl+C exits at once, or after the current fix if one is running, and a re-run continues, with no journal. Aborting an upload that a client is completing at that very moment loses the completed object, asAbortMultipartUploaddoes (TD-053): abort only uploads old enough to be abandoned. It refuses to start while an offline rewrite is unfinished, and on an HA cluster node it only repairs files: a missing blob is left to anti-entropy, which has it on a peer, and uploads are aborted through the S3 API.arca gcreclaims stale temp files (TD-051): the<blob>.tmpand sidecar temp files of interrupted writes, older than the grace period, are reclaimed byarca gc --reclaim, the single-node GC worker and cluster anti-entropy GC; the recovery files of an in-place rewrite (*.rewrite.tmp,*.meta.pending.tmp) are never touched.arca gcalso reports the unfinished multipart uploads and the bytes they hold, which it does not reclaim.arca fsckreports unfinished multipart uploads (TD-052): each upload that was never completed nor aborted is listed with its key, upload id, start date, part count and size, and the summary gives their total and the oldest one. They are valid state, so they do not make the exit code 1; the summary points atAbortMultipartUploadand theAbortIncompleteMultipartUploadlifecycle rule.arca fsck --grace-seconds(default 86400, asarca gc): files written within the grace period are skipped and counted apart instead of reported, so a check against a running server no longer flags the blobs and temp files of uploads still in flight. Use0only with the server stopped.arca fscksummary: one line per category with its count and, where it matters, the bytes involved (orphaned blobs, stale temp files, unfinished uploads).arca fsckandarca gcsupport PostgreSQL: both open the configured metadata backend.fsckused to open a SQLite file whatever the configuration said, andgcrefused PostgreSQL.
Fixed¶
- Interrupted writes no longer leave temp files behind (TD-051). A PutObject or UploadPart whose client disconnected left its partial
<blob>.tmp: the cleanup ran only on an error the write returned, never when the request was dropped. The fallback copy ofCompleteMultipartUpload(mixed or compressed parts) also left its temp file on any error, a missing part included, and every writer left one when the final rename failed. A guard now owns the temp file from its creation to the rename that publishes it, and removes it on every other way out. arca fsckunderstands multipart objects (TD-014, the fsck half). A completed multipart upload is stored as a composite: a sidecar listing its part blobs and no blob file of its own.fsckreported every multipart object asMISSING_BLOBplusORPHANED_SIDECAR, and every part, completed or still uploading, asORPHANED_BLOB: on a production server, 91,867 false findings that hid the real ones.fscknow computes what is referenced exactly asarca gcdoes (object rows, in-progress parts, and the parts of every live composite), so the two tools agree; a composite with a missing part is reported asMISSING_BLOBwith the missing part ids, and the sidecar and parts of a composite whose object is gone as orphans. Unreadable sidecars get their ownUNREADABLE_SIDECARcategory, and the recovery files of an unfinished in-place rewrite (*.rewrite.tmp,*.pending.tmp) are reported asREWRITE_RECOVERYinstead of stale temp files.- ListObjectVersions pagination no longer skips versions. Both metadata backends ignored
version-id-marker: a page that ended in the middle of a key's versions resumed at the next key, so the remaining versions of that key were never listed, and versions sharing a timestamp had no stable order across pages. The listing now resumes right after the marker version (nullfor the null version) in a total order. The same bug made theencrypt/decryptmaintenance jobs andarca fsckskip versions; all of them now share one paging helper. arca fsck --verify-checksumsno longer reports compressed objects as corrupt (the ETag hashes the plaintext, the file holds the compressed bytes), and no longer walks the whole blobs directory once per object to find each blob file.- Offline tools refuse a missing SQLite database instead of creating an empty one:
fsck,gc,encrypt-existinganddecrypt-existingnow stop withDatabase does not exist, so a wrongdata_dircannot make every blob look unreferenced.
0.31.2 — 2026-10-07¶
Added¶
arca --version(or-V) prints the version of the binary, so a host running a release binary can tell which one it has.
Fixed¶
- Large multipart objects can be downloaded again on a server with a low open-files limit. A GET of a multipart object opened the file of every part it covered before sending the first byte, so it needed one file descriptor per part: a 200 GB object of 5995 parts failed every download with
500 Internal Server Error(open part blob: No file descriptors available (os error 24)) on a server whose softRLIMIT_NOFILEwas 1024, and fewer, smaller downloads in parallel could exhaust a higher limit just as well. The data on disk was intact. Plain and SSE-S3/KMS composite reads, ranged or not, now open one part at a time, each when the previous one has been streamed, and the decrypt-and-re-encrypt fallback ofCompleteMultipartUpload(mixed or compressed parts under encryption) reads its parts the same way. The first part is still opened before the response starts, so a missing part remains an error response; a part that disappears later aborts the transfer. The Compose file shipped indocker/already raisesnofileto 65535; deployments using their own should do the same.
Security¶
- zlib CVE-2026-85091 (heap buffer overflow, MEDIUM) in both images. The console's Alpine base ships zlib 1.3.2-r0, and so does the newest Alpine 3.24 point release: the console Dockerfile now requires
zlib>=1.3.2-r1, next to the existing openssl constraint. Thearcabinary links zlib statically (librdkafka, throughlibz-sys), where the image scan cannot see it because the scratch image has no package database: the builder now requireszlib-dev/zlib-static >= 1.3.2-r1, which a build from a cached layer did not guarantee. Earlier binaries and images may embed the vulnerable 1.3.2-r0; zlib is reached there only by the Kafka notification connector's message compression.
0.31.1 — 2026-10-06¶
Added¶
- GitHub releases. Every release tag now gets a GitHub release, created by
bin/release(step 12 ofRELEASING.md): the notes are derived from the tagged CHANGELOG entry (the section headings and the bold title of every bullet, then links to the full entry, the compare view and the container images), previewed as GitHub renders them, and published only after confirmation. v0.31.0 is the first one. - Linux release binaries. Every GitHub release now carries the server binary for
linux/amd64andlinux/arm64(arca-X.Y.Z-linux-<arch>.tar.gz) and theirSHA256SUMS. The binary is the static (musl) one of the release image, archived by the Publish images workflow from the very image it scanned, so it runs on any Linux of that architecture with no dependency; each archive also carriesLICENSE-AGPL-3.0,NOTICEandTHIRD-PARTY-NOTICES.md.bin/releasedownloads them from the tag's workflow run, verifies the checksums and attaches them; its preview shows the assets as GitHub will.
Fixed¶
- The server no longer panics at startup when the system has no CA certificates (TD-049). The webhook and Elasticsearch connectors built their HTTP client at startup, whether or not any notification was configured, and aborted the server when the platform trust store could not be loaded (
No CA certificates were loaded from the system): the published image was not affected (it ships a CA bundle), but a bare binary on a host or in an image without one was. The server now starts, logs one warning per connector, and those connectors fail each delivery and connection test withHTTP client unavailable: <cause>.
0.31.0 — 2026-10-06¶
Added¶
- Published multi-arch container images. Every release is now published to the GitHub Container Registry as
ghcr.io/dxc-technology/arcaandghcr.io/dxc-technology/arca-console, each a single multi-arch image forlinux/amd64andlinux/arm64, so the same tag runs on x86 servers and on Apple Silicon / ARM hosts. A release is taggedX.Y.Z, plusX.Y,Xandlatestonly when it is the newest release of its minor line / major line / overall — publishing a fix for an older line never moves them backwards — and a prerelease gets its exact tag only; the major-onlyXtag is not published while Arca is at0.x(a minor bump may break compatibility, so:0would promise too much) and appears by itself from 1.0.0. Publishing is automatic on everyvX.Y.Ztag push (.github/workflows/publish-images.yml): each platform builds natively on its own runner (an emulated Rust release build would take hours), is scanned by Trivy, and is pushed by digest, untagged; only when all four builds pass are the digests merged into one index per image and tagged, so a scan failure on any platform publishes nothing. The Trivy gate fails on any finding and, forarca, also scansCargo.lock: the scratch image holds no package database and a plain Rust binary, so an image scan alone has nothing to check. The workflow also refuses a tag that disagrees withCargo.tomlor the console version.bin/build --pushruns the same pipeline from a local machine as a fallback. All of it lives in one library,bin/lib/images.sh, covered by 45 tests (bin/test scripts,dockermocked), so CI and the fallback cannot drift apart. -
OCI image metadata. Both production Dockerfiles now carry
org.opencontainers.image.*labels (title,description,source,url,licenses);versionandrevisionare added at publish time, and every label is copied to the multi-arch index as an annotation, which is where GHCR reads the package description from.sourcelinks each package to this repository.docker inspecton a running container now tells which release and commit it was built from. -
arca tls ensurekeeps a local CA and a server certificate valid, (re)generating only what is needed: a missing, unreadable or inconsistent CA (key not matching its certificate), or one expiring within the renewal window (30 days by default), is replaced; a server certificate that is missing, expiring, issued for other SANs or not signed by the current CA is reissued with the same CA, so a client that trusts the CA once keeps trusting every renewal. The CA lasts ten years, the server certificate one year (well inside the 398/825-day limits browsers and macOS apply to server certificates). The CA private key lives in its own directory and never reaches the one mounted in containers — Arca's certificate auto-detect would otherwise see two private keys and refuse to start. Idempotent: a run with nothing to do writes nothing.arca tls generateis unchanged. Reading certificates back usesx509-parser, already in the dependency tree throughrcgen(whosex509-parserfeature is now enabled to sign with an existing CA): no new crate. -
HTTPS without certificates of your own.
bin/arca start --tlsused to fail whencerts/was empty. It now decides at every start: your certificates incerts/(any.pem/.crt/.cert/.keyfile, symlinks followed) are served as before; with none, a local CA and a server certificate forlocalhost,127.0.0.1,::1andarcaare created incerts/local/on the first start and kept valid byarca tls ensureon every later one (plus any names inARCA_TLS_SANS, from the environment ordocker/.env). Dropping your certificates in wins at the next start and removing them falls back to the local ones; nothing incerts/itself is ever written. The CA key lives incerts/local-ca/, which no container mounts. The files are created as the host user with Arca's service group, so the host user can manage them and Arca and the console read the0640key through the group, as with any certificate. The choice is saved in.arca-envforbin/console. The TLS guide documents trusting the local CA once on macOS, Debian/Ubuntu or in the S3 clients. -
[storage] fsyncsetting (defaulttrue). Withtruethe server fsyncs blob data, sidecars and new shard directories and commits SQLite withsynchronous=FULL, so an acknowledged write survives a power loss or kernel crash (TD-038).falseskips every fsync and usessynchronous=NORMAL, the behaviour of previous releases: faster on slow disks, but objects acknowledged shortly before a power loss can be lost or truncated (writes stay atomic: a sidecar is never half written). The server logs a warning at startup when it is off. It applies to the live server only: the offline tools (recover,gc,migrate-db,compress-existing, ...) are always durable. With PostgreSQL the database side is PostgreSQL's ownsynchronous_commit.
Changed¶
- The Ceph s3-tests report counts documented AWS divergences as passed.
docker/s3-tests/report.pyhas a table of tests where Ceph expects RGW's answer and Arca deliberately gives AWS's; such a test counts as passed only when it fails on exactly the listed assertion (every earlier check in it passed), never on any other failure or error, and the HTML report andsummary.json(aws_divergences) list them with the reason. Two entries so far:test_object_checksum_sha256andtest_object_checksum_crc64nvme, which expectBadDigestfor a malformed checksum value where AWS answersInvalidRequest. Latest run: 371/825 (was 369), 0 unexpected failures. Covered bydocker/s3-tests/report_test.py. - Audit-log entries are committed in batches of up to 1 second. The audit writer shares the metadata database, so with SQLite FULL every audit commit was an fsync, paid even by a GET. It now commits when 1024 entries are queued or 1 second after the first one, and at once when the server starts a graceful shutdown. A crash can lose up to the last second of audit entries, which until then were only held in memory.
- Image names. The production images are now named after their registry repository everywhere: the Compose files,
bin/test, the Kubernetes manifests (deploy/kubernetes/, which now reference the published images) and the documentation. Local builds are tagged after their build target,ghcr.io/dxc-technology/arca:production/:developmentandghcr.io/dxc-technology/arca-console:production, never with a release tag, so a working-tree build can neither pass for a release nor shadow the publishedlatest. Images built locally under the oldarca/arca-consolenames are no longer used and can be removed withdocker rmi arca arca-console. Test and tooling images keep their names and are never published. - Documentation audited against the code. The architecture reference is rewritten from the current code: real crate graph and middleware order (there is no use-case layer), every storage trait and decorator and how they are stacked, the table list with what a cluster replicates, the write path, multipart composite blobs, how conditional writes are made atomic on each backend, and a clustering section with the design rationale. A new HA Design Decisions page records decisions H1 to H12 (what the code does today, why, the alternatives rejected), which code comments cite as "decision Hn". The S3 API reference gains a conditional-writes section and two documented divergences from AWS (a refused conditional
CompleteMultipartUploadis always412, never409;If-None-Matchaccepts a specific ETag). The HA guide is corrected where it promised more than the code does: the drift fingerprint covers the write quorum, notcluster_size; only object writes are ACK-counted in quorum mode; object tags do not self-heal; sync state is read fromGET /admin/cluster, notarca cluster status; TD-025 needs per-key load-balancer routing. The Admin API, CLI, Disaster Recovery, Configuration and TLS pages are completed or corrected likewise (health states,/admin/cluster,?node=, five missing CLI commands, whatarca recoverreally restores). The help ofarca cluster statusand the instructions printed byarca migrate-topologynow point atGET /admin/clusterfor sync state. Bugs found during the audit are tracked as TD-029 to TD-042.
Removed¶
- The "HA Hardening Plan" documentation page and the
.claude/planning documents. Plans and reviews were working documents for a single task. Their durable content (design decisions and rationale, invariants, known limits) now lives in the architecture reference, the HA Design Decisions page, the HA guide andTECH_DEBT.md, and code comments no longer cite IDs defined only there.
Fixed¶
- The offline
compress-existing,decompress-existing,encrypt-existinganddecrypt-existingcan no longer turn good data into bad on a power cut. They rewrote blobs that were already durable with nofsync, renamed the new blob over the old one and only then wrote the sidecar with a plainfs::write: a power loss during or shortly after a run could leave a zero-length blob, or a blob that no longer matched its sidecar (a compressed file described as uncompressed, which reads back as garbage and is compressed a second time by a re-run). Forencrypt-existingthe window was worse: the freshly generated wrapped key existed only in memory until the sidecar was written, so a failure there left ciphertext whose key was lost for good, and a re-run skipped the blob as "already encrypted". The new blob is now written to a temp file and fsynced, the new sidecar is written durably to a journal file, and only then are the blob and the journal renamed into place, each rename followed by a directory fsync (arca_storage::inplace; the durability helperssync_dir,rename_durableandwrite_file_atomicare now public). Running the same command again after a crash discards a rewrite that never replaced the blob, or completes one that did (including the database row for the encryption tools), before looking at the sidecar. Leftover temp files are named*.tmpso GC andarca fsckignore or report them; the encryption temp files used to end in.aenc-tmp/.plain-tmp, which GC andfscktook for blobs.fsyncitself cannot be observed by a test; the ordering, the cleanup and the recovery of every crash point are covered by unit tests. A crash between the blob rename and the sidecar rename leaves a mismatched pair until the tool is run again, so every real run now writes a durable marker (<data_dir>/.offline-rewrite-in-progress: command, re-run arguments, start time, pid) before touching any blob and removes it only after a clean finish:arca serverefuses to start while it exists (no override flag),recover,gc,migrate-dbandmigrate-topologyrefuse,fsckwarns, another rewrite command is refused until the interrupted one is re-run, and the same command clears it. Residual: the database row update of the encryption tools is still after the sidecar, repaired only when the crash happened inside the journaled swap. Behaviour change for scripts:encrypt-existinganddecrypt-existingnow exit NON-ZERO when the run ends with per-object errors (the summary is still printed, the marker is kept, and the error names the command to re-run); they used to print the errors and exit 0. A clean run and--dry-runstill exit 0.compress-existinganddecompress-existingalready aborted with a non-zero exit on the first failing object, so they are unchanged. - Keep-alive GETs over plain HTTP no longer take about 40 ms each (TD-047). The plain-HTTP listener left Nagle's algorithm on (the TLS listener already disabled it), so a response written as headers then body, which is every GetObject, waited for the client's delayed ACK: on a keep-alive connection every GET after the first took about 40 ms more than it should (measured 3 ms for the first, 42 ms for each following one, 1 KiB objects, Linux). Every accepted plain-HTTP connection now gets
TCP_NODELAY. All real S3 clients use keep-alive, so this affected every plain-HTTP deployment, for example one behind a TLS-terminating reverse proxy. PutObject was not affected (its response has no body). - Acknowledged writes now survive a power loss (TD-038). Blob and sidecar writes never called
fsync, and sidecars were written with a plain, non-atomicfs::write, so a power cut after a200 OKcould lose or truncate the data. The blob temp file is now fsynced before the rename (plainput, replicated raw writes, theconcatfallback, re-encryption), newly created shard directories are fsynced, and sidecars are written to a temp file, fsynced, renamed and the directory fsynced. A blob is durable once its sidecar has been written: the sidecar lives in the blob's directory, so its directory fsync also covers the blob's rename (3 fsyncs per small PUT, not 4), and every handler now fails the request, discarding the fresh blob, when the sidecar write fails instead of logging a warning and committing the row (UploadPart and the re-encryption job did that). SQLite now commits withsynchronous=FULLinstead ofNORMAL, so the metadata row of an acknowledged write survives a power loss too (in WAL modeNORMALcan lose the last commits). The new[storage] fsyncsetting (defaulttrue) turns all of this off for deployments that prefer throughput over durability (see Added). Measured with warp, 1 KiB PUTs at concurrency 16 (A = no fsync, the old behaviour; B = file fsyncs only; C = file fsyncs + SQLite FULL, the new default): local NVMe (AWS instance store, 0.16 ms per synchronous write) B -35%, C -37%; gp3 EBS (3.3 ms) B -53%, C -78%; Hyper-V virtual disk (3 ms) B -49%, C -77%. 1 MiB objects lose 6-20% on NVMe and concurrent gp3, large objects (512 MiB) 0-2%. On a slow disk C caps small PUTs at about 200 per second whatever the concurrency, because every SQLite commit is an fsync on the single writer connection (TD-048). UploadPartCopywrites a part sidecar for every part (part of TD-014). A plain (unencrypted) copied part used to get none, soCompleteMultipartUploadcopied every byte instead of assembling a composite, an encrypted store failed it with "missing sidecar", andarca recovercould not see the part. It now writes the same sidecar asUploadPart.arca compress-existingno longer damages encrypted blobs or aborts on multipart ones (TD-044). It compressed the ciphertext file of SSE-S3/SSE-KMS blobs in place, leaving the objects unreadable (parse header: invalid magic bytes), and aborted the whole run on composite (multipart) blobs, which have no file of their own. It now skips encrypted blobs (including SSE-C) and composite blobs, anddecompress-existingskips composite and encrypted blobs, each with aSKIPmessage and without stopping the run. Ifcompress-existingwas already run on a store with encryption enabled, some objects may be unreadable; the data and keys are intact and recoverable, the repair is still tracked in TD-044.- A failing
bin/teststep no longer leaks its test project. The self-contained modes kept their compose command in a function-localCOMPOSE; whenset -eended the script at a failing step, the EXIT trap ran after the function was gone, hitCOMPOSE: unbound variableand never issueddown -v, so thearca-test-<mode>project and its volumes stayed behind. The compose command (and, fortls-permissions, the names of the resources to remove) now live in script-level variables the cleanup can always read. Covered by 7 new tests inbin/test scriptsthat make a test step fail and check the cleanup, the failing exit code and the single cleanup of a passing run. - Upload bodies are now verified against the digest the client sends, as on AWS (TD-034). PutObject and UploadPart accepted and stored any body:
Content-MD5was never read,x-amz-checksum-*values were stored without being compared with the data, and a hexx-amz-content-sha256was signed but never checked, so a corrupted upload was acknowledged. A client that sends a wrong digest now gets400as on AWS and nothing is stored (an existing object or part is left untouched):BadDigestfor a wrongContent-MD5orx-amz-checksum-*,InvalidDigestfor a malformedContent-MD5,InvalidRequestfor a malformed checksum or two checksums at once,XAmzContentSHA256Mismatchfor a wrong hexx-amz-content-sha256. All five S3 checksum algorithms are verified (CRC32, CRC32C, CRC64NVME, SHA1, SHA256; SHA1 was not recognised before), whether sent as a header or as an aws-chunked trailer. Trailing checksums, which boto3 / botocore 1.36+ send by default over HTTPS, were silently dropped and are now recorded and returned withx-amz-checksum-mode: ENABLED; UploadPart returns its checksum and ListParts reports it when CreateMultipartUpload named an algorithm. Clients that send correct digests (boto3, AWS CLI, MinIOmcand minio-py, over HTTP and HTTPS) are unaffected. Still not verified: aws-chunked chunk signatures (TD-045) and multipart object checksums at CompleteMultipartUpload (TD-046). No new dependency crates:crc,sha1andsha2were already in the dependency tree. -
A failed upload no longer leaves a temporary file behind. When a PutObject or UploadPart body failed mid-stream (body larger than
max_body_size, client disconnect, malformed aws-chunked encoding),FsBlobStorereturned the error but left the partial{id}.tmpfile in the blobs directory, wherearca fsckreported it as stale. It is now removed. -
Compressed objects could not be read on a server with encryption enabled (TD-029). On a bucket with compression configured, a
GetObject(full or ranged) of a compressed object on a server with SSE-S3 enabled, globally or for that bucket, failed with500 InternalErrorfor practically every compressible object larger than 64 KiB, and for every object whose compressed frame came out larger than the object itself (incompressible bodies). Compression sits above encryption, so the encryption layer encrypts the compressed frame, but it sized its ranged reads with the object's uncompressed size from the sidecar instead of the frame's length; it now uses the frame's length (compressed_size). Only reads were wrong: objects already stored compressed and encrypted are intact and read correctly after upgrading, no repair is needed.bin/test compressionnow also runs the suite on an encrypted server, which has new tests for incompressible bodies, open-ended and suffix ranges, multipart uploads and copies. -
PostgreSQL conditional writes now have a regression test (TD-039).
bin/test postgresalso runstest_conditional_writes.pyagainst the PostgreSQL-backed server, so a reordering of the compare-and-swap read relative to the transaction's first locking statement (required under READ COMMITTED) no longer goes unnoticed. -
Test runs no longer destroy the developer's server (TD-030). Every self-contained
bin/testmode (tls,encryption,compression,per-bucket-encryption,recrypt,kms,postgres,migrate-db,migrate-topology,replication,notifications, every connector) andbin/perf-test --tls/--encryptionran in the developer's own compose project: they recreated the runningarcacontainer with their own config, overwroteconfig/.generated.toml, and finished withdown -v, deleting thearca-datavolume, while.arca-envwent on describing a server that no longer existed. Each one now runs in a compose project of its own,arca-test-<mode>, so its containers, network and volumes are separate and itsdown -vremoves only them; it generates its ownconfig/.generated-test-<mode>.toml(mounted throughARCA_GENERATED_CONFIG); and every host port its services publish becomes an ephemeral loopback port, because the suites reach their services over the compose network and a fixed9000(or8200,5432,8765,9001) would collide with the server already running. The OpenBAO, PostgreSQL and webhook-receiver ports gainOPENBAO_HOST_PORT,POSTGRES_HOST_PORTandWEBHOOK_RECEIVER_HOST_PORTlike the existingARCA_HOST_PORT, and the TLS overlay no longer publishes a fixed9000next toARCA_HOST_PORT. A running dev stack is left exactly as it was, and suites of different modes can run side by side.bin/test presigned,bin/test ssecandbin/perf-testwithout features, which start no server of their own, used a plaincompose runthat could recreate the running server from the base compose file: they now run against it with--no-deps, likebin/test integration. Covered by 33 newbin/test scriptstests, withdockermocked. bin/test integrationskipped the encryption and KMS suites (TD-031).test_encryption.py,test_per_bucket_encryption.py,test_recrypt.pyandtest_kms.pyonly run when an environment variable says the server supports them, and only their dedicated modes set it, so against a server started with--encryptionor--kmsthey were silently skipped.bin/test integrationnow sets them from the features recorded in.arca-env: global encryption runs the encryption suite, KMS also the KMS suite, per-bucket encryption (local key or KMS) the per-bucket and re-encryption suites.bin/test integrationtested a different server from the one you started. It randocker compose run --rm test, whosedepends_onrecreated the runningarcacontainer from the base compose file alone: the generatedconfig.tomland the certificate mount were dropped, so afterbin/arca start --tls --encryptionthe suite silently ran against a plain, unencrypted server (and, since the image rename above, against the stale:productionimage instead of the dev one). It now runs with--no-deps, never touching the server, reads.arca-envto reach it the way it was started — plain HTTP, HTTPS verified against the local CA, or, with your own certificates, HTTPS under a name the served certificate covers (its first plain DNS SAN, a wildcard made concrete asarca.<domain>, orARCA_TLS_HOSTNAMEfromdocker/.env), mapped to the arca container and verified against the system CAs — and stops with a clear error when no server is running.- The TLS test suites no longer touch
certs/, and work on Linux (TD-026).bin/test tlsgenerated its self-signed certificates into the repository'scerts/bind mount and deleted them afterwards — destroying any file of the user's with the same names — and on a real Linux host it failed outright, because thetls-initcontainer runs as65532and cannot write the invoking user's directory (invisible on macOS, where Docker Desktop fakes bind-mount ownership).bin/test tlsandbin/perf-test --tlsnow generate a throwaway CA + server certificate into a named volume (arca-tls-test-certs), serve it through the same compose overlay, and remove it at the end.bin/perf-test --tlsalso stops disabling certificate verification: its client now trusts the throwaway CA, and with--tls-fqdnyour own certificates are used in place instead of being symlinked intocerts/. Thetls-initcompose service, which was the only writer intocerts/, is removed. - A request right after an early reject could be misread on the same connection. When Arca answered a request before reading its body — any authentication or authorization reject in the S3 auth middleware, a missing bucket, a failed precondition — it kept the HTTP/1.1 connection alive although the body was still owed. With
Expect: 100-continue, which botocore sends on every PutObject, the client, having received a final status instead of100 Continue, legitimately never sends that body, and the server cannot tell the bytes that arrive next from the owed body: hyper sometimes swallowed the first bytes of the next request as the body and parsed the rest as a mangled request line. A DeleteObject sent after a denied PutObject then arrived asTE /bucket/keyand failed withSignatureDoesNotMatchinstead ofAccessDenied— the intermittent failure oftest_readonly_user_can_read_not_write, reproduced on the wire in 14% of raced follow-up requests. A new outermost layer (CloseOnUnreadBodyLayer) now wraps every request body, records whether it was read to the end, and addsConnection: closeto any response that leaves it unread, as RFC 9110 §10.1.1 asks and AWS S3 does — whichever middleware or handler produced the response, and whatever its status. It replaces the per-call-siteclose_afterthat covered only PutObject's own early rejects (commiteb7eb6e) and left the auth middleware, UploadPart and the configuration PUTs exposed. HTTP/2 is left alone: each stream is framed, so an unread body cannot desync the next request.http-body(MIT, already in the dependency tree through axum and hyper) becomes a direct dependency ofarca-proto.
Security¶
- Dependency updates for five advisories (all compatible point releases,
Cargo.lockonly):rustls0.23.40 → 0.23.45 (GHSA-2mjx-qc3c-rqvc, TLS 1.3 handshake messages accepted across encryption-level boundaries; pullsrustls-webpki0.103.15 andaws-lc-rs1.18.1 /aws-lc-sys0.45.0),hickory-resolver,hickory-protoandhickory-net0.26.1 → 0.26.2 (GHSA-5j98-2g5x-46v6, GHSA-6w6g-hm98-mhgm, GHSA-6f2x-v7q7-m7m5: hidden DNSSEC validation failures, an unbounded truncated-response retry loop, irrelevant CNAME records followed),xxhash-rust0.8.15 → 0.8.19 (GHSA-6g2r-675j-hx59).trivy fs --scanners vuln Cargo.lockreports 0 findings; the transitive licence audit is unchanged (no copyleft) andTHIRD-PARTY-NOTICES.mdis regenerated.
0.30.2 — 2026-09-23¶
Security¶
arca tls generatewrote every file world-readable, private keys included. All four outputs ofarca tls generate— and the CA plus every per-node key ofarca tls generate-cluster— went throughstd::fs::write, which creates a file0666 & ~umask, i.e.0644under the usual umask. The private keys were therefore readable by every local user on the host and by every process in any container sharing that directory. Modes are now explicit and split by role: certificates0644(public material), server and node private keys0640, the CA private key0600. The server key is deliberately group-readable rather than owner-only, because the group is the access channel: Arca runs as65532:65532and the web console runs as a different user (100:101), and in the Compose dev setup both read the same key — the console reaches it through a supplementary group, never through world-readable bits. The CA key is0600: nothing reads it at runtime, it only signs. The mode is applied at creation time (OpenOptions::mode) so a key is never even briefly world-readable, and re-applied afterwards for two reasons — the creation mode is masked by the process umask, which must not be allowed to strip the group bit a key needs, and it does not apply at all to a file left behind by an earlier run, which is what makes re-runningarca tls generatetighten keys an older version wrote loose. Action required: keys generated by an earlier version keep their old mode until they are regenerated orchmod'd by hand; treat a key that has been sitting world-readable on a shared host as disclosed and rotate it.
Fixed¶
-
The Kubernetes console deployment could not start.
deploy/kubernetes/arca-console.yamlsetrunAsUser: 101, but 101 is the nginx group — the image's user is uid 100. Running as 101 fails at startup: the entrypoint cannot rewriteindex.html(sed: can't create temp file) and nginx cannot create/var/lib/nginx/tmp/client_body, because the image pre-owns those paths as100:101. Corrected torunAsUser: 100,runAsGroup: 101, and the misleading comment on that line with it. The pod also gainedfsGroup: 65532so a mounted certificate Secret is readable when the console terminates TLS itself rather than at the ingress;arca-cluster.yaml, which was already correct at65532throughout, is unchanged. -
The console never enabled HTTPS with certificates produced by
arca tls generate. Its entrypoint skips CA material when auto-detecting the certificate and key, and did so by matching the bare substringcaagainst the file name — which matchesarca-server.crtandarca-server.key, inside the word "arca". Every file thetls-initservice writes was therefore skipped, no certificate was found, and nginx quietly kept serving plain HTTP while port 9443 refused connections. The match is now against acaword in the base name (ca.pem,arca-ca.crt,client-ca.pem,ca-bundle.crt), so CA files are still excluded and server files are not. The bug was masked for anyone whosecerts/directory also held real certificates under names likefullchain.pem/privkey.pem, which the old glob happened not to match. The entrypoint additionally no longer degrades silently when the key is present but unreadable: it logs the uid and groups it is running as and how to grant the missing one, instead of leaving "port 443 is closed" as the only symptom.
Changed¶
- The console reaches Arca's TLS private key through a shared group instead of world-readable bits. Arca's images run as
65532:65532, the console as100:101, and the Compose dev flow bind-mounts onecerts/directory into both — no owner and mode can grant read to both users without also granting it to everyone. The key is now group-readable by65532(Arca's service GID), and the console is given that GID as a supplementary group:group_add: ["65532"]indocker/docker-compose.tls.yml,fsGroup: 65532indeploy/kubernetes/arca-console.yaml— the same mechanism Kubernetes provides natively. Note that 65532 is the distrolessnobodyGID and is documented as "the identity Arca runs under", not as a privilege boundary. The TLS guide gained a File Permissions section stating the rule once (group-readable, never other-readable), the expected mode of each file, the Compose and Kubernetes snippets, and — the part that actually bites in production — a certbot--deploy-hookexample, because a renewal rewritesprivkey.pemasroot:root 0600and silently reverts a one-timechgrp, breaking TLS 60-90 days after the change that caused it.setfacl -m g:65532:ris documented as the alternative where group ownership cannot be changed.
Added¶
-
publiccode.yml— the metadata file that describes Arca to the Developers Italia open source software catalogue, written against version 0.7.0 of the publiccode.yml standard and validated with the official parser (italia/publiccode-parser-go). It declares the software as a stable standalone backend owned by DXC Technology, under AGPL-3.0-or-later, and carries a user-facing description, the feature list, the documentation links and the console screenshots. Two of its keys,softwareVersionandreleaseDate, go stale at every release and are now part of the version-location checklist inRELEASING.md;releaseDatein particular holds no version string, so the usual "grep for the old version" sweep does not catch it. Note that the catalogue's crawler only scans repositories registered against a publisher, so the file alone does not list Arca in the catalogue. -
AI-generated code disclosure. A new
NOTICEfile at the repository root states that substantially all of this repository - source code, tests, configuration, scripts and documentation - was generated with the assistance of AI coding tools, under human direction and review, and that the disclosure does not alter the AGPL-3.0-or-later terms. The notice is surfaced wherever a reader is likely to look rather than repeated in every one of the ~200 source files: an info callout in the License section ofREADME.mdand of the documentation home page, the MkDocscopyrightfooter so it renders on every documentation page, a//!line in each of the five crate roots (so it also appears incargo docoutput), and a comment inconsole/index.htmlandconsole/js/app.js. -
Trivy filesystem-scan configuration (
trivy.yaml,trivy-secret.yaml,.trivyignore) —trivy fs .previously reported a fixed root/test credential (AKIA5B6BSHJA8CIHZSVGand its secret key) used throughouttests/, the Compose test services, and the console login placeholder, as 9 leaked-secret findings; it is a value Arca itself generates for local containers, never a real AWS credential, sotrivy-secret.yamlnow allow-lists it by exact value wherever it appears. The scan also reported the 4rustls-webpkiadvisories already tracked and dismissed as unreachable in TD-024;.trivyignoresuppresses those same 4 GHSA IDs, pointing back at that entry. Two remaining findings — the console Kubernetes manifest and a handful of test-only Dockerfiles — are real, minor gaps left visible and tracked as TD-027/TD-028 rather than suppressed. -
bin/test tls-permissions— a self-contained suite for the permission semantics above, because nothing else could catch a regression in them. The existing TLS tests run against bind mounts, and Docker Desktop on macOS fakes bind-mount ownership: a0600key owned by the host user appears owned by whatever UID the container runs as and reads fine, so any uid/gid assertion built on one would pass vacuously on a developer Mac and fail only on a real Linux host. The new suite stages its material in a named Docker volume, where ownership is real on every host, and runs nine checks: the four modes and the owning GID thatarca tls generatewrites, seen from inside a container rather than from the host; Arca (65532) serving HTTPS from the0640key; the console (uid 100) serving HTTPS with the service GID; and — so the suite has teeth — the console without it failing to enable TLS, plus the bare read of the key failing and succeeding in the same two configurations.
0.30.1 — 2026-09-14¶
Changed¶
- Base images of every distributed image are now pinned to exact versions.
rust:alpine→rust:1.98.1-alpine3.24(builder, and thedevstage that inherits it),debian:stable-slim→debian:13.6-slim(development runtime) andalpine→alpine:3.24.1(console);productionstaysFROM scratch, which has no tag to pin. Each pin is digest-identical to what the floating tag resolved to when it was set, so this changes nothing about the images today — it stops them changing underneath us tomorrow, which matters because the in-Dockerfile package pins (libcrypto3>=3.5.8-r0,libssl3>=3.5.8-r0,libuuid>=2.42.3-r1) are each relative to a specific base image version and go stale silently when the base moves. The Rust tag deliberately keeps its-alpine3.24suffix, which fixes the apk repository branch the builder's C dependencies (ring,rdkafka) come from, not just the compiler version; Debian uses the numeric point release rather thanstable-slimbecause the numeric tag also pins the apt suite to the codename (trixie), soapt-get installin that stage cannot silently cross a Debian major release. Note that pinning the Alpine rootfs does not freeze the apk index: thev3.24branch keeps publishing updates, which is whyalpine:3.24.1still installs openssl 3.5.7-r0 while the pinned constraint pulls 3.5.8-r0 from the repository. Images used only for testing and tooling (python:alpine,python:3-slim, the Playwright screenshot image, and the third-party services in the connector/KMS/cluster compose overlays) stay on floating tags on purpose: they are never distributed.AGENTS.mdgained a "Base Image Pinning and Upgrades" section with the upgrade procedure — how to find and verify a new tag, and, critically, how to re-check every package pin against the new base and remove the ones it has overtaken instead of leaving a constraint whose CVE list is no longer true. TheFROMlines are the single source of truth for the tags in force: the documentation states the pinning principle and points at the Dockerfiles rather than repeating version numbers that would need rewriting at every bump, and this changelog is the only file where exact versions are recorded.
Security¶
-
Console image rebuilt on plain
alpine+ thenginxpackage instead ofnginx:alpine. The official image ships the image-filter, xslt, geoip, njs and acme modules, none of which the console loads (it serves static files and has noload_moduledirective anywhere), and they drag inlibgd→libxpm→libxt→libsm→libuuidalong withtiff,freetype,libx11,libavif,libxml2,libxslt,curlandgeoip. That transitivelibuuid(util-linux) is what a container scan flagged: CVE-2026-53612, CVE-2026-53613, CVE-2026-53614, CVE-2026-76642, CVE-2026-78408, CVE-2026-78409, CVE-2026-78410 (High) and CVE-2026-27456 (Medium), all against 2.42.1-r0. Rather than upgrade the package, the whole unused dependency chain is gone: the image drops from 70 packages / ~65 MB to 18 packages / ~13 MB, andlibuuidis no longer installed at all, so future util-linux advisories cannot reach the console. Alpine's nginx includes/etc/nginx/http.d/*.confrather thanconf.d, so the server config and the entrypoint's TLS rewrite moved to that path. Arca's own image is unaffected (FROM scratch, no OS packages). -
All four images now scan clean (
trivy image, 0 findings). Two gaps remained after the rebuilds above. The console's base still shipped openssl 3.5.7-r0, leaving 20 findings (libcrypto3/libssl3, fixed in 3.5.8-r0) that the previousnginx:alpinebase carried too — the earlier scan had simply not listed them, reporting onlylibuuid. And both Python images shippedpip, whose vendoredmsgpackandsetuptoolscopies trail their own advisories (GHSA-6v7p-g79w-8964, CVE-2025-47273, CVE-2026-59890); nothing needs pip at runtime, so it is removed once the requirements are installed. Remediation is by pinned point upgrade, never a blanketapk upgrade: each Dockerfile names the individual package and the advisory that forced it (apk add 'libcrypto3>=3.5.8-r0' 'libssl3>=3.5.8-r0',apk add 'libuuid>=2.42.3-r1'), so a build pulls a known set of versions and the diff records why.>=rather than=because Alpine drops superseded versions from its repo index, which would turn an exact pin into a build failure the day a newer build lands. Where no fix exists yet the finding is accepted and tracked inTECH_DEBT.mdrather than papered over with a blanket upgrade. For reference,trivyon the pre-change console reported 52 findings acrosscurl,libcurl,libcrypto3,libssl3,libuuidandlibexpat. The console image grows 12.6 → 18.4 MB (replacing base packages rewrites them in a new layer); still well under a third of the original 65.5 MB. -
Connector-test receiver images rebuilt on the same hygiene rules.
webhook-receiverandgrpc-receivercarried the samelibuuidadvisories. Unlike the console, the dependency is genuine there — CPython's_uuidextension linkslibuuid.so.1— so the package is upgraded (a pinnedapk add 'libuuid>=2.42.3-r1', from the base image's 2.42.1-r0) rather than removed, and both images move to the currentpython:alpine.grpc-receiveradditionally dropped thegcc/g++/musl-dev/libffi-dev/linux-headerstoolchain it kept in the runtime layer: musl wheels now exist forgrpcio, sopip install --only-binary=:all:never compiles from source, and stub generation moved to a builder stage that keepsgrpcio-toolsout of the runtime — 820 MB → 90 MB.grpcio1.66.2 → 1.84.0 andprotobuf5.29.6 → 7.36.1 along the way. Neither image is ever distributed; both exist only forbin/test notificationsandbin/test connectors grpc, which pass unchanged (20 and 7 tests). Thewebhook-receivercompose service also gained an explicitimage: arca-webhook-receiver, matching every other locally built service — without it compose derived the name from the project directory and an image built under the old name lingered as an untracked orphan, which is how a stale copy stayed on disk long enough to be scanned.
Fixed¶
-
Console container reported
unhealthyin every mode. TheHEALTHCHECKprobedhttp://localhost:80/, and busyboxwgetresolveslocalhostto::1first while nginx listens on IPv4 only, so the probe was always refused. It now targets127.0.0.1and, because TLS mode replaces the server config with an HTTPS-only one that stops answering on port 80, falls back tohttps://127.0.0.1:443/(certificate validation skipped: a liveness probe, not an integrity check). Verified healthy in both plain-HTTP and TLS modes. -
Cluster 507 test could not start its third node. The
InsufficientStorageintegration phase replacesarca-3's data volume with a 16 MiB tmpfs, and Docker gives a tmpfs the familiar world-writable1777only when it creates the mount point — mounted over a directory the image already ships (/data, mode 755) it inheritsroot:root 755instead. That was harmless while the images ran as root, but since they were switched to a non-root user in 0.30.0 the node died at startup withunable to open database file: /data/arca.db, haproxy then failed to resolve the missing container, and the phase timed out waiting for three live nodes. The overlay now sets the mode explicitly. Note the YAML octal prefix (mode: 0o1777): Compose reads a bare1777as decimal and mounts the tmpfs as mode3361, which fails the same way. Test-harness only, no change to any distributed image; the perf overlay's tmpfs (alocal-driver volume rather than a bind-style tmpfs mount) was checked and is unaffected.
0.30.0 — 2026-09-01¶
Fixed¶
- Conditional writes (
If-Match,If-None-Match,x-amz-if-match-last-modified-time,x-amz-if-match-size) are now atomic compare-and-swap, not check-then-commit. Every conditional-write path evaluated the precondition against a snapshot read long before the write actually committed — forPutObjectandCompleteMultipartUploadthe gap spanned the entire body upload / part assembly, so two concurrent conditional writes to the same key both routinely passed their check and both got200 OK, silently degrading to last-writer-wins on exactly the primitive S3 clients use for coordination.DeleteObject(plain and version-specific) and theDeleteObjectsbatch had the same shape with a narrower window. Preconditions are now re-evaluated authoritatively inside the same transaction that performs the write, on both backends (SQLite and PostgreSQL):MetadataStoregainedput_object_if,delete_object_ifanddelete_object_version_if, and every handler commits through them instead of a plain read-then-write. The early snapshot check is kept as a fast-path optimisation (fail fast before uploading a doomed body) but is no longer authoritative. Behaviour change: a concurrent conditional write that previously raced to200 OKnow correctly receives412 PreconditionFailed— this is the fix, not a regression; no correct client could depend on the old racy200. DeleteObjectsbatch conditional deletes on a specific version checked the bucket's latest object instead of the targeted version. A version-specific entry with anETag/LastModifiedTime/Sizeprecondition was validated againstget_latest_object, so deleting an older version whose ETag matched could be wrongly refused (or, on a versioned bucket with tags, another version's stale metadata could wrongly pass). It now checks the exact version being deleted, matching the plain (non-batch)DeleteObjectpath.- PostgreSQL backend: conditional writes were not actually serialized under concurrency. The new authoritative CAS check (above) ran its precondition read before
next_object_seq()— the single statement in each transaction that actually contends under PostgreSQL's default READ COMMITTED isolation, since a plainSELECTdoes not block concurrent transactions. Concurrent writers for the same key could all read the same pre-write snapshot, all pass their precondition, and commit one after another, reproducing the exact check-then-commit race the fix above exists to close — on PostgreSQL only (SQLite's single-connection actor already serialized every statement regardless of order). Fixed by taking the seq first input_object_if,delete_object_ifanddelete_object_version_if, turning it into a serialization gate so each transaction's precondition check is guaranteed to see genuinely current state. This also closes a gap indelete_object_if's non-cluster unversioned-delete path, which previously took no serializing statement at all. - Known limitation, tracked as TD-025: in a cluster, conditional-write atomicity is exact per node, not cluster-wide —
put_object/delete_objectcommit locally then fan out, with no per-key authoritative node to arbitrate across peers. Two conditional writes racing the same base ETag on two different nodes can both be accepted, converging afterward by last-writer-wins. Mitigated by pinning a key's conditional-write traffic to one node via sticky load-balancer routing; see the HA guide.
Added¶
licensefield in the Cargo manifests. The workspace declared no licence at all in its package metadata, socargo metadata,cargo packageand any downstream tooling reading the manifests saw Arca as unlicensed even though the repository has always carried the licence file and the notices inREADME.md.[workspace.package]now setslicense = "AGPL-3.0-or-later"and all five crates inherit it withlicense.workspace = true. Metadata only: no code, dependency or behaviour change.
Security¶
- Dependency vulnerability remediation.
quick-xml0.40.1 → 0.41.0 (RUSTSEC-2026-0194, RUSTSEC-2026-0195),h20.4.14 → 0.4.17 (RUSTSEC-2026-0258, transitive viahyper),crossbeam-epoch0.9.18 → 0.9.20 (RUSTSEC-2026-0204, transitive viamoka),event-listener5.4.1 → 5.4.2 (RUSTSEC-2026-0221, transitive viaasync-lock/lapin/moka/sqlx-core). Thegrpc-receiverconnector-test tooling'sprotobufbumped 5.28.2 → 5.29.6 (CVE-2025-4565, CVE-2026-0994) — library-only, never part of the distributedarcabinary.rustls-webpki0.102.8 (4 published advisories, transitive viarumqttc's MQTT connector) remains unfixed:rumqttc0.25.1, the latest published release, hard-pinsrustls-webpki = "^0.102.8"with no newer version available — tracked as TD-024 rather than patched with an unvetted override. - All Docker images now run as non-root by default. The production (
scratch) and development (debian:stable-slim) Arca runtimes, the nginx-based console, and every auxiliary test/tooling image (Dockerfile.test,Dockerfile.perf,grpc-receiver,webhook-receiver,s3-tests) drop root and run as a fixed non-root user — excepts3-tests, which runs as the invoking host user (docker/docker-compose.yml'sS3TESTS_UID/S3TESTS_GID, set bybin/s3-tests) since it writes results into a bind-mounted host directory.HEALTHCHECKinstructions were added to every long-running server image that lacked one. See the TLS guide for a permissions caveat this introduces for bind-mounted certificate files on Linux hosts. - Kubernetes manifests hardened.
deploy/kubernetes/arca-cluster.yamlandarca-console.yamlgained pod- and container-levelsecurityContext:runAsNonRoot, explicitrunAsUser/runAsGroupmatching each image's built-in user, dropped Linux capabilities,seccompProfile: RuntimeDefault, and (for thearcacontainer, whose only writable path is the/datavolume)readOnlyRootFilesystem: true. THIRD-PARTY-NOTICES.mdregenerated. It had silently gone stale since thelicensefield was added in a prior commit:cargo-about's rule for skipping Arca's own crates keys onpublish = false, which that commit never set, so every generation attempt since has been failing at the root-cause level rather than just being out of date. Fixed by addingpublish = falseto[workspace.package]andpublish.workspace = trueto all five crates.- Bootstrap credential no longer uses AWS's published SigV4 documentation example. Every dev/test default (Docker Compose,
s3-testsconfig, console login-form placeholder, screenshot automation, storage unit-test fixtures, integration/perf test defaults, and the admin-API docs example) usedAKIAIOSFODNN7EXAMPLE/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY, which secret scanners and password managers flag as a known/compromised credential even though it was never a real secret. Replaced with a freshly generated random credential everywhere it served as a bootstrap default. The same string still appears, deliberately unchanged, incrates/arca-auth's SigV4 conformance tests, where it is AWS's own published test vector with hardcoded expected signatures rather than a credential.
0.29.0 — 2026-07-28¶
Security¶
- Anti-replay window on header-signed S3 and admin requests. The
Authorization-header SigV4 paths (S3 API and/admin/*) never validatedx-amz-dateagainst the server clock, so a captured signed request — sniffed off a plain-HTTP hop, or lifted from an access log or proxy trace — stayed replayable forever. Both paths now reject a request whosex-amz-datelies more than 15 minutes from the server clock (the AWS SigV4 convention, and the same window/cluster/v1/*already enforced since 0.26.0), returning403 AccessDenied. The signature coversx-amz-date, so an attacker cannot re-date a captured request without the secret key. Presigned (query-string) requests were already bounded by their ownX-Amz-Expires. Clients whose clock drifts beyond ±15 minutes from the server's will now be refused — run NTP. - Auth fails closed when a credential's user no longer exists. A valid credential whose
user_iddid not resolve to ausersrow was silently promoted to a synthetic root identity (a leftover backward-compatibility accommodation for pre-users-table credentials), which meant a dangling credential — one left behind after its user was deleted — authenticated as root and bypassed all policy evaluation. Both the S3 and admin auth paths now deny such a request (403 AccessDenied), and a genuine metadata-backend failure is a500 InternalErrorinstead of being collapsed into the same root fallback. To remove the source of dangling credentials,UserStore::delete_usernow deletes the user's credentials in the same transaction on both backends (SQLite and PostgreSQL), so a credential can no longer outlive its user. This is not merely defensive:DELETE /admin/users/{id}refuses with409 Conflictwhile credentials exist (unchanged), but two paths reach the store without that check and could mint a dangling credential that authenticated as root — thearca user deleteCLI, which validated only "not root", and the cluster control-plane apply of aUserDeleteop, where the deleting node's check cannot see a credential another node created concurrently. In the cluster the cascade is also what converges the two nodes on "user gone, its keys gone" instead of leaving one peer with a permanently denied key.arca user deletenow prints each access key it revokes rather than dropping them silently. Credentials predating the users table are unaffected: the migration that addedcredentials.user_idbackfilled them to the root user, which exists.
Fixed¶
- Admin access follows the user, not the credential. A credential carried its own
adminboolean, and the/admin/*gate required BOTH a root user ANDcredential.admin = true. Two paths minted credentials that could never satisfy it:POST /admin/users/{id}/credentials(the console's "create credential" flow) hardcodedadmin = false, so a second credential created on the root user signed in as a plain user with no admin reach at all, whilePOST /admin/credentialshonoured the request'sadminfield — the same identity therefore got different privileges depending on which endpoint created its key. Privileges are a property of the identity, so the flag is gone: the gate now allows a request when the credential's useris_root, or when the user's effective grants allow thearca:*action mapped from the admin path (which also, for the first time, lets a non-root user reach the specific admin endpoints its grants cover). Thecredentials.admincolumn is dropped (SQLite migration v26 / PostgreSQL0014),adminis removed from the Admin API request and response bodies (replaced byuser_id, the owning user), fromarca credential add --admin, and from the console's create-credential form and card badge (now the owner's username, linking to that user). An incomingadminfield is silently ignored rather than rejected, so an older cluster peer can still replicate credentials to an upgraded node. The reverse direction is not supported: a 0.29 node replicates credentials without the field, and a pre-0.29 peer requires it, so a cluster must not be left with mixed versions across this release. The lockout guards on delete/deactivate now key on "last active credential belonging to a root user".
Removed¶
adminflag on credentials. Dropped from the core type, the database (SQLite migration v26 / PostgreSQL0014), the Admin API request/response bodies, thearca credential addCLI and the console. See the entry above for the reasoning; incomingadminfields are ignored rather than rejected, so older clients and export files keep working, but a cluster must not be left running mixed versions across this release.
0.28.0 — 2026-07-24¶
Added¶
- Guided topology transition
migrate-topology(Phase 30, M4). Scripts the two in-place single-node ⇄ HA-cluster topology changes so the operator does not hand-assemble the config and the small DB chores. The cluster is fully replicated (not sharded), so neither direction redistributes data.arca migrate-topology --to-cluster [--output <file>]turns a standalone instance into the first node of a new cluster: it refuses if already clustered, reconciles this node'sobject_seqwrite counter toMAX(seq)(so the first clustered write cannot skip a pre-cluster object), and prints a ready-to-paste[cluster]stanza with a generatedcluster_id, a strong randomsecret(32 bytes hex, reusing thering-backed RNG),mode = "quorum",cluster_size = 3,discovery = "mdns", and commented templates for static/DNS discovery and[cluster.tls](pointing the operator atarca tls generate-clusterfor the inter-node CA). Restarting with the stanza enables cluster (tombstone) mode automatically; empty joiners then converge via anti-entropy — no data copy.arca migrate-topology --to-single --forcecollapses a cluster back to one node on the surviving authoritative node: it purges the cluster-only state (object tombstones + control-plane tombstones) andVACUUMs SQLite (PostgreSQL autovacuum needs none), then the operator strips[cluster]and restarts standalone;--forceis the explicit confirmation that every peer is synced and stopped (collapsing while a peer is behind loses its un-replicated writes). CLI-only by design (no console job): both directions are inherently operator+restart actions an online job cannot perform. New runbook in the HA guide. - Metadata-backend migration
migrate-db(Phase 30, M3). Copy ALL metadata from one backend to the other (SQLite ⇄ PostgreSQL) in place, without re-uploading data: blob files are filesystem-resident and untouched, only the metadata database moves. After a run the operator switchesmetadata_backend(and[storage.postgres]) in the config and restarts onto the new backend. Available two ways: an offline CLI escape hatcharca migrate-db --to <sqlite|postgres> [--force](server stopped, no writes race the copy), and an online maintenance-mode job (type: "migrate-db", params{ "target", "force" }) that drains the S3 API on its node while it runs. The copier is a single generic, type-aware column-walker driven by a static description of all 24 data tables (name + ordered columns + each column's logical kind) rather than per-table structs; it reads/writes every backend's native representation (SQLite TEXT timestamps ⇄ PGTIMESTAMPTZ, SQLite INTEGER booleans ⇄ PGBOOLEAN, SQLite TEXT JSON ⇄ PGJSONB), copies tables in foreign-key-safe order, and reconciles source/destination row counts per table (a mismatch aborts naming the table). Refuses a destination that already holds operator data unless--force(which fully replaces it); the always-present schema baseline (root user, built-in grants, seq counter) is ignored. Theobject_seqwrite counter and the audit/metrics surrogate sequences are migrated/reseeded so writes on the new backend don't collide. - In-place re-encryption jobs (Phase 30, M2). Encrypt existing plaintext objects to SSE-S3 (AES-256), or decrypt them back, without re-uploading and without migrating to a new instance. Runs as a maintenance job (console or admin API,
type: "encrypt"/"decrypt") in either live mode (copy-on-write + compare-and-swap + an optional byte-rate throttle, zero downtime) or maintenance mode (S3 drained on the node, full speed). Copy-on-write is used in both modes: a new encrypted blob is written and the object row is atomically swapped to it, so concurrent readers always see a consistent blob and the cluster's blob-id immutability is preserved — the new blob reaches peers via read-repair / anti-entropy and the orphaned old blob is reclaimed by GC. The ETag and Last-Modified are preserved (no lifecycle-clock impact); cluster convergence rides a dedicatedcontent_updated_atLWW dimension (separate from Object Lock'slock_updated_at) so a re-encrypted row is adopted deterministically without ever clobbering a concurrent lock change, and vice versa. SSE-C objects (customer-held keys) and multipart/composite objects are skipped (TD-014). An offline CLI escape hatch (arca encrypt-existing/arca decrypt-existing) performs the same transform in place for disaster recovery when the server is stopped. Console Maintenance page gains the encrypt/decrypt launchers (bucket/prefix filter, throttle). - Maintenance jobs subsystem (Phase 30, M1). Long-running, operator-launched maintenance operations are tracked as
maintenance_jobsrows (SQLite migration v24 / PostgreSQL0012) so progress survives restarts and is observable from the console. A single background worker processes one job at a time, committing progress per item so pause / cancel / restart are always safe; in a cluster the worker is leader-gated (only the worker-leader node runs it). A job inmaintenancemode drains the S3 API on its node for its lifetime — every S3 request is refused with503 ServiceUnavailable(+Retry-After) AND the health endpoint reportsdrainingso the load balancer stops routing — while the admin API and worker stay live (the graceful-shutdown drain stays non-blocking, by design). New JSON admin API under/admin/maintenance/jobs(create / list / get-with-logs / pause / resume / cancel / clear-history, single-job lock, state-guarded transitions) and a console Maintenance page (launch panel, live active-job card with progress / ETA / rate, log panel, paginated job history with a clear-history action). The re-encryption (M2) and migration (M3/M4) job types build on this foundation.
Fixed¶
- Re-encryption no longer clobbers Object Lock state during cluster convergence (Phase 30). In-place re-encryption changes an object's blob/algorithm/key WITHOUT bumping
last_modified— exactly like a retention / legal-hold change. Both used to reuse the singlelock_updated_atLWW dimension, so during anti-entropy a re-encryption could revert a newer lock state (or a lock change revert a newer re-encryption) whenever the two raced on different nodes. Re-encryption now stamps its owncontent_updated_atdimension (SQLite migration v25 / PostgreSQL0013), andapply_remote_objectmerges the content column group (blob/algorithm/key, bycontent_updated_at) and the lock column group (retention/legal-hold, bylock_updated_at) independently, so the two converge without ever overwriting each other regardless of apply order. migrate-dbmaintenance job requires maintenance mode. The onlinemigrate-dbjob copies every metadata table across separate read transactions, so a concurrent write mid-copy would land in some already-dumped tables but not others, producing a torn destination that per-table row-count reconciliation cannot detect. Alive-mode request is now rejected with a clear message pointing at maintenance mode (or the offline CLI); previously it was accepted and could silently produce an incomplete copy.
0.27.1 — 2026-07-07¶
Added¶
- Per-run blob GC log summary. Each blob GC pass (the single-node background worker and the cluster anti-entropy worker) now emits a concise INFO summary —
blob GC pass startedandblob GC pass complete scanned=… candidates=… reclaimed=… failed=… elapsed_ms=…— so a scheduled run is visible even when it reclaims nothing (previously only a non-zero reclaim was logged). A pass skipped by the fail-safe (an enumeration error) is logged at WARN.arca gclikewise prints the scanned/orphan counts.
0.27.0 — 2026-07-06¶
Added¶
arca gc— reclaim orphaned blob files on a single node. Orphan blobs (on disk, referenced by no live object row, in-progress part, or non-orphan composite sidecar) accumulate from interrupted uploads, overwrites, crashes between the metadata and blob delete, and swallowed blob-delete failures. Until now the only reclamation path was the cluster anti-entropy worker, which is spawned only when clustering is enabled — a single-node deployment never reclaimed them and they grew unbounded (arca fsckcould report them but not remove them). The new offline command reuses the same composite-aware, fail-safe selection as the cluster worker: it previews by default (dry run) and deletes only with--reclaim.--grace-seconds(default 86400) protects freshly-written blobs whose object row may not be committed yet; drop it to0when the server is stopped. Schedule it from cron or run it on demand. The shared selection logic now lives in one place (blob_gc::collect_reclaimable_blobs), used by both the CLI and the anti-entropy worker.- Opt-in single-node blob GC worker. For hands-off operation, a background worker can reclaim orphans on a schedule without an external cron job. Off by default; enable with
[storage] blob_gc_enabled = true(plusblob_gc_interval_seconds, default 3600, andblob_gc_grace_seconds, default 86400). It is skipped under clustering (the anti-entropy worker already reclaims orphans) and exposes thearca_blobs_reclaimed_totalmetric. On very large stores prefer cron-ingarca gcso the full-store scan runs out of the serving process. arca_blob_delete_failures_totalmetric. Counts blob-delete failures during object deletion. When a blob delete fails after its metadata row is already gone, the object is genuinely deleted (a subsequent GET returns 404), soDeleteObjects/DeleteObjectstill report success per S3 semantics — but an orphan blob is left on disk. This counter makes the orphan-creation rate observable (previously visible only inwarn!logs), so operators can see whetherarca gcneeds to run.
Fixed¶
- Console: shared bucket links now land on the bucket after login. Opening a deep-link URL (e.g. a bucket link sent by another user) and then logging in previously always dropped you on the dashboard (admin) or the buckets list (non-admin), discarding the link. The console now preserves the deep link and routes to it after login, only defaulting to home when no destination was given. If the target bucket is inaccessible or missing, the user is bounced home with an explanatory toast —
403→ "You don't have access to bucket …",404→ "Bucket … does not exist". Because S3 returns no error for a non-existent prefix, a missing sub-folder (a non-empty prefix whose listing is entirely empty, with no folder marker) is inferred client-side and returns the user to the bucket root with a "Folder … does not exist" toast; genuinely empty folders still open normally.
0.26.1 — 2026-06-12¶
Security¶
time0.3.41 → 0.3.47 — CVE-2026-25727 fix picked up (resolves TD-011). The Docker builder moved fromrust:1.85-alpinetorust:alpine(currently Rust 1.96), unblocking thetimeversions that contain the stack-exhaustion fix; the MSRV pins (home,serde_with,darling) became unnecessary and were removed from the Dockerfile.time0.3.48 remains excluded for a non-security reason (a coherence clash withrcgen— tracked as TD-017).rustls-pemfileretired (resolves TD-012, RUSTSEC-2025-0134). PEM parsing migrated torustls-pki-types(PemObject), which is maintained as part of the rustls project; the unmaintained dependency is gone from the workspace.
Changed¶
- Full dependency refresh — every crate brought to its latest version. The lockfile had never been systematically refreshed since the project started; this pass updates all ~540 locked packages and bumps 23 declared majors/minors, with the code migrated where APIs changed:
sqlx0.8 → 0.9 (PostgreSQL/MySQL): dynamically composed queries now go through the newAssertSqlSafeaudit opt-in (all of Arca's dynamic SQL is fixed fragments + bind parameters, audited by construction).redis0.27 → 1.2: connection-info overrides moved to the new builder setters.lapin2.5 → 4.10 (AMQP): native tokio integration — thetokio-executor-trait/tokio-reactor-traitshims are gone from the workspace.tonic/prost0.12/0.13 → 0.14 (gRPC): prost codegen moved to the newtonic-prost/tonic-prost-buildcrates; TLS features renamed (tls-ring).quick-xml0.36 → 0.40: entity references now arrive as separate reader events — the manualDeleteObjectsparser accumulates text fragments and resolves character/predefined entities itself.- RustCrypto digest 0.11 ecosystem (
sha20.11,md-50.11,hmac0.13),rand0.10,rcgen0.14 (issuer-based signing API),reqwest0.13 (TLS roots via the platform verifier),brotli8,lz4_flex0.13,governor0.10,async-nats0.49,rumqttc0.25,rdkafka0.39,mongodb3.7,thiserror2,toml1. - Known constraints handled:
rusqlitestays at 0.37 (the latesttokio-rusqliterequires^0.37);timepinned at 0.3.47 (TD-017, see Security). - The integration-test image moved to
python:3-slim; its Python dependencies and the console's CDN libraries already float to latest by construction.
- Process-default crypto provider hardened. The refreshed graph compiles more than one rustls crypto provider (lapin's TLS stack brings
aws-lc-rsalongside Arca'sring), so every constructor that builds TLS machinery (cluster client, membership prober, Vault client, replicator, webhook/Elasticsearch connectors, the rustls config loader) now installs theringprocess default idempotently — previously onlymain()did, which left non-mainentry points (and unit tests) able to hit the multi-provider ambiguity panic.
Fixed¶
- Generated certificates now pass modern strict TLS verification. OpenSSL in strict mode — the default for Python 3.13+ clients — refuses CA-issued certificates without an Authority Key Identifier and CA certificates without the KeyUsage (keyCertSign) extension.
arca tls generateandarca tls generate-clusternow emit both, so freshly minted material works with current AWS SDKs, curl and browsers out of the box. (Surfaced by the refreshed integration-test image; previously generated certificates keep working with clients that do not enforce strict mode — regenerate them to be future-proof.) - Cold Docker builds produced empty workspace crates. The builder's dependency-caching layer compiles dummy crates whose cargo fingerprints are newer than the COPY'd real sources' mtimes, so on a cold build (fresh cache, first build, right after
docker builder prune) cargo considered the real sources unchanged and linked the empty dummies — hundreds ofunresolved importerrors. The real sources are now touched after the COPY; warm builds are unaffected (unchanged context = layer-cache hits all the way). Reproduced on both the old and new toolchain — a latent defect exposed by the cache prune, not by the dependency refresh. - Integration tests aligned with current S3/SDK semantics (no server changes — the server was right): checksum assertions now request
ChecksumMode=ENABLEDon HEAD/GET as S3 requires; suspending versioning on an Object-Lock bucket expects AWS's real409 InvalidBucketState(not 400); the notification event-log test signs withS3SigV4Authlike every other admin test (the genericSigV4Authsigns the payload hash over http but never sendsx-amz-content-sha256, which the server rightly rejects).
0.26.0 — 2026-06-12¶
Security¶
- Verified inter-node mutual TLS with a cluster CA (Phase 29.1 R4, review §3.7(C), decision H12 — resolves TD-015). Inter-node TLS no longer accepts invalid certificates: the
danger_accept_invalid_certsaccommodation is gone. A new[cluster.tls]section distributes an operator-owned cluster CA plus this node's CA-signed cert/key (mint everything with the newarca tls generate-cluster): outbound cluster clients verify peer certificates against the CA and present the node cert as their client identity, while the listener requests client certificates — optional at the TLS layer, because S3 clients share the single port — and/cluster/v1/*refuses any request whose connection did not present one signed by the cluster CA. This is the independent second factor on top of the R3 challenge-response (an attacker now needs the secret AND a CA-signed key), and it closes the passive-sniffing / active-MITM exposure on the cluster network. Breaking: a cluster running over HTTPS now refuses to start without[cluster.tls](fail closed — there is no insecure fallback); plain-HTTP clusters are unaffected (the R3 peer authentication remains their baseline; use TLS on any network you do not fully trust). - Anti-replay window on cluster requests (Phase 29.1 R4, review §3.1/§3.7(C)).
/cluster/v1/*now rejects signed requests whosex-amz-datelies more than 15 minutes from the node's clock (the SigV4 convention). Previously a captured signed request — e.g. a blobGETsniffed off a plain-HTTP segment — remained replayable forever; the signature covers the timestamp, so it cannot be refreshed without the secret. Cluster nodes already require NTP. - Cluster secret strength enforced (Phase 29.1 R4, review M5/§3.7(B)). Startup now refuses a
[cluster].secretshorter than 16 characters and the placeholders shipped in the reference configs (dev-cluster-secret-change-me,CHANGEME-CLUSTER-SECRET), with a warning when the secret looks low-entropy — generate one withopenssl rand -hex 32. The secret keys all inter-node authentication (SigV4 + the peer challenge), so a guessable value hands an attacker the whole cluster. The dev/test cluster configs now ship a floor-compliant dev secret; deployments still on a placeholder fail at startup with a clear message until it is replaced. - Cluster endpoint defense-in-depth (Phase 29.1 R4, review §3.4, M6, D12.1). The JSON cluster endpoints (
object,object/delete,op,manifest,control-snapshot,ping) now cap request bodies at an explicit 2 MiB (blob transfer streams to disk and stays uncapped); theblob_idpath parameter must parse as a UUID before it reaches the blob layer, where it becomes a file name; and the replication receive side drops node-localserver_configkeys (the node identity), so not even a buggy or malicious peer can rewrite who a node is — the filter previously existed only sender-side. - Cluster peers are now authenticated, not just cluster requests (Phase 29.1 R3, review §3.7(A)/D1, decision H12). Inbound
/cluster/v1/*requests were always SigV4-verified, but the outbound direction trusted any endpoint that answered a public health probe: a rogue process on the cluster network registering itself over mDNS with the rightcluster_idwould be counted alive, receive the replication fan-out of every new write with no knowledge of the cluster secret, and sustain a ghost write-quorum. Membership now probes peers with a signedGET /cluster/v1/pingcarrying a fresh nonce, and a peer is authenticated only if its response proves possession of the cluster secret (nonce_mac = HMAC(secret, nonce)— answering 200 proves nothing, a recorded MAC is useless against a fresh nonce). Replication fan-out (objects, blobs, control plane), anti-entropy pulls (manifest, control snapshot, blob repair — also a data-poisoning vector), the write-quorum count and the cluster-minimum capacity now all consider eligible peers only: alive AND authenticated AND config-aligned. Legacy pre-upgrade peers (no ping route) stay visible as alive but are excluded until upgraded — complete a rolling upgrade promptly. On plain-HTTP clusters this closes the rogue-peer attack; passive sniffing / active MITM still require the verified inter-node TLS arriving with R4. - Public cluster health minimized (Phase 29.1 R3, review §3.5/§3.7(B)). The unauthenticated
GET /cluster/v1/healthnow returns only{status, node_id}. It previously exposed the cluster config fingerprint — an offline brute-force oracle for the cluster secret (truncated SHA-256 over mostly-guessable inputs) — plus disk capacity. That detail moved to the authenticated ping, where only secret holders can read it.
Added¶
- Write-aware health check (Phase 29.1 R9, review D5).
GET /admin/health?writable=1answers503 {"status":"read_only"}(withRetry-After) while the node's cluster write gate is closed — write quorum lost or cluster size exceeded — and200otherwise (single-node deployments andavailablemode are always writable). A node that lost the quorum deliberately stays200on the default health check (it serves every read); pointing a separate load-balancer write pool at the write-aware variant routes write methods only to nodes that would accept them, instead of letting ~1/N of the writes fail with a client-visible 503 while the cluster is degraded. Both shipped HAProxy configs carry the write pool as a commented reference backend. [cluster] tombstone_grace_seconds(Phase 29.1 R9). Advanced seconds-granularity override oftombstone_grace_days(≥ 1), for tests and demos that must observe the tombstone-GC liveness guard within seconds; production deployments keep sizing the grace in days.- Cluster operational runbooks and honest consistency docs (Phase 29.1 R9, review D3/D5/D7/D10/D11/D12.4, §5-doc). The HA guide gains an Operational Runbooks section — replacing a dead node (an empty disk is safe by construction), restoring a node from backup (and when not to: a backup older than the tombstone grace), resizing the cluster (cold procedure; why rolling cannot work), rotating the cluster secret, taking coherent backups (filesystem snapshots vs stopped-node copies, SQLite WAL caveat), and forming a cluster from non-empty nodes (a silent union+LWW merge — discouraged, with the safe alternative). The consistency documentation now states plainly what was previously only implied: in
availablemode concurrent writes to the same key are both acknowledged but only the LWW winner survives (the loser is discarded with no error to the client) and durability is single-copy until the fan-out lands (RPO > 0); inquorummode reads are not linearizable (R = 1 by design) and a minority node serves reads that age for a partition's whole duration; the WORM/Object-Lock trust model in a cluster (verbatim remote applies; immutability rests on the secret, the cluster CA and OS access on every node); and the clock-skew test gap is declared instead of implied away. - Tombstone-GC and blob-repair integration tests (Phase 29.1 R9, review §5.4). New cluster test phase on a dedicated overlay: the proactive blob-repair sweep is proven by deleting an object's payload file straight from a node's volume and watching the file reappear (observed on the volume — a client GET would mask the sweep with lazy read-repair) and serve intact bytes; the §3.2 GC liveness guard is proven end-to-end — an object deleted while a node is down beyond the grace blocks tombstone GC on both survivors, the returning node learns the deletion (no resurrection anywhere), and the guard releases.
- Per-node admin views behind the load balancer (Phase 29.1 R8, review D6, decision H9). The audit log, metrics history, notification event log and replication journal are strictly node-local (each node records what it served), so behind a load balancer their console views showed whichever node luck routed to — with nothing saying which. The four list endpoints (
GET /admin/audit,/admin/metrics/history,/admin/notifications/events,/admin/replication/journal) now accept?node=<node_id>: the serving node proxies the query server-side to that peer over the signed cluster transport (newPOST /cluster/v1/admin/*receive routes, behind the same peer authentication, mTLS enforcement and body caps as every cluster endpoint — browsers never talk to nodes directly and the cluster credential never leaves the server side). Only eligible peers are valid targets (unknown node → 404, dead/drifted → 503, unreachable → 502); every response carries a top-levelnodelabel, including the default LB path.?node=allreturns the merged view: a parallel fan-out to every eligible node, rows merged newest-first, each labeled with its source node, plus a per-source result report so a partially-failed fan-out is visible. Pagination of the merged view is per-source-page (each node is asked for the same window and the newest rows across nodes are kept) — an approximation by design, documented instead of hidden behind cross-node cursors. The console gains a node selector in all four views (hidden when clustering is off): default "This node (via LB)" with a badge always naming the node that answered, per-node options, and "All nodes" with per-row source badges; Monitoring draws one chart series per node in the merged view. Clear All and journal Retry are disabled while a node is selected — they operate on the node serving the request, not the viewed one. - Syncing readiness gate for re-entering cluster nodes (Phase 29.1 R7, review D2). A node returning from downtime (or replaced with an empty disk) used to enter the load-balancer rotation immediately and answer
404/ partial listings for data it had not pulled yet — wrong answers given with full confidence, on ~1/N of the routed traffic, with nothing signalling it. A clustered node now tracks the completion of its first anti-entropy pass toward every eligible peer since startup, and until then the plainGET /admin/healthanswers503 {"status":"syncing"}(with aRetry-Afterhint; a shutdown drain takes precedence), so the LB and the Kubernetes readiness probe keep it out of rotation exactly while it could mislead clients. The verbose health stays inspectable (200withstatus: "syncing"); with no eligible peers the node reportsok(degraded-but-serving, there is nothing to sync from). New cluster test phase: a node restarted after downtime must hold the 503 until caught up, and the moment it reports ready the data written during its absence must already be readable on it. - Per-peer sync state exposed (Phase 29.1 R7, review D2/M1).
GET /admin/clusternow reports the node-levelsyncingflag plus, for every peer, this node's pull state toward it: thehwmmanifest cursor, thelagstill to pull (against the peer's ping-reported write cursor),last_reconcile,first_pass_doneandskipped_entries. The console topology card shows a syncing notice and amber per-nodesync lag/skippedindicators when non-zero. - Restore-from-backup rewind detection (Phase 29.1 R7, review D3c). Each node advertises its object write cursor on the authenticated ping; a peer reporting a cursor below what this node already consumed from it was restored from a backup, and its post-restore writes (whose
seqvalues re-use the rewound range) would have stayed invisible to the incremental sync until this node restarted. The high-water mark is now reset automatically (one idempotent full re-pull, logged as a warning). A freshness guard — the report must be newer than the last cursor advance — keeps sustained writes from false-alarming. [cluster] blob_repair_budget(Phase 29.1 R7, review M2). The proactive blob-repair sweep now attempts at most this many peer fetches per anti-entropy tick (default 100, ≥ 1) and resumes where it left off on the next tick — previously a node missing many blobs repaired them all in one pass, monopolizing the anti-entropy worker for hours on a large store. Between budget slices, lazy read-repair still covers anything a client actually asks for.- Complete control-plane reconcile (Phase 29.1 R5, review D9 — resolves TD-016). The anti-entropy snapshot merge now covers EVERY control-plane family, not just the original five: grant attachments (
user_grants,team_grants), team memberships, per-bucket config keys (versioning, encryption, …), bucket tag sets, and cluster-wideserver_configsettings. Each family carries a per-row LWW timestamp (migrations sqlite v22 / pg 0010) and deletions leave tombstones, so a node that was down when a grant was revoked, a membership removed or a bucket setting changed now fully self-heals at re-entry — previously those families replicated in real time only and a missed change persisted until the next write touched it. Child families are parent-filtered in the merge: rows whose parent (user, team, grant, bucket) resolved deleted are never adopted, replacing per-child cascade tombstones. Bucket tag sets reconcile as ONE entity per bucket (matching the replace-all semantics ofPutBucketTagging). - Multipart uploads reconcile across the cluster (Phase 29.1 R5, review D4). In-progress multipart uploads and their part rows are now part of the control snapshot: a node that was down during
CreateMultipartUpload/UploadPartlearns them at re-entry, and amultiparttombstone recorded at Complete/Abort time stops a closed upload from resurrecting off a stale peer.CompleteMultipartUploadon a node that has the part rows but is missing some part bytes (their fan-out never reached it) now repairs them from a peer before assembling instead of failing. - SSE-C objects get cluster read-repair (Phase 29.1 R5, review §3.6). SSE-C blobs always replicated (the sidecar write ships the customer-key-encrypted bytes verbatim — peers never see the key), but a GET on a node that had the object row and not yet the bytes failed until the next anti-entropy pass, because the SSE-C read path bypassed the cluster wrapper. SSE-C reads now repair the missing bytes from a peer synchronously, like every other object.
arca tls generate-cluster(Phase 29.1 R4). Mints the cluster CA plus one CA-signed certificate per node (--node name=san1,san2,..., SANs covering every name/IP peers dial; serverAuth + clientAuth EKUs so the same pair serves as listener certificate and inter-node client identity), and prints the[server.tls]/[cluster.tls]snippets to paste on each node. Operators with their own PKI can supply equivalent material instead.- Zero-downtime cluster secret rotation (Phase 29.1 R4, review D3b, decision H8). New optional
[cluster] secret_previous: inbound cluster authentication accepts the current secret first, then the previous one, while outbound signing, the challenge MAC and the config fingerprint always use the current secret (the ping answers with the secret the request verified under, so probes keep verifying across the window). Runbook: setsecret_previous= old andsecret= new on every node, rolling restart, then removesecret_previous. During the window expect transient drift flags (the fingerprint includes the secret): nodes on the new secret form the writable side once they reach a majority. - HA Hardening plan (Phase 29.1). The remediation plan for the findings of the in-depth Phase 29 HA review is now part of the official roadmap: a new Phase 29.1 section (progress bar, dependency graph, summary table, milestone checklist) plus the full working plan — fixed design decisions, per-milestone detail, and a finding-by-finding traceability table — published as a living page on the documentation site.
- Cluster-size write guard (Phase 29.1 R3, review D3a, decision H6). In
quorummode, if MORE eligible nodes than the configuredcluster_sizeare observed, every node refuses writes with503(fail-closed, no config escape hatch) and/admin/clusterreportssize_exceeded: true. The write majority is derived fromcluster_size, so an over-sized membership (e.g. a 4th node started with the 3-node config) would let two disjoint "majorities" accept conflicting writes — split-brain. Resizing has a documented cold procedure (R9 runbook). - Tombstone-GC liveness guard (Phase 29.1 R3, review §3.2). Anti-entropy now skips the tombstone purge while any known peer has been unreachable beyond the grace window, with a loud warning and a
tombstone_gc_blocked: trueflag on/admin/cluster— purging would erase the very tombstones that returning peer needs to learn its missed deletions, resurrecting deleted data. Membership pruning (below) eventually evicts a never-returning peer so GC cannot stay blocked forever. - Cluster membership pruning (Phase 29.1 R3, review M3). Peers unreachable beyond
[cluster] peer_prune_days(default:tombstone_grace_days) are evicted from membership (and from the admin/console topology). A pruned node that later returns is re-discovered and re-synced like a new candidate; returning beyond the tombstone grace is the documented residual resurrection risk. /admin/clustertopology detail (Phase 29.1 R3). Per-nodeauthenticatedflag (did the node prove possession of the cluster secret on its last probe), cluster-leveleligible_node_count(what the write quorum is measured against, vs. the visibility-onlylive_node_count),size_exceededandtombstone_gc_blocked.[lifecycle]worker interval (Phase 29.1 R6). New optional[lifecycle] interval_seconds(default: 3600, the previous fixed value) controls how often the background worker evaluates bucket lifecycle rules.
Changed¶
- Cluster deploy references aligned (Phase 29.1 R9, review §5-deploy). The production HAProxy config, the test/demo one and the HA-guide snippet now agree: production health checks use
fall 3 rise 2(ride out a transient blip), the test/demo config keepsfall 2 rise 1for fast convergence and says so, and both carry commentedbalance source(sticky read-your-writes) and write-aware-pool examples. The Kubernetes readiness probe tightens toperiodSeconds: 5, failureThreshold: 2(~10 s worst-case window of traffic to a crashed pod) and its comments now describe the syncing semantics introduced in R7. - The lifecycle evaluator is now a cluster singleton (Phase 29.1 R6, review §3.3, decision H5). The object table is fully replicated, so every node used to evaluate the same lifecycle rules and delete the same expired objects — N× the delete fan-out, duplicate audit entries and races between concurrent deleters. Only the worker leader now runs the evaluation tick (expirations, noncurrent-version deletes, stale-multipart aborts): the node with the lowest
node_idamong the eligible nodes, the same authenticated-and-config-aligned predicate that gates the write quorum — so an unauthenticated rogue cannot steal the role and silence the workers. No election protocol: each node computes the role locally from its membership view, and failover to the next-lowest node is automatic at the following membership tick (a brief double-execution window during membership disagreement is accepted — the work is idempotent)./admin/clusterand/admin/health?verbose=1report the node's claim asworker_leader; in a stable cluster exactly one node saystrue. The per-node workers (metrics snapshot, retention purge, notification delivery) and the Phase 28 bucket-replication worker stay un-gated by design: they operate on strictly node-local state — in particular the replication journal records each client write only on the node that served it, so deliveries to external destinations are already exactly-once cluster-wide (the review's duplicate-delivery premise did not hold; gating that worker would instead strand the entries of non-leader nodes). - Configuration export omits the node identity, import refuses it (Phase 29.1 R5, review D12.2).
GET /admin/exportno longer includes the node-localnode_idsettings key, andPOST /admin/importskips it even when present in the document (e.g. an export taken before this change) — importing another node's identity would corrupt cluster loop-prevention. Double defense, mirroring the existing replication-side filter. - Cluster failure detector tolerates one missed probe (Phase 29.1 R3, review D12.3). A peer is declared dead after 2 consecutive probe failures (and alive again at the first success) instead of flapping on a single missed probe — a routine GC pause or dropped packet no longer bounces a node out of the quorum. Dead peers now also keep their
last_seentimestamp in/admin/cluster(how long a node has been unseen is what the GC guard and pruning reason about). - Config drift now excludes a node from the write quorum (Phase 29.1 R3, review D1/H7). A peer whose cluster-critical config differs (
config_ok: false— wrong secret, different master key, different mode) stays visible in the topology but no longer counts toward the write quorum and no longer receives replication fan-out: it could not store the replicas correctly anyway. With 1 of 3 nodes drifted the cluster keeps writing (2 eligible ≥ quorum 2); with 2 of 3 drifted the aligned node refuses writes with503. Drift detection itself no longer depends on the (removed) public fingerprint: a wrong-secret node is detected by its403on the authenticated ping, an aligned-secret/wrong-key node by the fingerprint now exchanged inside the ping. - Real-network-partition test infrastructure (Phase 29.1 R2, review D8/§5.1/§5.2). The 3-node cluster suite grows from 11 to 24 tests and now exercises the scenarios the consistency modes exist for, not just crash-stops.
bin/cluster partition <n>/heal <n>cut a node off the inter-node network while its process stays alive (the cluster compose now uses two networks, so the test runner keeps reaching the isolated node). New phases verify: inquorummode an isolated node refuses writes with503immediately — inside the window where its membership still believes the peers are alive, the missing fan-out ACKs close it (the R1 true-quorum fix at work) — while reads keep being served, the majority side keeps writing, and the healed node converges and becomes writable again; inavailablemode (new config overlay) a split-brain write to the same key is accepted on both sides and exactly one LWW winner survives everywhere after heal, and a 1-of-3 minority node still accepts writes. The catch-up phase now also covers the control plane: a bucket and a credential created while a node is down appear on it after re-entry. - True write quorum in cluster
quorummode (Phase 29.1 R1, review §2.1). A replicated write (PutObject, CompleteMultipartUpload, DeleteObject, delete markers, version deletes) is now acknowledged to the client only when at leastwrite_quorumnodes durably hold it at ACK time: the local copy plus every peer whose fan-out response certified "row applied AND blob present". Previously the quorum was only an admission gate on membership liveness — a write whose fan-out failed on every peer was still acknowledged and could exist on a single machine. On a quorum shortfall the client now receives503 ServiceUnavailablewith aRetry-Afterheader; the local copy is not rolled back (as in any quorum system without distributed transactions, the error means "not acknowledged as replicated", not "undone") and anti-entropy either propagates it or a retry overwrites it.availablemode is unchanged. - Parallel cluster fan-out (Phase 29.1 R1, review §2.4). Replication to peers (object rows, blobs, version deletes, control-plane ops) is now sent to all live peers concurrently instead of one at a time, so a slow peer no longer adds its latency to every write and the ACK counting above sees all responses in one round-trip.
Fixed¶
- Kubernetes liveness probe could kill a node mid catch-up (Phase 29.1 R9). Since the R7 syncing gate,
/admin/healthanswers 503 while a returning node re-pulls what it missed — which is exactly right for readiness, but the shipped manifest pointed the liveness probe at the same path: a catch-up outlasting three probe periods would get the pod restarted in a loop precisely while it was healing. The liveness probe now uses?verbose=1, which always answers 200 on a live process. - Object-Lock state could be silently erased cluster-wide by anti-entropy (Phase 29.1 R7, finding N2). Retention/legal-hold changes keep
last_modifiedunchanged (matching S3), so two replicas of the same object version could differ only in lock state while their last-writer-wins key tied — and the tie was resolved in favor of whichever copy happened to be applied last. The losing sequence: a retention is set while one node is down; another node later restarts and re-pulls that peer's full manifest (its sync cursor is in-memory), re-applies the stale lock-free copy over its own newer lock state, and the rewrite's fresh manifest cursor propagates the regression to every node — the retention silently disappears cluster-wide, with no error anywhere. For a WORM feature this is data loss of the protection itself. Object rows now carry alock_updated_attimestamp (sqlite migration v23 / PostgreSQL 0011), stamped by every retention/legal-hold change, and the replication apply guards order equal-timestamp copies by it — a stale copy can no longer clobber a newer lock state, in either direction. Found by the new cluster catch-up test; root-caused via seq-counter forensics on the preserved node databases. - A poison manifest entry no longer blocks a peer's incremental sync forever (Phase 29.1 R7, review M1). A row that persistently failed to apply stopped the anti-entropy cursor toward that peer at the failing entry, freezing the whole incremental sync (everything behind it included) until a restart. After 5 consecutive failed passes on the same entry the node now skips it with a warning and counts it as
skipped_entriesin/admin/cluster— operator evidence that the key may not converge locally until it changes again on the peer. - Unreadable blob mtimes no longer silently disable blob GC (Phase 29.1 R7, review M8). When a blob file's mtime cannot be read, the GC walk falls back to "just written" — the safe direction (never reclaim), but previously a silent one: on a filesystem that cannot report mtimes, orphan blobs would simply never be collected with no trace of why. The fallback now logs a warning naming the file.
- Object-Lock changes were invisible to cluster anti-entropy (Phase 29.1 R5, finding N1).
PutObjectRetention/PutObjectLegalHoldupdated the object row without stamping a fresh manifest cursor (seq) on either backend, so a peer that was down during a lock change never received it — only the real-time fan-out carried it, and a dead node misses that by definition. For a WORM feature this silently weakened the guarantee on the returning node. Lock updates now stamp a freshseq, so the changed-since manifest re-delivers the row with its lock columns at re-entry. - Steady-state anti-entropy churn: identical rows were endlessly redelivered (Phase 29.1 R5, review M7).
apply_remote_objectrewrote the row (stamping a freshseq) even when the incoming copy was byte-identical to the local one. Two caught-up nodes therefore kept re-delivering their whole object tables to each other on every anti-entropy pass, forever — each apply bumped aseqthe peer then saw as new. An identical incoming row is now a no-op (the equal-timestamp guard still lets through rows that differ only in lock columns, which N1 above relies on), so a converged cluster reaches a quiet fixed point. - PostgreSQL: rows could be skipped by cluster incremental sync (Phase 29.1 R1, review §2.2). The PG backend assigned the anti-entropy manifest cursor (
seq) from a PostgreSQLSEQUENCE, which is not transactional: a row could commit with a lowerseqafter a peer had already advanced its cursor past it, so the incremental sync silently never delivered it. The sequence is replaced by a commit-ordered single-row counter (object_seq, pg migration 0009, mirroring the SQLite design): the counter's row lock serializes assignment until commit, so cursor order now equals commit order and no committed row can be skipped. - Control-plane merge could resurrect deleted entities on crash (Phase 29.1 R1, review §2.3). The anti-entropy control-plane merge executed entity deletions before adopting the corresponding tombstones, each in its own transaction: a crash between the two left "row deleted, no tombstone", and the next reconcile with a peer still holding the live row re-created it — for a revoked credential, a security hole. Tombstones are now adopted first, so an interrupted merge always converges to the deletion.
- Cluster test flakiness (Phase 29.1 R2, review §5.3). Two latent races in
bin/test cluster: the post-convergence wait was shorter than HAProxy's eviction time (fall 2 inter 2s≈ 4 s), so a test could hit the load balancer while a stopped node was still in rotation; and the 507 phase could read the cluster-minimum free space before the tiny-disk node was alive in the topology (its stats are cleared while it is considered dead). The wait now gives the LB a 5 s margin and the 507 precondition requires all 3 nodes alive.
0.25.1 — 2026-06-10¶
Changed¶
- Performance: per-bucket replication-config cache. Object writes (PutObject, CompleteMultipartUpload, delete markers, PutObjectTagging) no longer read the bucket's
replication_configurationfrom the metadata store on every request. The result is cached per bucket for 30 seconds — mirroring the existing per-bucket encryption cache — so buckets without replication (the common case) skip the database query entirely on the write hot path. The cache is invalidated immediately onPutBucketReplication,DeleteBucketReplication, and the admin credential-cascade disable, so a configuration change still takes effect at once. - S3 compatibility scope. Ceph s3-tests for RGW-only extensions (
x-rgw-*headers,?list-type=unordered, account usage) — which are not part of the AWS S3 API — are now excluded from the reported compatibility denominator. A new "Intentional divergences from AWS S3" section in the API reference documents Arca's deliberate allowance of enabling Object Lock on an existing empty bucket (AWS permits it only at bucket creation).
Fixed¶
- Intermittent
400 Bad Requeston a keep-alive connection reused immediately after a rejected conditionalPutObject. When aPutObjectwas rejected before its request body was read (a failedIf-Match/If-None-Matchprecondition →412, orNoSuchBucket/NoSuchKey/EntityTooLarge/ invalid tagging), the unconsumed request body could desync the HTTP/1.1 keep-alive connection, so the next request parsed on it intermittently failed with a protocol-level400. These early-reject responses now sendConnection: close(matching AWS S3), so the client never reuses a possibly-desynced connection.
0.25.0 — 2026-06-03¶
Added¶
- High-availability clustering (Phase 29). Arca can now run as a symmetric, self-configuring, fully-replicated multi-node cluster — every node is identical, holds the full dataset, and serves reads and writes. Enable it with a single
[cluster]config section (orbin/arca --clusterfor local dev); there is no special "primary" and no separate coordinator. - Real-time replication of the whole data and control plane: objects (including multipart uploads, object tags, retention and legal hold), buckets and bucket config, and identity/authorization (credentials, users, teams, grants) and server settings. A write to any node fans out to its peers, so a client behind a load balancer can read its write from any node.
- Two consistency modes.
quorum(default, CP): a write needs a majority of nodes; if a node loses the majority it goes read-only and rejects writes with503 ServiceUnavailablerather than risk divergence.available(AP): always writable, best-effort replication. Last-writer-wins conflict resolution with a deterministicblob_idtiebreak. - Self-healing. A returning or lagging node catches up automatically: an anti-entropy worker reconciles objects (changed-since manifests) and the control plane (full-snapshot merge), proactively repairs blobs whose bytes a node is missing, and garbage-collects orphaned blobs (composite-multipart-aware). Tombstones ensure a delete is never resurrected by a peer that missed it.
- Peer discovery via mDNS (LAN), static seed list, or DNS (e.g. a Kubernetes headless Service).
- Cluster-aware storage capacity. Because every node holds the full dataset, the cluster's capacity is bounded by its smallest node; the dashboard reports the cluster-minimum free space and a write larger than that is rejected with
507 InsufficientStorage, even if the node serving it has room. - Config-drift detection. A node whose cluster-critical config (cluster id / secret / mode / size / master key) differs from its peers is flagged on
/admin/clusterand in the console, without the cluster refusing to run. GET /admin/cluster— cluster topology, consistency mode, write-quorum status, per-node liveness and config alignment, and cluster-effective disk capacity ({"enabled": false}on a single node).GET /admin/health?verbose=1— health plus the cluster snapshot for operators; the default (non-verbose) health shape consumed by load balancers is unchanged.- (Console) Cluster topology on the Dashboard: a dedicated topology card showing the consistency mode, write status, and a live node list (online/offline, "this node", endpoints, last-seen, config-mismatch flags). The card is now shown on single-node deployments too, where it presents the single local node — the dashboard layout is identical whether or not clustering is enabled.
- Tooling and deployment.
bin/clustermanages a local 3-node cluster (compose + HAProxy);deploy/kubernetes/arca-cluster.yaml(StatefulSet + headless-Service DNS discovery) anddeploy/haproxy/(HAProxy + keepalived floating VIP) provide production templates; a High Availability guide documents the design, trade-offs, and operations.
0.24.0 — 2026-05-21¶
Added¶
- (Console) Keyboard navigation across the file list, turning the side panel and the fullscreen preview into a slideshow. ↑ / ↓ walk the list and reload the inline preview in place if it is open; ← / → step through the fullscreen modal. The fullscreen header gains a
position / totalcounter and ‹ › arrows for mouse users, and the next two images are prefetched so navigation feels instant. Editable fields (search, tags) keep their native arrow behaviour. - (Console) The sidebar identity block now shows the authenticated user's username next to the Admin / User badge, with the access key on a dedicated muted line underneath instead of a truncated access key. Works for non-admin users too.
GET /admin/meis now identity-only: any valid Arca credential can read its own username, with noarca:*grant required. Previously the username probe neededarca:ViewServerInfo, which forced admins to attachAdministratorAccessto every user just to populate the sidebar — and that side-effect also exposed the empty Users / Teams / Grants / Audit Log pages where every call inside was a 403. The grant gate on the rest of the admin API is unchanged.- (Console) "Open in new tab" button in the fullscreen preview header. Useful when the in-iframe renderer is weak — most notably macOS Safari, which displays PDFs as a tiny thumbnail strip in iframes but renders them correctly at top level (where macOS users can also hand off to Preview.app).
Changed¶
- (Console) Server card on the Dashboard now sits in a stable 3×2 grid with a new Topology tile (currently "Single node", will reflect multi-node in Phase 29). Longer labels like
Per-bucket (Vault)no longer wrap. - (Console) The drag-and-drop zone in the bucket browser now stretches to the bottom of the file area instead of hugging the first few rows. Anywhere under the file list is droppable; the sidebar is not.
Fixed¶
- (Console) The Effective Grants tab on the user-detail page now refreshes automatically after attaching / detaching a direct grant or joining / leaving a team. Previously the tab kept showing the stale state until the user navigated away and back.
0.23.1 — 2026-05-07¶
Fixed¶
- Plain
PutObjectandUploadPartno longer block tokio workers on MD5. The previous pipeline interleaved hashing and disk writes on the same async task, so the CPU-bound MD5 work serialized the runtime that was supposed to be pulling network bytes — leaving plain uploads slower than encrypted ones after v0.23.0 moved AEAD off the runtime (the symptom that triggered this release). MD5 and write now run on a dedicated blocking worker fed by a bounded channel.
Single-stream (1 × 1 GiB, tmpfs): plain steady at ~266 MB/s; encrypted 188 → 235 MB/s (+25%). Concurrent (4 × 256 MiB): plain ~833 → ~862 MB/s (+3.5%), encrypted 594 → 706 MB/s (+19%) — the largest win where the runtime was most contended (encrypted, low parallelism), exactly the PBM-backup workload.
- (Console) Admin error toasts now show the backend's descriptive message (e.g. Username "root" already exists) instead of the bare HTTP category (e.g. Conflict). The S3 endpoints that were still surfacing Error ${status} were converted to use the existing <Message> extractor at the same time.
Added¶
- (Console) Soft duplicate-description warning on Create / inline-edit forms for users, teams, grants and credentials. Advisory only — descriptions remain free-form, the save path is unchanged.
0.23.0 — 2026-05-06¶
Fixed¶
- Encrypted multipart uploads no longer pay the 2× decrypt+re-encrypt tax on
CompleteMultipartUpload. The defaultconcatimplementation used to decrypt every part and re-encrypt the whole final blob, so a Percona PBM MongoDB backup that took 1h plain took 2h with SSE-S3. The encrypted store now produces a composite sidecar that points at the still-on-disk encrypted parts (no copy, no extra crypto). Reads only decrypt the chunks that overlap the requested range; deletes cascade to the parts. Sidecar schema is additive, so old sidecars still deserialize. - Plain multipart uploads also benefit from the composite sidecar. Same trick when every part is plain (no encryption, no compression), eliminating the byte-by-byte copy that previously dominated
CompleteMultipartUpload. Encrypted-or-compressed parts still take the existing copy + MD5 path. - HTTP/2 keep-alive no longer panics.
keep_alive_intervalon the hyper auto-builder requires a Timer; the TLS server now installs one explicitly so internal pings don't abort the runtime.
Bench (4 concurrent multipart uploads, 256 MiB each, 8 MiB parts, tmpfs)¶
| Scenario | Throughput | CompleteMultipartUpload p50 |
|---|---|---|
| Plain baseline | 527.8 MB/s | 0.71s |
| Encrypted baseline | 207.7 MB/s | 3.03s |
| Plain after fix | 836.9 MB/s | 0.003s (≈237× faster) |
| Encrypted after fix | 594.9 MB/s | 0.004s (≈758× faster) |
Changed¶
- AEAD chunk crypto runs off the tokio async runtime. The encrypting stream now batches 4 × 64 KiB chunks and hands each batch to a
spawn_blockingworker; up to 4 batches fly per stream so network reads, disk writes and CPU-bound AEAD overlap instead of serializing on the same worker thread. Tag and ciphertext are produced in place — one allocation per batch instead of two per chunk. - Tokio runtime is now built explicitly instead of via the
#[tokio::main]macro. New[server.runtime]config section:worker_threads,max_blocking_threads(both default tonum_cpus). - HTTP/2 server tunables exposed via
[server.http]: max concurrent streams, keep-alive interval/timeout, initial stream/connection window. The TLS auto-builder is now constructed once and shared across connection tasks. - Process-wide
SystemRandomfor DEK / nonce generation (previously a fresh instance per call).
Docker¶
docker-compose.ymlnow setsnofile=65535andnet.core.somaxconn=4096on thearcaservice so connection fan-out (multipart, parallel PBM workers) doesn't hit a hidden FD ceiling. Newdocker-compose.perf.ymloverlay (tmpfs/datafor reproducible perf measurements) is layered on automatically bybin/perf-test --tls/--encryption.
Benchmarks¶
perf_test.pygained aparallel-multipartscenario that issues N concurrent multipart uploads and reportsCompleteMultipartUploadtime separately. Used to detect the encrypted-concat regression and verify the fix.
Added¶
- (Console) New
2dtimeframe button in the Monitoring view, between24hand7d. - (Console) Replication credentials are now created and managed from a dedicated modal flow with inline location hints, instead of the old free-form text fallback.
Documentation¶
- (Console manual) New screenshots covering the replication setup and journal flow.
- (Project report) Refreshed with v0.23.0 metrics: ~23 full-time-equivalent days, 285 commits, 72,651 LoC, 2,152 tests, 28/31 phases done.
Tech debt¶
- TD-014 (recorded, not yet fixed):
arca recoverandarca fsckdo not understand composite blobs and currently flag them as "orphaned sidecar". Runtime S3 reads/writes are unaffected. Proper fix sketched inTECH_DEBT.md.
Console (other)¶
- Anchor monitoring chart edge x-ticks so the leftmost / rightmost labels can no longer clip out of the SVG viewport.
0.22.0 — 2026-04-22¶
Added¶
- Phase 28 — Replication (P3). Asynchronous cross-instance replication for disaster recovery and geographic distribution. One-way per-rule with explicit loop prevention, so two-way mirrors converge instead of bouncing forever. Destination is any S3-compatible endpoint (Arca, AWS S3, MinIO, …) and outbound requests are signed with full AWS SigV4. Versioning is required on the source bucket (AWS CRR semantics).
- Change journal records every pending / in-flight / failed delivery in a
replication_journaltable (SQLite v17 / Postgres0004). Per-event-type dispatch (PUT / delete-marker / tag), last-writer-wins conflict resolution against the destination'sLast-Modified, exponential backoff capped at one hour, and aFAILEDstate stamped only after the terminal attempt so transient failures don't leak to clients. - Loop prevention: every outbound request carries
x-amz-arca-replication-source: <source_endpoint_id>. Receiving Arca tags the objectREPLICAand skips journal emission. Mirror configurations are integration-tested end-to-end: a single PUT on the source reaches the mirror exactly once and never bounces back. x-amz-replication-statusresponse header on the relevant object handlers, with valuesPENDING/COMPLETED/FAILED/REPLICA.- Admin API for managing the journal (list with filters, retry an entry) and destination credentials (list, create, delete, query usage). Deleting a credential cascade-disables every rule that references it so the worker can't loop forever against a vanished reference.
/admin/infogained areplication_enabledflag. PutBucketReplication/GetBucketReplication/DeleteBucketReplicationS3 handlers with Arca-flavoured XML extensions (<Endpoint>,<CredentialRef>,<Region>alongside the standard<Bucket>).[replication]config section with poll interval, batch size, retry budget, request timeout, source endpoint id, and journal retention windows (30 days for completed rows, 90 day hard cap so the table stays bounded during an extended outage).- Docker replication test mode:
bin/arca start --replicationboots a secondarca-replicainstance on:9001;bin/test replicationruns the full two-instance integration suite (4 boto3 tests: PUT, delete-marker, tag, mirror no-loop). - (Console) Per-bucket Replication card in Bucket Settings with an amber "Versioning required" banner that links to the versioning card, rule rows showing
source ➜ destinationwith inline Enabled/Disabled toggles and tag-filter chips, and a modal editor (Prefix + Tag filter, Destination, Credential Ref dropdown with an inline "+ New" flow). - (Console) New admin view at
#/replication— Destination Credentials card above the Replication Journal. The journal has inline column-header filters, color-coded status chips with a pulse dot onin_flight, a side detail panel, pagination, auto-refresh every 30 s, and per-row / per-panel "Retry delivery now" CTAs on failed rows. - (Docs) New
guide/replication.mdcovering setup, the loop-prevention contract, two-way mirrors, destination-credential management, journal and retry workflow.
Changed¶
ObjectRecordgained areplication_statusfield; wire format and existing migrations are unchanged.
0.21.0 — 2026-04-20¶
Added¶
- Phase 27 — Transparent at-rest compression (P2).
CompressingBlobStorewrapsFsBlobStore/EncryptingBlobStoreto compress plaintext before encryption and disk write, while keeping the wire format unchanged (ETagis still MD5 of plaintext;Content-Lengthreports plaintext size). Six algorithms ship:zstd,lz4,snappy,gzip,brotli,xz. Anautomode picks per-object via a deterministic rule table based onContent-Typeand size (no sampling or ML). Compression is per-bucket, console-managed — the wrapper is always installed and activates only when a bucket has compression configured via the console or the Arca-specificPUT/GET/DELETE /{bucket}?compressionsubresource. Baked-in MIME and size filters skip already-compressed content; skipped reasons are surfaced as Prometheus counters. Chunked frame format with a footer chunk index enables O(1) ranged reads. Mixed-mode: compressed and plain blobs coexist transparently via sidecar metadata. arca compress-existing/arca decompress-existingCLI: offline atomic retrofit of an existing data directory (resumable via sidecars,--dry-run,--bucket,--algorithmoverrides).arca fsckalready recognizes.compressing.tmporphans.- Prometheus series:
arca_compression_plaintext_bytes_total{algorithm},arca_compression_compressed_bytes_total{algorithm},arca_compression_skipped_total{reason},arca_storage_compression_ratio. - (Console) Per-bucket Compression card in bucket settings — matches the Object Lock card pattern: inline algorithm dropdown + level input + single Enable/Disable button, with an "Enabled" status badge and current algorithm/level displayed when active.
- (Console) Inline help system: every setting carries a "?" trigger next to its label. Hovering shows a short one-line hint tooltip; clicking opens a modal with the full explanation, bullets, rows, notes and code samples. Backed by a single Alpine component (
helpTrigger(topicId)) and a global modal listening forarca:open-helpcustom events. All 13 starter topics live inconsole/js/help.js; adding help to a new setting is one topic entry plus a six-line snippet next to the label. - (Console) Help coverage pass: bucket-settings cards (encryption, compression, versioning, object-lock, lifecycle, notifications), Settings page (log level, retention windows, S3 region, preview size limits) and the dashboard bucket-size widget (now also shows compression and object-lock badges, previously missing).
- (Console) Create-bucket dialog gained an Enable Object Lock checkbox. When ticked, the
PUT /{bucket}request sendsx-amz-bucket-object-lock-enabled: trueso Object Lock and versioning are enabled atomically with the bucket. The API client'srequesthelper now forwards caller-supplied headers through SigV4 signing. - (Console) Irreversible bucket-setting toggles (first-time Versioning enable, Object Lock enable) now require typed bucket-name confirmation in a modal before firing. Prevents accidental clicks in production (versioning cannot be disabled, only suspended; Object Lock cannot be disabled at all).
- (Docs) New
guide/compression.md, new Compression note inguide/configuration.md, new CLI entries forcompress-existing/decompress-existing. Console user guide updated for the new Object Lock enablement flow, confirmation modals and card intros.
Changed¶
- Object Lock can now be enabled on empty existing buckets, not only at bucket creation.
PutObjectLockConfigurationused to reject every call on a bucket whoseobject_lockconfig was absent withInvalidBucketState(strict S3 semantics). The handler now accepts the request when the bucket still has no objects and auto-enables versioning as before; buckets that already contain objects are still rejected, with a clearer message ("can only be enabled on empty buckets or at bucket creation"). Three new integration tests cover the accepted-empty / rejected-non-empty / accepted-at-creation matrix. - (Console) Buckets list cards: creation date moved to its own line above the capability badges, and the badge row now wraps so four or more indicators (encrypted + versioned + locked + compressed) no longer overflow the card.
- (Console) Bucket settings page normalized: every card (Encryption, Compression, Versioning, Object Lock, Lifecycle, Event Notifications) now opens with a short plain-English intro paragraph explaining what the setting does and whether it can be turned off. Versioning and Object Lock intros carry the amber "This action cannot be undone." red-thread, matching the Object Lock card that set the pattern.
- (Console) Compression and Object Lock inline controls (dropdowns, inputs, buttons) harmonized with the global Settings page baseline (
text-sm px-3 py-1.5); no more size discrepancy between pages.
0.20.0 — 2026-04-18¶
Added¶
- Kafka notification connector: produce S3 events to Kafka topics via
rdkafka(librdkafka). Supports SASL authentication, configurable security protocol, and custom topics - AMQP notification connector: publish S3 events to RabbitMQ via
lapin(pure Rust AMQP 0-9-1). Supports custom exchanges, routing keys, durable queues, and publisher confirms - Elasticsearch notification connector: index S3 events as documents via REST API (
reqwest). Supports custom indices and basic auth. Zero new dependencies - Syslog (RFC 5424) notification connector: send S3 events as syslog messages over UDP or TCP. Configurable facility, severity, and app name. Zero new dependencies
- SMTP notification connector: deliver S3 events as e-mail via
lettreover SMTP or SMTPS. Supports STARTTLS, PLAIN authentication, custom subject/sender, and configurable timeout (smtp_timeout_seconds, default 15 s). Body is the same S3 event JSON the webhook connector posts - gRPC notification connector: deliver S3 events as a unary
arca.notifications.v1.NotificationService/NotifyRPC viatonic+prost. Supports h2c and TLS, Bearer token via gRPC metadata, custom CA certificates for self-signed servers, and arbitrary user metadata forwarded through the proto map. Configurable timeout (grpc_timeout_seconds, default 10 s) - Connector configuration guide (
guide/connectors.md): single-page reference for all 13 connectors with destination URL formats, properties, XML examples, and timeout tuning - (Console) Connector-specific form fields for Kafka, AMQP, Elasticsearch, Syslog, SMTP, and gRPC (all now active in the connector picker)
- (Docs) Rewrote the Event Notifications section of the console user guide with dedicated Webhook / SMTP / gRPC form examples and three new screenshots
Fixed¶
- Kafka/AMQP connector integration tests: resolved TD-013. The AMQP tests used to fail because
wait_for_amqp_receiverrandocker exec rabbitmq-diagnosticsas root in a tight loop, racing with RabbitMQ's.erlang.cookieinitialization and crashing the container (EACCES). Switched the AMQP and Kafka readiness helpers todocker inspect-based health polling so they never exec into the receiver. Also fixed the Kafka test subscriber (manual partition assignment +seek_to_endavoids consumer-group coordination races) and raised the SigV4 admin-API default timeout so thetest-connectorfailure path for Kafka's unreachable host does not time out the HTTP client before Arca can respond
Removed¶
- AMQP DNS pre-resolution workaround: the
resolve_urihelper that rewrote AMQP URIs to use a tokio-resolved IP was added under the false TD-013 diagnosis. With the real fix in place,lapin/tcp-streamresolves hostnames correctly from the Alpine/muslarcabinary, so the workaround is gone and the connector passes the URI straight toConnection::connect - ONVIF connector: dropped from Phase 26 before any implementation shipped. The
ConnectorType::Onvifenum variant, the console picker entry, and the roadmap checklist item have been removed. ONVIF integration was judged too niche to justify its maintenance cost; an external adapter can bridge ONVIF events into Arca via the webhook or gRPC connector when needed - "Coming soon" paths in the console: since every connector is now implemented, removed the dead
activeflag onCONNECTOR_TYPES, thesoonlabel + disabled-tile styling in the picker, and the conditional templates that were only reachable for unimplemented connectors
0.19.0 — 2026-04-15¶
Added¶
- Configuration export API:
GET /admin/exportreturns full instance configuration as JSON (settings, users, teams, grants, credentials, buckets, bucket configs). Supportssectionsquery parameter for selective export andinclude_secretsfor secret key visibility (masked by default) - Configuration import API:
POST /admin/importapplies exported JSON to a running instance. Three conflict modes:skip(default),overwrite, anddry_run. Processes sections in dependency order, automatically skips masked credentials - (Console) Export/Import buttons in Settings page with polished modals: section checkboxes with aligned labels, secrets toggle with warning, drag-and-drop file upload, conflict mode selection, per-section result display
- Configurable log level with runtime reload: log level can be changed via
PUT /admin/settings/log_leveland takes effect immediately without restart - Git commit hash at startup: compile-time embedded git hash logged on server start for traceability
- SQLite read connection pool: separate read-only connection pool for concurrent query execution
BlobStore::concat: efficient multipart assembly without intermediate copies- Batched audit writes: dedicated writer task with channel-based batching to reduce per-request overhead
- Admin & Server Enhancements section in roadmap for tracking features outside numbered phases
Changed¶
- Request logging moved from INFO to DEBUG level to reduce noise
Fixed¶
- Unused imports in multipart.rs and validate.rs tests
- Timeline divider label in project report (changed from "MVP" to "NOW")
0.18.2 — 2026-04-11¶
Changed¶
- Performance: release profile optimization — enable
opt-level = 3, thin LTO,codegen-units = 1, and symbol stripping for significantly faster crypto, hashing, and XML parsing - Performance: TLS session resumption — enable server-side session cache (256 entries) to avoid full handshakes on client reconnections
- Performance: TCP_NODELAY on TLS connections — reduce latency on small responses (HEAD, DELETE, errors)
- Performance: SQLite tuning — set
synchronous=NORMAL(safe with WAL), 64 MB page cache, 256 MB mmap,temp_store=MEMORY, 5 s busy timeout - Performance: 64 KB ReaderStream chunks — increase from 8 KB default, reducing syscalls by ~8x on large object downloads
- Performance: eliminate double allocation in decryption — reuse the in-place decryption buffer instead of copying plaintext
- Performance: compact sidecar JSON — drop pretty-printing for smaller sidecar files and faster I/O
Fixed¶
- Unused imports in
arca-protomiddleware (std::net::IpAddr,http::StatusCode)
Added¶
- Perf-test summary table: all test results in a single table with throughput and latency percentiles
- Global Performance Index: weighted geometric mean score for run-to-run comparison
- Perf-test
--tlsflag: self-contained TLS testing with self-signed certificates - Perf-test
--tls-fqdnflag: TLS with existing certificates and FQDN verification - Perf-test
--encryptionflag: self-contained encryption testing - Perf-test
-q/--quietflag: show only the summary table
0.18.1 — 2026-04-10¶
Added¶
- NATS notification connector: delivers S3 event notifications by publishing JSON payloads to a NATS subject. Supports custom subject names via
subjectproperty (defaultarca.notifications), optional token or user/password authentication, and configurable connection timeout (nats_timeout_seconds). Includes admin API connectivity test viaPOST /admin/notifications/test-connectorwithconnector_type=nats - (Console) NATS connector enabled in notification editor with subject, token, and user/password configuration fields
- MQTT notification connector: delivers S3 event notifications by publishing JSON payloads to an MQTT topic. Supports custom topic names via
topicproperty (defaultarca/notifications), configurable QoS level (0/1/2, default 1), optional username/password authentication, and configurable connection timeout (mqtt_timeout_seconds). Includes admin API connectivity test viaPOST /admin/notifications/test-connectorwithconnector_type=mqtt - (Console) MQTT connector enabled in notification editor with topic, username, and password configuration fields
- Database notification connectors (PostgreSQL, MySQL, MongoDB): delivers S3 event notifications by inserting rows/documents into a database table or collection. Shared
db_commonmodule provides DDL generation and event field extraction. Tables/collections are auto-created on first delivery - PostgreSQL connector:
sqlx-postgresdriver,TIMESTAMPTZcolumns, supportstableandschemaproperties (default:arca_notifications) - MySQL connector:
sqlx-mysqldriver,DATETIME(6)columns, supportstableproperty (default:arca_notifications) - MongoDB connector:
mongodbdriver, BSON documents, supportsdatabase(default:arca) andcollection(default:arca_notifications) properties - (Console) PostgreSQL, MySQL, and MongoDB connectors enabled in notification editor with database-specific configuration fields
- Deployment files: Kubernetes manifests (Deployment, Service, PVC, ConfigMap), systemd unit file, and sample configuration file for production deployments
- Troubleshooting guide: documentation page with common issues, diagnostic steps, and solutions
Fixed¶
- SIGHUP killing the process when TLS is not enabled: signal handler crashed on
unwrap()of the TLS reloader when TLS was not configured - (Console) STORAGE SIZE chart Y-axis showed "undefined" labels when all values were 0 (fractional tick values caused negative index in
formatBytes)
0.18.0 — 2026-04-09¶
Added¶
- Redis Pub/Sub notification connector: delivers S3 event notifications by publishing JSON payloads to a Redis Pub/Sub channel. Supports custom channel names via
channelproperty, optional password authentication, and configurable connection timeout (redis_timeout_seconds). Includes admin API connectivity test viaPOST /admin/notifications/test-connectorwithconnector_type=redis - Dedicated connector test stream:
bin/test connectors redis(and futurebin/test connectors all) runs connector integration tests against real Docker-based receivers, separate from the normal development test flow - Presigned URL tracking: generated presigned URLs are now tracked in the database (metadata only, not the URL itself, for security). Active shares are visible in the console with a permanent blue share icon next to shared files. Clicking the icon shows a detail modal with method, duration, remaining time, creator, and a button to remove the record. Expired records are purged automatically by the retention worker
GET /admin/presigned-urls?bucket=X: lists active (non-expired) presigned URL records for a bucketDELETE /admin/presigned-urls/:id: removes a presigned URL tracking record (the URL itself remains valid until expiry)POST /admin/presignresponse now includes anidfield for tracking reference (SQLite migration v16, PostgreSQL migration v3)- (Console): Monitoring chart time range buttons expanded with 6m and 1y presets, plus a custom date range picker (From/To datetime inputs)
bin/seed-metrics: development utility to generate fake metrics data for testing monitoring charts
Fixed¶
- Monitoring chart time ranges: 24h, 7d, and 30d views only showed ~8 hours of data because the API returned
LIMIT 500most-recent rows. Fixed with server-side downsampling usingROW_NUMBER()CTE to return evenly spaced points across the full range (both SQLite and PostgreSQL) - (Console): X-axis labels now adapt to actual data span (time-only for short ranges, date+time for days, month+day for weeks, month+year for long ranges) instead of being hardcoded per button
- Ceph s3-tests: 18 additional tests now pass (lifecycle, object lock, versioning, encryption)
0.17.1 — 2026-04-03¶
Added¶
- Modular notification connector architecture: trait-based
NotificationConnectorinarca-corewithConnectorRegistryfor pluggable delivery backends. Webhook is the first implementation; 9 additional connectors (Kafka, AMQP, Redis, NATS, MQTT, PostgreSQL, MySQL, MongoDB, Elasticsearch) planned for Phase 26 ConnectorTypeenum: all 10 connector types defined withdisplay_name(),category(),all()helpers for UI enumerationWebhookConnector: extracted webhook delivery logic into a standalone connector implementing theNotificationConnectortrait, with Bearer token authentication support viaauth_tokenproperty- Webhook auth token:
DestinationConfignow supportspropertiesmap (Arca extension) for connector-specific settings. Webhook connector readsauth_tokenand sendsAuthorization: Bearer <token>header POST /admin/notifications/test-connector: generalized admin endpoint for testing any registered connector type; existingtest-webhookendpoint preserved as backward-compatible alias with auth_token support- (Console): Notification editor redesigned as a modal dialog (replacing inline lifecycle-style forms) with connector type selector grid showing all 10 types grouped by category (Functions, Queue, Database), SVG icons, and "coming soon" badges for unimplemented connectors
- Roadmap Phase 26: Notification Connectors phase added (9 connectors, modular per-connector Docker test infrastructure)
Changed¶
- (Console): Settings page reorganized: "Monitoring" section renamed to "Data Retention" with all retention policies grouped together (Audit Log, Notification Events, Metrics Snapshots), ordered by sidebar position
- Notification event retention is now a server setting (
notification_retention_days) manageable from the console, following the same TOML > DB > default precedence as audit and metrics retention (default: 7 days) - Notification worker refactored to dispatch via
ConnectorRegistryinstead of hardcoded webhook delivery. Retry logic (exponential backoff) is now generic across all connector types DestinationConfigextended withconnector_type(default: Webhook) andproperties(HashMap) fields, both Arca extensions preserved in JSON storage and XML serializationNotificationEventRecordextended withconnector_typefield (SQLite migration v15, PostgreSQL migration v2)- (Console): Section title changed from "Notification Webhooks" to "Event Notifications"; notifications list shows connector type badge and icon
- Webhook test receiver (
docker/webhook-receiver/server.py) now supportsAUTH_TOKENenv var for Bearer token validation (returns 401 on mismatch)
Fixed¶
- (Console): Monitoring chart line clipping at maximum values: Y-axis scale now always extends above the data maximum, preventing the line from being drawn outside the SVG viewBox
0.17.0 — 2026-04-02¶
Added¶
- Phase 25: Notifications and Event System
PutBucketNotificationConfiguration/GetBucketNotificationConfiguration: S3-compatible bucket notification configuration via?notificationquery parameter. Accepts all three S3 destination types (TopicConfiguration,QueueConfiguration,CloudFunctionConfiguration), treating all as webhook destinations- Event emission:
s3:ObjectCreated:Put,s3:ObjectCreated:Copy,s3:ObjectCreated:CompleteMultipartUpload,s3:ObjectRemoved:Delete,s3:ObjectRemoved:DeleteMarkerCreatedevents emitted from handlers via non-blocking mpsc channel - Webhook delivery worker: background worker consumes events, matches per-bucket notification rules (event type + prefix/suffix filters), delivers HTTP POST to configured webhook URLs with exponential-backoff retry (configurable max retries, base delay, timeout)
- Notification event persistence:
notification_eventstable (SQLite migration v14, PostgreSQL schema update) stores delivery records with status tracking (pending/delivered/failed) - Per-webhook Enabled/Disabled toggle: Arca extension persisted via
<Enabled>XML element, allowing temporary webhook suspension without removal NotificationStoretrait: CRUD + purge for notification event records, implemented for both SQLite and PostgreSQL backends- Admin API:
GET /admin/notifications/events(list with filters),GET /admin/notifications/events/count,DELETE /admin/notifications/events(clear with confirmation),POST /admin/notifications/test-webhook(connectivity test) [notifications]config section (optional): tuning parameters for channel size, retry behavior, webhook timeout, and event retention days (default: 7)- Event retention: notification events automatically purged by the retention worker based on
event_retention_days - Notification config caching: in-memory cache with 60s TTL in the delivery worker to avoid per-event DB reads
- (Console): Notification event log viewer with audit-style inline column header filters (Time, Event, Bucket, Key, Destination, Status), pagination, side panel detail, clear-all with confirmation
- (Console): Per-bucket notification rules editor in bucket settings (lifecycle-style inline forms, Enabled/Disabled toggle, prefix/suffix filters, test webhook button)
- (Console): Sidebar navigation link for Notifications (between Audit Log and Monitoring)
- Docker webhook receiver service (
docker/webhook-receiver/) for integration testing bin/test notificationsmode with webhook receiver compose overlay- 35 unit tests (XML parsing/roundtrip, event matching, filter matching, validation, NotificationStore CRUD)
- ~18 integration tests (configuration CRUD, webhook delivery, event format, filters, batch delete, admin API)
0.16.1 — 2026-04-01¶
Fixed¶
- PostgreSQL backend not activating: when using
--postgreswith other features (e.g.--encryption), themetadata_backendkey from the postgres config fragment landed under the wrong TOML section due to naive fragment concatenation, silently falling back to SQLite - Auto-detect
metadata_backend = "postgres"from presence of[storage.postgres]config section, removing the need for an explicit key in the fragment
0.16.0 — 2026-03-31¶
Added¶
- Configurable preview size limits: 3 new server-side settings (
preview_max_size_mb,preview_max_text_mb,preview_max_video_mb) to control the maximum file size for object previews in the console - (Console): New "Console" section in the Settings page to manage preview thresholds per file type (images/PDF/HTML, text/Markdown, video), with 0 = unlimited
- (Console): Preview logic reads limits from server settings instead of using hardcoded values
- 2 new integration tests for preview settings CRUD and validation
0.15.0 — 2026-03-31¶
Added¶
- Phase 24: PostgreSQL Backend
PgStore: PostgreSQL metadata backend implementing all 8 store traits (MetadataStore, CredentialStore, UserStore, TeamStore, GrantStore, AuditStore, MetricsStore, ServerConfigStore) viasqlx-core/sqlx-postgres- Config switch:
[storage] metadata_backend = "sqlite" | "postgres"with[storage.postgres]section for connection string and pool settings - Docker overlay:
docker-compose.postgres.ymlwith PostgreSQL 17 Alpine,--postgresflag forbin/arca startandbin/test postgres - Migration runner: consolidated initial schema (equivalent to SQLite v1-v13) applied automatically at startup
/admin/info: returnsmetadata_backendfield ("sqlite" or "postgres")- (Console): Database indicator in server info panel showing current backend type
- 20 PostgreSQL-specific integration tests covering buckets, objects, multipart, versioning, tags, lifecycle, copy, range reads
0.14.0 — 2026-03-31¶
Added¶
- Phase 22: S3 API Completeness
ListParts:GET /{bucket}/{key}?uploadId=Xwith paginationGetObjectAttributes:GET /{bucket}/{key}?attributeswith ETag, Checksum, ObjectParts, StorageClass, ObjectSize- Checksum algorithms: store and return client-provided
x-amz-checksum-sha256/crc32/crc32c/crc64nvmeon PutObject, GetObject, HeadObject - Storage classes:
storage_classfield on ObjectRecord, acceptx-amz-storage-classheader - SQLite migration v13:
storage_class,checksum_algorithm,checksum_valueon objects;checksum_value,last_modifiedon parts;checksum_algorithmon multipart_uploads - Resolves TD-002 (storage class) and TD-008 (content-type source)
- Phase 23: Performance and Hardening
- Request size limits:
[server.limits]TOML section, streamingLimitedByteStream, Content-Length fast-reject,EntityTooLargeerror (default 5 GB) - Rate limiting: per-IP and per-credential GCRA via
governorcrate,SlowDown503 withRetry-After(disabled by default) - Metadata cache: in-memory LRU via
mokacrate,CachingMetadataStorefor bucket existence and object HEAD, configurable size/TTL in[server.cache], write-through invalidation - Graceful shutdown: drain mode via
tokio::sync::watch, health endpoint returns 503 during configurable drain window - Request validation middleware: header count limit, null byte rejection, user metadata size limit
- Performance benchmarks: HEAD/DELETE benchmarks with
--jsonoutput and--baselinecomparison
0.13.0 — 2026-03-26¶
Added¶
- Phase 21: Object Lock (WORM Compliance)
- 6 new S3 operations:
PutObjectLockConfiguration,GetObjectLockConfiguration,PutObjectRetention,GetObjectRetention,PutObjectLegalHold,GetObjectLegalHold - Retention modes: GOVERNANCE (bypassable with permission) and COMPLIANCE (absolute protection) with retain-until-date
- Legal hold: per-object ON/OFF flag, independent of retention
- Default retention: bucket-level config applied automatically on PutObject
- Enforcement: blocks hard-deletion of locked versions, delete markers always allowed, auto-enables versioning (prevents suspension)
- SQLite migration v12: adds
retention_mode,retain_until_date,legal_hold_statuscolumns - 7 new S3 policy actions including
s3:BypassGovernanceRetention - Lifecycle worker respects Object Lock
- Console: Object Lock — lock badge in bucket list and breadcrumbs, retention mode/period in bucket settings, versioning suspend disabled when locked
- Phase 20: Lifecycle Rules
- 3 new S3 operations:
PutBucketLifecycleConfiguration,GetBucketLifecycleConfiguration,DeleteBucketLifecycleConfiguration - Rule features: object expiration after N days, noncurrent version expiration, abort incomplete multipart uploads
- Filters: by prefix, tag, or combined (And filter). Rules stored as JSON in
bucket_configtable (no schema migration) - Background worker: periodic evaluation across all buckets, configurable interval (default 1 hour), batch processing (100 objects per rule per cycle), audit logging for all lifecycle actions
- Console: lifecycle rules editor — add/remove rules with prefix filter, expiration days, noncurrent version days, abort upload days
- Admin setting:
lifecycle_evaluation_interval— configurable evaluation interval (60–86400 seconds) viaGET/PUT/DELETE /admin/settings/lifecycle_evaluation_interval
Fixed¶
- Tagging: tag validation on PutObject — invalid
x-amz-taggingheaders now rejected with 400 before writing the blob - Tagging:
x-amz-tagging-countheader — GET and HEAD responses now include tag count when the object has tags - Tagging: multipart upload tags —
CreateMultipartUploadnow capturesx-amz-taggingand applies tags atCompleteMultipartUpload - Versioning: delete marker detection — GET/HEAD on a delete marker now returns 404 with
x-amz-delete-marker: trueandx-amz-version-id - Versioning: CompleteMultipartUpload
x-amz-version-id— response now includes version ID when versioning is enabled - Versioning: UploadPartCopy with versioned source — now uses
?versionId=from copy source header instead of always fetching latest - Versioning: conditional DELETE with delete markers — conditional headers on DELETE now correctly evaluate against latest version including delete markers
- S3 compatibility: +18 Ceph s3-tests passing, 338/829 (40.8%), up from 320/829 (38.6%)
- Encryption: key mismatch returns 403 — wrong master key now returns
AccessDeniedinstead ofInternalError
0.12.0 — 2026-03-24¶
Added¶
- Phase 19: Object Tagging
- 6 new S3 operations:
GetBucketTagging,PutBucketTagging,DeleteBucketTagging,GetObjectTagging,PutObjectTagging,DeleteObjectTagging - Inline tags:
x-amz-taggingheader on PutObject,x-amz-tagging-directiveon CopyObject - Limits: max 10 tags per object/bucket, key max 128 chars, value max 256 chars
- Version-aware: tags tied to specific object versions, cascade deletes on object/bucket removal
- SQLite migration v11: new
object_tagsandbucket_tagstables - Console: tag editor — view, add, and remove object tags in the detail side panel
- Roadmap restructured — Phase 19 split into Object Tagging (19) and Lifecycle Rules (20), subsequent phases renumbered
0.11.0 — 2026-03-24¶
Added¶
- Console: search and filter — real-time debounced search bar on all list views (Buckets, Users, Teams, Grants, Credentials, Bucket Detail), filters files and folders, updates treemap and select-all in sync
- Console: audit operation filter — smart presets (S3 Read, S3 Write, All S3, All Admin, Data Changes) with category chips, drill-down, and individual operation checkboxes
- Console: responsive mobile layout — full support down to 375px, collapsible sidebar, responsive grid, touch-friendly controls, horizontal-scrolling tables, viewport-safe popovers
Fixed¶
- Removed unused Rust imports (
deletein router,MetadataStoreandadmin_settingsin worker) to eliminate compiler warnings
0.10.0 — 2026-03-23¶
Added¶
- Console: object preview — collapsible preview panel with fullscreen modal. Supports images (JPEG, PNG, GIF, WebP, SVG, AVIF), video (MP4, WebM, MOV with native controls), text/code with syntax highlighting, Markdown, HTML in sandboxed iframe, and PDF
- Environment variable overrides —
ARCA_SERVER_BIND,ARCA_SERVER_PORT,ARCA_STORAGE_DATA_DIRoverride config file values
0.9.1 — 2026-03-22¶
Added¶
- Console: inline column header filters — Time (date range popover), Bucket (value list with counts), Key (text search), User (value list with counts), Status (value list with color-coded codes), and Operation (tag-based include/exclude popover) filters are built into the table column headers
- Console: clear audit log — "Clear All" button with confirmation modal requiring explicit
CLEAR AUDIT LOGtext input.DELETE /admin/auditbackend endpoint - Console: date range filter — From/To datetime pickers for the Time column, server-side filtered
- Console: status column filter — filterable by HTTP status code via dropdown with checkboxes and counts
- Console: sticky filters — all audit log filters (column headers, operation tags, page size) persist across navigation via sessionStorage
Changed¶
- Console: audit log layout — filter bar removed, all filters moved into column headers with consistent UX (funnel icon on hover, cyan badge when active, X to clear)
- Console: fixed sidebar — sidebar no longer scrolls with page content
Fixed¶
- Console: multi-file upload — fixed regression where only the first file was uploaded (live FileList invalidated during async iteration)
- Audit log feedback loop — read-only monitoring operations (Health, Metrics, ListAudit, etc.) are no longer logged to prevent audit entries from generating more audit entries
- Audit log missing identity — access key and user ID now correctly extracted from the Authorization header before auth middleware consumes the request
0.9.0 — 2026-03-21¶
Added¶
- Phase 18: Monitoring, Metrics, and Audit
- Prometheus metrics endpoint:
GET /admin/metrics(unauthenticated) with request counters by operation/status, latency histograms (10 buckets), and gauges (active connections, buckets, objects, storage size) - Audit logging: every S3 and admin operation recorded in
audit_logtable with timestamp, operation, identity, status, duration.GET /admin/auditwith filters (bucket, operation, user, time range) and pagination. Tag-based operation filter with include/exclude modes in console - Instance-wide settings:
GET/PUT/DELETE /admin/settings/{key}for runtime-configurable settings (region, retention days). TOML config takes precedence when set (read-only in console) - Configurable region:
regionin[server]TOML section or via Admin API. Per-bucket region viabucket_config. Resolves TD-004 - Metrics history: periodic gauge snapshots stored in
metrics_snapshottable.GET /admin/metrics/historywith time range filter. SVG sparkline charts with labeled axes - Retention management: configurable audit log and metrics retention (days). Background worker purges old records hourly
- Background worker framework: reusable
BackgroundWorker::spawn_periodicfor periodic tasks (metrics snapshots, retention purge) - Console: Settings page — instance-wide settings with lock indicators for TOML-set values, auto-save for editable fields
- Console: Audit Log page — filterable table with tag-based operation filter, pagination with first/last buttons, page size selector, slide-in detail panel, auto-refresh
- Console: Monitoring page — SVG sparkline charts for objects, storage, buckets, connections with time range selector and labeled axes
- Console: fixed sidebar — sidebar no longer scrolls with content, stays pinned to viewport
- Database migration v10:
server_config,audit_log, andmetrics_snapshottables - Integration tests: 27 new tests for Prometheus, audit, metrics history, settings, and region
Fixed¶
- Multi-file upload: fixed regression where uploading multiple files or folders in the console only uploaded the first file (FileList reference invalidated by input reset during async iteration)
0.8.1 — 2026-03-21¶
Fixed¶
- DeleteObjects with VersionId: batch delete (
POST /{bucket}?delete) now supports<VersionId>per object, enabling hard-deletion of specific versions and delete markers. Previously, VersionId was ignored and versioned deletes only created more delete markers, making buckets impossible to empty. - Console: object list now refreshes after deleting a specific version from the version history panel.
- Ceph s3-tests: 50 additional tests now pass (270 → 320), primarily in Versioning and Bucket categories.
Changed¶
bin/s3-tests: report generation no longer skipped when pytest exits non-zero (test failures). The script now correctly captures the exit code and proceeds to generate the HTML report, SVG badge, passlist, and summary.json.
0.8.0 — 2026-03-20¶
Added¶
- Phase 17: Object Versioning
- Bucket versioning config:
PutBucketVersioning/GetBucketVersioningwith three states (Disabled / Enabled / Suspended) - Version IDs: every
PutObjecton a versioned bucket generates a UUID version ID, returned viax-amz-version-idheader - Delete markers:
DeleteObjecton versioned buckets creates a delete marker instead of removing the object;x-amz-delete-marker: trueheader - Version-specific operations:
GetObject?versionId=X,HeadObject?versionId=X,DeleteObject?versionId=Xfor accessing or permanently deleting specific versions - ListObjectVersions: rewritten with real version data, delete markers, and
Ownerelements (replaces TD-003 fake implementation) - CopyObject with source versionId:
x-amz-copy-source: bucket/key?versionId=Xcopies a specific version - Suspended versioning: new writes get null version ID, existing real versions preserved
- Batch DeleteObjects: safe on versioned buckets (creates delete markers, no panic on empty blob_id)
- SQLite migration v9: objects table recreated with
version_id,is_latest,is_delete_markercolumns, partial unique index - Error codes:
NoSuchVersion(404),MethodNotAllowed(405) for GET on delete markers arca recover: sorts sidecars bylast_modifiedto recover newest version;SidecarMetagainsversion_idfieldarca fsck: handles all object versions and skips blob checks for delete markers- Web console: bucket versioning toggle (Enable/Suspend) matching encryption switch style
- Web console: versioning indicators (clock icon) in dashboard, bucket list, and bucket detail breadcrumbs (blue=Enabled, amber=Suspended)
- Web console: version history panel in object detail with download/delete per version, Latest badge, delete marker indicators
- Web console: "Show deleted" toggle in bucket browser header for versioned buckets, showing deleted files and directories with strikethrough + red badge
- Web console: scrollable breadcrumbs for deep directory paths with auto-scroll to deepest level
- Web console: centralized SVG icons (
icons.encryptionShield,icons.versioningClock,icons.versioningSuspended) inapp.js - 13 new unit tests for versioning (put/get/delete versioned, delete markers, suspended mode, stats)
- 20 versioning integration tests (config, PUT, GET, HEAD, DELETE, batch delete, ListVersions, CopyObject, backward compat)
- Resolves: TD-003
Fixed¶
- File re-upload after delete now works (reset file input value after upload)
- Batch
DeleteObjectsno longer panics on versioned buckets (skip blob deletion for delete markers)
0.7.0 — 2026-03-18¶
Added¶
- Phase 16: Access Control and Bucket Policies
- RBAC foundation: users, teams, grants (policy documents) with full CRUD via Admin API
- Policy evaluation engine: AWS IAM-style policy documents with Effect/Action/Resource matching, wildcard support, deny-overrides evaluation
- S3 authorization middleware: non-root users are authorized against effective policies for every S3 operation (action + resource ARN matching)
- Admin authorization: non-root users need
arca:*grants to access admin endpoints (e.g.,AdministratorAccessgrant) - Identity resolution: credentials linked to users, users belong to teams, grants attach to users or teams. Effective policies = direct grants + team-inherited grants
- Built-in grants: AdministratorAccess, S3FullAccess, S3ReadOnlyAccess created during migration
- Owner model: buckets and objects track their creator's username (resolves TD-001)
- Admin API endpoints:
/admin/me,/admin/users,/admin/teams,/admin/grantswith full CRUD, membership management, grant attachments, effective grant queries - CLI
arca usersubcommand:create,list,deletefor offline user management - CLI
arca credential add --user: associate new credentials with specific users - Web console: Users, Teams, Grants management views with dual-list shuttle components for membership and grant assignment
- Web console: inline editing of names and descriptions for users, teams, grants, and credentials (save-on-change)
- Web console: credential activate/deactivate toggle on credential cards
- Admin API:
PUT /admin/credentials/{id}for updating credential active status and description - Admin API:
PUT /admin/users/{id}now accepts optionalusernamefield for renaming - Admin API:
PUT /admin/teams/{id}now accepts optionalnamefield for renaming - Documentation: Access Control guide page with diagrams explaining the identity model, auth flow, and effective grants
- Documentation: database ER diagram (Mermaid) in architecture page showing all 12 tables and relationships
- Documentation: full Admin API reference (37 endpoints), updated CLI reference, console manual with 19 screenshots
- Ceph s3-tests: machine-readable
summary.jsonoutput after test runs - SQLite migration v8: users, teams, grants, team_members, user_grants, team_grants tables; owner fields on buckets/objects; user_id on credentials
- 41 RBAC integration tests (user/team/grant CRUD, attachments, effective grants, E2E access control)
- 90 new unit tests (47 policy evaluator, 37 RBAC store implementations, 6 credential/user/team update)
0.6.0 — 2026-03-16¶
Added¶
- Phase 15: Presigned URLs, Query-String Auth, and SSE-C
- Query-string SigV4 authentication: presigned URLs with
X-Amz-Algorithm,X-Amz-Credential,X-Amz-Date,X-Amz-Expires,X-Amz-SignedHeaders,X-Amz-Signaturequery parameters - Presigned URL generation:
generate_presigned_url()inarca-authfor server-side URL creation - Admin presign endpoint (
POST /admin/presign): generate presigned GET/PUT URLs with configurable expiry, optional endpoint override for correct public URL, percent-encoded paths - SSE-C (Server-Side Encryption with Customer-provided keys): AES-256-GCM encryption where the customer provides the key on each request. Supported for PutObject, GetObject, HeadObject, and CopyObject (including cross-mode: SSE-C to plain, plain to SSE-C, SSE-C to SSE-C with different keys)
- Console: Share button on objects with presigned URL generation, expiry selector (1h/6h/1d/7d), and copy-to-clipboard
- SSE-C CORS headers for cross-origin browser access
- 17 presigned URL integration tests (GET, PUT, HEAD, DELETE, security, admin presign, special chars, endpoint override)
- 17 SSE-C integration tests (put/get roundtrip, error handling, head, copy, range reads, delete, validation, multipart rejection)
- 18 new unit tests (12 auth, 6 storage) for presigned URL parsing/signing and SSE-C encryption
0.5.0 — 2026-03-16¶
Added¶
- Streaming archive endpoint (
POST /admin/archive): download multiple objects as a tar.gz archive streamed on-the-fly, with no temporary files on the server - Console: directory upload via file picker and drag-and-drop, preserving directory structure
- Console: multi-select with checkboxes, select-all, and floating action bar
- Console: batch delete with recursive directory support and confirmation dialog (warnings for multiple items and recursive directory wipes)
- Console: batch download as streaming tar.gz archive
CHANGELOG.mdwith full release historyRELEASING.mdwith release procedure checklist
0.4.1 — 2026-03-16¶
Added¶
- Plugin system: composable features via
--flags(--tls,--encryption,--kms), replacing combinatorial config/compose files - Binary extraction:
bin/build --binary [--arch amd64|arm64]extracts a standalone Linux binary tobuild/ - Console: disk usage shown in dashboard stats
- Console: documentation link in sidebar
--helpflag on allbin/scripts- Auto-create master key in Vault/OpenBAO at startup if missing
Changed¶
- Centralized all builds in
bin/buildwith--dev,--console,--binaryflags - Consolidated version in workspace
Cargo.toml(single source of truth, inherited by all crates) - Use
debian:stable-sliminstead ofbookworm-slimfor dev image
Fixed¶
- Foreground mode (
bin/arca start) not stopping associated services on Ctrl+C - Treemap button not rendering on click in console
0.4.0 — 2026-03-15¶
Added¶
- Phase 14: SSE-KMS with Vault/OpenBAO — fetch master encryption key from Vault/OpenBAO KV v2 at startup,
[encryption.kms]config section,reqwestwithrustls-tlsbackend - Per-bucket encryption with bucket settings console page
- Database schema diagram in architecture documentation (SVG for dark mode)
Changed¶
- Adopted semver
0.x.xpre-release versioning (from1.2.0to0.3.0) - Console uses standard ports (80/443) instead of 3000
- Replaced
RUST_LOGwithARCA_LOGfor log level configuration - Replaced ICO favicon with transparent PNG for Safari/Chrome compatibility
0.3.0 — 2026-03-13¶
Added¶
- Phase 13: Server-Side Encryption (SSE-S3) — AES-256-GCM via
ring, chunk-based streaming (64 KiB), envelope encryption (random DEK per object wrapped by master KEK), ETag computed on plaintext, mixed-mode coexistence,bucket_configtable for per-bucket settings --config (-c)option for custom config files inbin/arca- Combined TLS + encryption config for development
- Version number in web console sidebar
0.2.0 — 2026-03-13¶
Added¶
- Phase 12: Native TLS/SSL —
tokio-rustls+hyper_util,ringcrypto backend, single port for all traffic including health checks, ArcSwap for cert hot-reload on SIGHUP - Console HTTPS support
- Full post-MVP product roadmap (Phases 12–26) with dependency diagram
Fixed¶
- HTTP/2 SigV4 host header (
:authoritypseudo-header handling)
0.1.0 — 2026-03-10¶
Added¶
- Phases 0–11: complete MVP — 15 S3 operations, single-node server
- S3 API: CreateBucket, HeadBucket, ListBuckets, DeleteBucket, PutObject, GetObject, HeadObject, DeleteObject, DeleteObjects, CopyObject, ListObjectsV2, CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload
- Five-crate Cargo workspace: arca-core, arca-auth, arca-proto, arca-storage, arca-server
- Streaming-first architecture: never buffers full objects in memory
- AWS SigV4 authentication with credential CRUD via Admin API
- Disaster recovery: sidecar
.metafiles,arca recoverfor full DB rebuild,arca fsckfor integrity checking - Web console: Alpine.js + Tailwind CSS dashboard, bucket browser, credential management, role-aware UI
- Admin API: health, info, stats, credential CRUD under
/admin/* - S3 compatibility hardening: 270/829 Ceph s3-tests passing (32.6%), MinIO Client support
- Documentation site: MkDocs with Material theme, architecture docs, user guides
- Scratch-based production Docker image (8.6 MB)