Encrypting before you upload is easy. The problem is that it takes the catalog with it — encrypt the object keys and you have a bucket of opaque blobs plus a local index you now have to keep safe separately, which is the thing you were trying to avoid.
| searchable catalog | host cannot read it | |
|---|---|---|
| Plain S3 | yes | no — the provider reads everything |
| Encrypt-then-upload | no — you encrypted the names too | yes |
| blindrange-s3 | yes | yes |
A prefix listing here is a dyadic index lookup over encrypted keys,
not a scan of anything readable. So ls logs/2026/08/ is a native
operation and no node learns what it matched.
[blindrange]
type = s3
provider = Other
endpoint = http://127.0.0.1:8720
access_key_id = blindrange
secret_access_key = <yours>
force_path_style = true
# then, as usual
rclone copy ~/documents blindrange:archive/documentss3 = boto3.client(
"s3", endpoint_url="http://127.0.0.1:8720",
aws_access_key_id="blindrange", aws_secret_access_key="...",
config=Config(s3={"addressing_style": "path"}))
s3.put_object(Bucket="archive", Key="logs/2026/08/app.log", Body=data)
s3.list_objects_v2(Bucket="archive", Prefix="logs/2026/")aws configure set default.s3.addressing_style path aws --endpoint-url http://127.0.0.1:8720 s3 cp file.txt s3://archive/ aws --endpoint-url http://127.0.0.1:8720 s3 ls s3://archive/logs/
python3 examples/s3gateway/gateway.py \
--state ~/.blindrange/s3.brdb \
--bootstrap seed.blindrange.dev:7501 \
--secret blindrange-public \
--access-key blindrange --secret-key <choose one>
# → http://127.0.0.1:8720Where this process runs is the entire security model. The master key lives in it. Objects are encrypted there, before anything leaves the machine, and the nodes holding them see pseudorandom keys and AEAD blobs.
This is the same line Storj draws between its hosted gateway, where keys sit server-side, and its self-hosted one that keeps encryption end-to-end. Hosting it for you would be more convenient and would remove the only reason to choose it over S3, so we don't offer that half.
TOP-SECRET-MARKER to confidential/salaries.csv,
then scan every key and value on all three nodes' disks: the payload is
absent, the filename is absent, and the path is absent — while the object
still reads back byte-identical. That check runs in the test suite as
test_09_nodes_hold_nothing_readable, not once by hand.Two pieces, deliberately split:
ListObjectsV2 with a prefix a
native lookup rather than a scanThat second point matters for the bill: object bodies cost storage and replication but almost no index amplification, so an archive of few large objects sits at the cheap end of the calculator — near object-storage pricing rather than database pricing.
Verified against boto3 — an independent SigV4 implementation, which is the only way to know the signer is right rather than merely self-consistent.
| PutObject / GetObject / HeadObject / DeleteObject | yes |
| ListObjectsV2 — prefix, delimiter, continuation | yes |
| ListBuckets, CreateBucket, DeleteObjects | yes |
| Multipart upload | yes — 5 MB via boto3 upload_file, byte-identical |
| SigV4 verification | yes — a wrong secret is refused |
| aws-chunked streaming bodies | decoded |
Using a real client paid for itself immediately: it found two
bugs that would otherwise have shipped. The canonical query string wasn't
RFC 3986-encoded, so a prefix like logs/2026/ failed to sign
while an unfiltered listing kept working — the bug hid behind the
case anyone tries first. And HEAD emitted two
Content-Length headers, which makes a strict client abort
rather than degrade.
Range: header is ignored and the
whole object returned, which will disappoint anything that seeks inside
archives); no versioning, ACLs, bucket policies, lifecycle rules, tagging
or presigned URLs; buckets are implicit — CreateBucket
succeeds without creating anything and a bucket exists once it holds an
object; multipart parts are held in memory until completion, so a very
large multipart upload is bounded by RAM; and streaming payload
signatures are not verified, which is why binding to a non-loopback
interface with --no-auth is refused outright.
The backup demo uses plain boto3 and imports nothing from blindrange — it would run unchanged against AWS. Backing up the package source to a three-node network:
snapshot 20260815T180000Z: 14 files, 259,724 bytes in 0.3s find work --name '*.py' → 13 of 14 objects restore + verify → 14 identical, 0 differ diff -r original restored → IDENTICAL to the original tree what the machine storing it sees: B:0c31fd78ff7a2432bd4eec2a0e68d5dd -> FkVzGNFZ1d9mPoXmYJUeluoF B:5619f394bdb1bd32fb6e1e5d587bec10 -> 46jrSv2aUjqM8ovsEcQFCdvT 2,059 keys, none readable, none named after your files