Standalone or Spinifex · Quick Start · S3 API Support · Architecture · Configuration · Spinifex Integration · Development · Roadmap
--- # Predastore: Distributed, S3-compatible object storage Predastore is a distributed object-storage system implementing commonly used Amazon S3 APIs. It combines Raft metadata, Reed–Solomon erasure coding, QUIC transport and append-only storage segments. Predastore can run independently and provides the default object-storage backend for Spinifex. ## Standalone or Spinifex? Predastore runs on its own or as the object store of a [Spinifex](https://github.com/mulgadc/spinifex) cluster, and one thing decides which: whether the config carries an `[iam]` table. Without it the gate resolves every access key from the `[[auth]]` entries in its own TOML, dials nothing, and depends on no other service. With it the gate also reads users, groups, roles, policies and session credentials out of Spinifex's NATS JetStream KV buckets, with the config-defined accounts still matched first. Standalone is enough when a fixed set of service accounts and buckets, written into the config and distributed with it, is the whole access model. Reach for Spinifex when credentials have to change without a restart, or when more than one tenant shares the cluster. | | Standalone | Under Spinifex | |---|---|---| | Credentials | `[[auth]]` entries in the TOML | IAM access keys in NATS KV, plus the config accounts | | Authorisation | none — every config account is fully trusted | IAM policies, evaluated per request | | Buckets | `[[bucket]]` entries, plus whatever clients create | created through the API, owned per account | | Multiple tenants | no — every key reaches every bucket | yes, enforced by account ownership | | Users, groups, roles | no | IAM users, groups and roles | | Temporary credentials | no | STS assumed-role sessions | | Rotating a key | edit the TOML, restart the host | `CreateAccessKey`, no restart | | Keys and certificates | `predastore-keygen`, distributed by you | issued on `spx admin init`, distributed on join | | Cluster topology | you write the TOML | generated from the joined node set | | External dependencies | none | NATS, and the Spinifex daemon that populates it | The second row is the one that decides most deployments. A config-defined account in `[[auth]]` bypasses both policy evaluation and the bucket-ownership check, so every key in the file reaches every bucket in the cluster no matter which account owns it. Standalone predastore cannot hand out a credential that is less than total, and that — rather than any missing S3 operation — is what a multi-tenant deployment needs Spinifex for. ## Quick Start ### Build ```bash make build ``` Build through the Makefile rather than calling `go build ./cmd/s3d` yourself. The binary links a FIPS boot check that panics on startup unless it was compiled with `GOFIPS140=v1.0.0`, which the Makefile exports and a bare `go build` does not. ### Run a Development Cluster `scripts/start.sh` launches a cluster from one of the profiles in `config/`, generating the TLS keypair and master key under `$PREDA_DIR` (default `/tmp/predastore`) on first run: ```bash ./scripts/start.sh -w 1host # one process, S3 on https://127.0.0.1:8443 ``` Four profiles ship with the repo. They differ only in how many hosts the nodes are spread over, which is what decides the transport between them: | Profile | Hosts | RS | Inter-node transport | Needs `sudo` | |---------|-------|----|----------------------|--------------| | `1host` | 1 | (1, 0) | in-process pipe only | no | | `3host` | 3 on `10.11.12.{1,2,3}` | (2, 1) | QUIC between hosts, pipe within | yes | | `4host` | 4 on `10.11.12.{1..4}` | (2, 1) | QUIC between hosts, pipe within | yes | | `5host` | 5 on `10.11.12.{1..5}` | (3, 2) | QUIC between hosts, pipe within | yes | Every host in every profile sets `admin_port = 9099`, so `/healthz` and `/readyz` answer on the host's own address. That is plain HTTP with no authentication, which is safe on a loopback dev cluster and is why the flag default is `0` rather than a port. The multi-host profiles put every host on one machine behind loopback aliases, so they need `sudo` to add those aliases and to install the generated certificate as a trust anchor. Each host answers S3 on its own address. Stop, reset or benchmark the cluster with: ```bash ./scripts/stop.sh # every cluster under $PREDA_DIR ./scripts/stop.sh 3host # only that one, leaving any others running ./scripts/clean.sh ./scripts/bench.sh 3host ./scripts/bench.sh disk ``` ### Run a Host `./bin/s3d` runs one host of a cluster: the nodes the config pins to it, the S3 gate among them. `-host` names the `[[host]]` to run, and everything else about the process — which nodes, which addresses, which ports — follows from that entry: ```bash ./bin/s3d \ -config cluster.toml \ -host 1 \ -data-dir /var/lib/predastore \ -tls-cert /etc/predastore/server.pem \ -tls-key /etc/predastore/server.key \ -encryption-key /etc/predastore/master.key ``` | Flag | Description | |------|-------------| | `-config` | Path to the configuration file (required) | | `-host` | ID of the `[[host]]` this process runs (required) | | `-data-dir` | On-disk root for this host's nodes; overrides `data_dir` | | `-encryption-key` | Path to the 32-byte AES-256 key protecting data at rest; overrides `encryption_key` | | `-tls-cert`, `-tls-key` | This host's TLS identity; overrides `tls_cert` / `tls_key` | | `-bind-addr` | Listen address for this host's cluster traffic, without a port; overrides `bind_addr` | | `-gate-bind-addr` | Listen address for the S3 API, without a port; overrides the gate node's `bind_addr` | | `-log-level` | `debug`, `info`, `warn` or `error` | Each of these host-local settings may come from either the file or a flag, so the same configuration file can be deployed unchanged to every machine and the paths supplied per process. The S3 port is not a flag: it is the gate node's `port`. A cluster whose `[[host.node]]` entries all sit under one `[[host]]` runs entirely in one process over the in-process pipe, with no inter-node socket and no certificate beyond the one the gate serves. That is a property of the config, not a launch mode. The encryption key file must be exactly 32 raw bytes (no base64, no header) with mode `0600`. Generate one with `( umask 0177 && openssl rand -out master.key 32 )`. The same key must be supplied to every host in a cluster; rotating it is not currently supported (see Roadmap → envelope encryption). ### Run as a systemd Service `make install` lays down the binary, a unit, and the drop-ins that create the service's user and its directories. It installs files and performs nothing else, so it is usable from a package build and leaves the sequencing to you: ```bash make build sudo make install # binary, unit, sysusers, tmpfiles, sysctl sudo systemd-sysusers && sudo systemd-tmpfiles --create sudo sysctl --system # required for multi-host sudo cp /etc/predastore/predastore.toml.example /etc/predastore/predastore.toml sudo $EDITOR /etc/predastore/predastore.toml # replace every CHANGEME sudo predastore-keygen /etc/predastore # the at-rest key and a TLS identity sudo systemctl daemon-reload && sudo systemctl enable --now predastore ``` `DESTDIR` and `PREFIX` are honoured, so a package build can stage the same target into a buildroot. An existing `./bin/s3d` is installed as it stands rather than rebuilt, which is what lets the install run under `sudo` without `go` on root's `PATH`, so run `make build` first to pick up any source change. The service runs as an unprivileged `predastore` user under a strict sandbox: `/etc` read-only, `/var/lib/predastore` the only writable path, no capabilities at all, and a syscall filter. Its host-local settings — which `[[host]]` it runs, where its data and keys live — come from `/etc/predastore/predastore.env`, which installs as `predastore.env.example` so an upgrade cannot overwrite an edited one. The shipped example config names those paths itself, which is what a single machine wants; a fleet distributing one identical TOML overrides them per host in the env file instead. The service logs at `warn`, rather than the `info` a hand-run `s3d` defaults to, on the grounds that a long-running service wants a quiet journal. `predastore-keygen` is idempotent, and it refuses to mint a TLS identity for a config naming more than one host. Peers verify each other against the system trust store, so hosts that each generated their own keypair never elect a leader rather than failing outright. Generate one keypair, distribute it to every host, and install its CA — see [Standalone TLS Trust](#standalone-tls-trust). ### Run in Docker The image needs no arguments and no configuration file. On first start it generates a TLS identity and an at-rest key into its volume and serves a single-host RS(1, 0) cluster: ```bash make docker-build docker run -d -p 8443:8443 -p 9099:9099 -v preda-data:/var/lib/predastore predastore:dev ``` Two compose topologies ship under `deploy/`. The cluster profile is four hosts at RS(2, 1), each with a gate, a meta replica and a blob node; every gate answers for the whole cluster, so `8443`–`8446` are four ways into the same thing: ```bash make compose-up # four hosts make compose-up COMPOSE_FILE=deploy/compose.single.yml make compose-down ``` `make docker-smoke` then checks the image itself — non-root, no private key in any layer — and drives aws-cli, rclone, restic and a `registry:3` on top of the running cluster. It asserts response content rather than exit codes, and gates on `scripts/smoke-baseline.txt` so it fails on a regression rather than on the checks predastore does not pass yet. Re-record that file with `make docker-smoke-baseline` in the same change as the fix. #### Environment `s3d` itself reads no environment variables, so the entrypoint renders a configuration file from these and passes it as `-config`. Every one is optional. | Variable | Default | Description | |---|---|---| | `PREDA_PEERS` | `127.0.0.1` | Comma-separated host addresses in host-id order. More than one entry makes it a cluster | | `PREDA_HOST_ID` | `1` | Which `[[host]]` this process runs | | `PREDA_HOST_ID_FROM_ORDINAL` | `0` | Set to `1` to derive the host id from a StatefulSet pod ordinal | | `PREDA_BIND_ADDR` | `0.0.0.0` | Cluster-plane listen address | | `PREDA_RS_DATA`, `PREDA_RS_PARITY` | `1`/`0` alone, `2`/`1` otherwise | Erasure-coding width | | `PREDA_BLOB_NODES` | `1` | Blob nodes per host | | `PREDA_REGION` | `ap-southeast-2` | Region | | `PREDA_S3_PORT`, `PREDA_META_PORT`, `PREDA_BLOB_PORT` | `8443`, `6660`, `9991` | Node ports | | `PREDA_ADMIN_PORT` | `9099` | `/healthz` and `/readyz`; `0` runs neither | | `PREDA_ACCESS_KEY_ID`, `PREDA_SECRET_ACCESS_KEY`, `PREDA_ACCOUNT_ID` | the AWS example pair | Config-defined service account | | `PREDA_DATA` | `/var/lib/predastore` | Data root | | `PREDA_TLS_CERT`, `PREDA_TLS_KEY` | under `$PREDA_DATA/tls/` | TLS identity, generated if absent | | `PREDA_KEY` | `$PREDA_DATA/master.key` | AES-256 key at rest, generated if absent | | `PREDA_CONFIG` | — | Use this profile verbatim and render nothing | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` | Two of these have constraints the container cannot paper over: - A cluster is refused without explicit credentials. The AWS example pair is a single-host evaluation default, not something to run four hosts on. - A cluster needs **one TLS keypair shared by every host**, because peers verify each other against the system trust store. Hosts that each generated their own identity do not fail loudly, they simply never elect a leader, so the entrypoint refuses to generate one when `PREDA_PEERS` names more than one address. `deploy/docker/gen-certs.sh` mints a shared local-development keypair; `make compose-up` runs it for you. ## S3 API Support | Area | Operations | | --- | --- | | Buckets | `CreateBucket`, `DeleteBucket`, `ListBuckets`, `HeadBucket` | | Objects | `PutObject`, `GetObject`, `DeleteObject`, `HeadObject`, `ListObjects`, `ListObjectsV2` | | Multipart | `CreateMultipartUpload`, `UploadPart`, `CompleteMultipartUpload`, `AbortMultipartUpload` | | Authentication | AWS Signature Version 4, presigned URLs | Multi-object delete (`DeleteObjects`) takes up to 1000 keys per request and reports the outcome of each one. How far that support goes is measured rather than claimed. [docs/S3-COMPATIBILITY.md](docs/S3-COMPATIBILITY.md) holds the result of running `ceph/s3-tests` — the suite Ceph RGW, MinIO and Garage are all validated against — with the per-case manifest in `scripts/s3-tests-baseline.txt`. Read it before assuming an operation behaves the way S3 documents it. ### AWS CLI Examples Against the `1host` dev profile. Its certificate is self-signed, so either install it as a trust anchor or pass `--no-verify-ssl`: ```bash export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY export AWS_DEFAULT_REGION=ap-southeast-2 # Create a bucket aws --no-verify-ssl --endpoint-url https://127.0.0.1:8443/ s3 mb s3://my-bucket # Upload a file aws --no-verify-ssl --endpoint-url https://127.0.0.1:8443/ s3 cp ./file.txt s3://my-bucket/ # List bucket contents aws --no-verify-ssl --endpoint-url https://127.0.0.1:8443/ s3 ls s3://my-bucket/ # Download a file aws --no-verify-ssl --endpoint-url https://127.0.0.1:8443/ s3 cp s3://my-bucket/file.txt ./downloaded.txt ``` ## Architecture