Skip to content

Repository files navigation

aks3 (working name)

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.

Origin and license

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

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).

Quickstart

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.

Configuration

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"

Talking to it

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.

Addressing

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.

Known limitations

  • 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 PutObject can carry. No CopyObject, no batch DeleteObjects, 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 makes photo.jpg and Photo.JPG one 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 KeyTooLongError and a 400 from every operation on it. Nesting the key with / separators keeps each component under the limit.

Run with Docker

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.

Stopping it

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 = 25
docker 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: 30s

Kubernetes 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.

What is in the image

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.

About

Artifact Keeper S3 bucket

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages