A single-binary S3-compatible object store written in Rust, with a storage engine derived from MinIO's design.
Next to the other Rust object stores, aks3 is AGPL where RustFS is Apache-2.0,
and strongly consistent where Garage is eventually consistent and built for
resilience across unreliable links. What it is aiming at is behavioral fidelity
to MinIO backed by compliance results anyone can check. Today that means 18
tests from the ceph/s3-tests suite, listed in tests/compliance/allowlist.txt
and enforced on every change; the list is small because the suite's teardown
needs operations Phase 0 does not have yet, and growing it is the roadmap.
Alongside it, tests/compliance/boto3-tests/ drives the same server with a
current boto3 and gates on that too. It covers the ground s3-tests does not:
the integrity checksums the AWS SDKs have sent by default since January 2025,
in both the header and the aws-chunked trailer form, along with presigned URLs
and the response shapes an SDK parses rather than a status code. aks3 accepts
those checksums today but does not verify or store them, and each place that
diverges from AWS is named in a test rather than left to be found in
production.
aks3 is licensed under the GNU Affero General Public License v3.0 only. The full
text is in LICENSE.
The storage engine's design is derived from MinIO: the on-disk layout, the write
discipline, and the locking model. See NOTICE for the attribution, the pinned
reference commit, and the trademark statement.
Status: pre-alpha, Phase 0.
Phase 0 is a single node serving one data directory. Nine S3 operations are
implemented: CreateBucket, HeadBucket, DeleteBucket, ListBuckets,
PutObject, GetObject (including byte ranges), HeadObject, DeleteObject,
and ListObjectsV2 with prefixes, delimiters, and pagination. Requests are
authenticated with SigV4 against a single root credential, and may address a
bucket path-style or, once a domain is configured, virtual-hosted style (see
Addressing).
Build the binary:
cargo build --release
rust-toolchain.toml pins the compiler to 1.98, so rustup picks it up without
being asked. The oldest compiler the workspace builds on is 1.94.1, recorded as
rust-version in the root Cargo.toml.
Write aks3.toml:
listen = "127.0.0.1:9000"
data_dir = "./data"
root_access_key = "admin"
root_secret_key = "secretpassword"Run it:
./target/release/aks3 --config aks3.toml
The data directory is created if it is not there, and the server logs the address it bound before it accepts anything.
listen defaults to 127.0.0.1:9000, data_dir to ./data, and
shutdown_grace_seconds to 8 (see Stopping it). The root
credentials have no default and have to be set, so the shortest config that
works is those two lines on their own.
virtual_host_domains is empty by default, which leaves every request read as
path style; see Addressing.
Six environment variables override the file, which is what lets an image ship a config and still take its credentials at run time:
| Variable | Sets |
|---|---|
AKS3_LISTEN |
listen |
AKS3_DATA_DIR |
data_dir |
AKS3_ROOT_USER |
root_access_key |
AKS3_ROOT_PASSWORD |
root_secret_key |
AKS3_SHUTDOWN_GRACE |
shutdown_grace_seconds |
AKS3_VIRTUAL_HOST_DOMAINS |
virtual_host_domains (comma-separated) |
With the credentials in the environment there is no need for a file at all:
AKS3_ROOT_USER=admin AKS3_ROOT_PASSWORD=secretpassword ./target/release/aks3
For TLS, add a [tls] table naming a PEM certificate chain and its private key.
Without one the server speaks plain HTTP, which is the Phase 0 default.
[tls]
cert_pem = "/etc/aks3/cert.pem"
key_pem = "/etc/aks3/key.pem"export AWS_ACCESS_KEY_ID=admin
export AWS_SECRET_ACCESS_KEY=secretpassword
export AWS_REGION=us-east-1
aws --endpoint-url http://127.0.0.1:9000 s3 mb s3://demo
aws --endpoint-url http://127.0.0.1:9000 s3 cp README.md s3://demo/readme.md
aws --endpoint-url http://127.0.0.1:9000 s3 ls s3://demo/
aws --endpoint-url http://127.0.0.1:9000 s3 rm s3://demo/readme.md
aws --endpoint-url http://127.0.0.1:9000 s3 rb s3://demo
The same operations driven from the AWS SDK for Rust are what
crates/server/tests/smoke.rs runs on every cargo test.
A client can name the bucket in the path (/demo/readme.md, path style) or in
the hostname (demo.s3.example.com/readme.md, virtual-hosted style). Path
style needs no configuration and is what the block above uses: the AWS CLI
picks it whenever --endpoint-url names a custom endpoint.
Virtual-hosted style has to be turned on, by naming the domains that are the store's own:
virtual_host_domains = ["s3.example.com"]or AKS3_VIRTUAL_HOST_DOMAINS=s3.example.com in the environment, several
separated by commas. A domain is a hostname with no port, since the port a
client connects to is not part of the name it sends; a domain that carries one
is refused at startup rather than left to match nothing. Overlapping domains
(example.com alongside s3.example.com) are refused for the same reason: a
host under both would name two different buckets.
With s3.example.com configured, demo.s3.example.com names bucket demo,
my.data.s3.example.com names my.data, and s3.example.com itself names no
bucket and is read as path style. A host that is not under a configured
domain is also read as path style, never guessed at as a bucket, so turning
this on does not break clients that reach the store by service name, tailnet
name or IP. Point DNS (a wildcard record, or /etc/hosts for a trial) at the
store for the names you expect, and give TLS a certificate that covers
*.s3.example.com, because a TLS wildcard matches one label only.
This is what lets clients that offer no path-style option work at all — the
AWS SDK for Java v2 and things built on it, Unity Catalog included.
crates/server/tests/vhost.rs drives it end to end with signatures the AWS SDK
for Rust produced, including the case that keeps path style working while a
domain is set.
- One node and one data directory. No erasure coding, no replication, no distributed mode.
- No multipart upload, so an object is limited to what one
PutObjectcan carry. NoCopyObject, no batchDeleteObjects, and no versioning API. - One root credential. No additional users, groups, or policies.
- Object keys become paths under
data_dir, and letter case is kept as the client sent it. On a case-insensitive filesystem, which is the macOS default for APFS volumes, that makesphoto.jpgandPhoto.JPGone object where S3 has two. Use a case-sensitive volume for anything that matters. Linux filesystems are already case-sensitive. - Because keys become paths, a key whose components are longer than the
filesystem's name limit (255 bytes on APFS and ext4) cannot be stored. S3
allows any key up to 1024 bytes, so such a key is legal and aks3 rejects it,
with
KeyTooLongErrorand a 400 from every operation on it. Nesting the key with/separators keeps each component under the limit.
Images are published to ghcr.io/artifact-keeper/aks3 for linux/amd64 and
linux/arm64. latest follows the default branch, a release tag v0.2.0
publishes 0.2.0, and every build also gets an immutable sha-<short> tag.
docker run -d --name aks3 \
-e AKS3_ROOT_USER=admin \
-e AKS3_ROOT_PASSWORD=secretpassword \
-p 9000:9000 \
-v aks3-data:/data \
ghcr.io/artifact-keeper/aks3:latest
The image sets AKS3_LISTEN=0.0.0.0:9000 and AKS3_DATA_DIR=/data, so the
credentials are the only thing it needs from you. There is no default for them
and the container exits non-zero without both, naming the one it is missing.
It runs as uid 1001 in group 0, and /data is group-writable, so a runtime
that substitutes a uid of its own works as long as the uid is in group 0.
The same options in compose:
services:
aks3:
image: ghcr.io/artifact-keeper/aks3:latest
ports:
- "9000:9000"
environment:
AKS3_ROOT_USER: admin
AKS3_ROOT_PASSWORD: secretpassword
volumes:
- aks3-data:/data
volumes:
aks3-data:Point the AWS CLI at it exactly as in Talking to it, with
--endpoint-url http://127.0.0.1:9000.
docker stop asks with SIGTERM, as do a Kubernetes pod deletion and a systemd
unit stop. aks3 treats it the same way it treats Ctrl-C: it stops accepting
new connections and gives the ones already running a window to finish in,
eight seconds by default, before exiting 0, so a GetObject part way through
its body is not cut off. A stop with nothing in flight takes a fraction of a
second.
Connections still open when the window is up are dropped, and the server says
so in its log before it exits. Raising a supervisor's stop timeout does not
lengthen the window; shutdown_grace_seconds does, up to a limit of 600:
shutdown_grace_seconds = 25docker run -e AKS3_SHUTDOWN_GRACE=25 ...
Once the drain has started, a second Ctrl-C or SIGTERM does not cut it short. The same handler catches it and the window runs to its end, so the way to stop early is the SIGKILL at the end of the supervisor's own timeout. That is worth knowing before setting a window of minutes rather than seconds.
Zero is allowed and means what it says: the drain runs, but nothing still open gets any time, so a stop never waits. That suits a deployment where clients retry and a fast rollout matters more than the request in flight. A value above 600 is refused at startup rather than accepted, on the grounds that it is more likely a typo than a plan, and a drain that outlasts every supervisor's patience would turn every stop into a SIGKILL.
Whatever the window is, the supervisor's timeout has to exceed it. The thing that
sent the SIGTERM is counting down to a SIGKILL of its own, and if that lands
first the drain is cut off part way through and the container reports exit code
137.
docker stop allows ten seconds by default, which is enough for the eight second
default here, but it is worth setting rather than inheriting:
docker stop -t 30 aks3
services:
aks3:
stop_grace_period: 30sKubernetes allows thirty seconds by default (terminationGracePeriodSeconds),
and systemd ninety (TimeoutStopSec), so neither needs changing for the default
window. Raise them alongside shutdown_grace_seconds if you raise it, and lower
shutdown_grace_seconds if a supervisor allows less than eight seconds.
The base is registry.access.redhat.com/ubi9/ubi-micro, which carries no
package manager: no dnf, no microdnf, no rpm. Nothing can be installed
into a running container, and there is no curl, no ps, and no network
tooling in there to reach for.
There is a shell. bash and coreutils come with the base, so
docker exec -it aks3 bash works for looking at what is under /data.
Anything beyond that wants tools the image does not have, so use
docker debug aks3, or a container that shares its namespaces:
docker run --rm -it --pid container:aks3 --network container:aks3 \
registry.access.redhat.com/ubi9/ubi bash
Every image is scanned with Trivy before it is pushed, and a CRITICAL or HIGH
finding that upstream has already fixed stops the publish. Findings with no fix
released do not, because a package ubi-micro has not yet had a fix for is Red
Hat's to release and blocking on it would only stop us shipping anything else.
Those are still recorded: the full results, all severities, go to this
repository's code scanning alerts, and a weekly job rescans the published
latest so a vulnerability disclosed after the build turns into a tracking
issue rather than going unnoticed.