hybrids3: S3-Kompatibler Objektspeicher, der Keinen Doktortitel Verlangt

Setz nginx vor MinIO auf ein Pfadpräfix und versuch, presignte URLs zu benutzen. Nur zu, ich warte.
Das Problem ist AWS Signature V4. Die Signatur deckt den Anfragepfad ab. nginx unter /storage/ schneidet dieses Präfix ab, bevor es weiterleitet, also signiert boto3 /storage/bucket/key, der Server sieht /bucket/key, der HMAC passt nicht, du kassierst einen 403. Das ist kein Konfigurationsfehler, so funktioniert nginx’ Pfadpräfix-Proxying und so funktioniert die SigV4-Prüfung. Sie sind strukturell unvereinbar, es sei denn, du wirst schlau mit deiner Proxy-Config oder wechselst zu einem Server, der das anders handhabt.
docker-hybrids3 handhabt es anders. Es hat eine Config-Option path_prefix, du setzt sie auf /storage und alle Routen wandern nativ unter dieses Präfix. Kein Pfadabschneiden am Proxy. boto3 signiert /storage/bucket/key, nginx leitet /storage/bucket/key weiter, der Server sieht /storage/bucket/key. Die Signaturen passen. Das ist auch der Grund, warum ich alles hinter einem einzigen nginx-Gateway fahre (aigate macht das), ohne mich mit der Proxy-Config zu prügeln.
Das andere, was mich zum Bauen gebracht hat: wenn ein KI-Agent eine Datei schreiben und dir eine URL zurückgeben muss, willst du dafür keinen verteilten Objektspeicher hochziehen. Du willst etwas, das in zwei Minuten startet und dir aus dem Weg geht. HybridS3 hat einen MCP-Server eingebaut, der Agent ruft upload_object auf, bekommt eine URL zurück, fertig.

Was Es Ist

SQLite für Metadaten, flache Dateien auf der Platte. boto3 funktioniert damit. Nacktes HTTP mit curl funktioniert. Auth per Bearer-Token, keine SigV4-Zeremonie für einfache HTTP-Anfragen. Drei Schnittstellen, ein Container, keine Web-Konsole, kein IAM, kein verteilter Modus.
Buckets werden in der Config definiert, nicht per API erzeugt. Du weißt immer genau, was existiert. Du willst einen Bucket? Trag ihn ins YAML ein, starte neu. Du willst TTL-Ablauf? Setz ttl: 24h auf den Bucket und die Objekte löschen sich nach ihrem letzten Schreibvorgang selbst. Keine Lifecycle-Policies, keine Cron-Jobs.

Es Starten

docker run -d --name hybrids3 
    -p 8080:8080 
    -v ./config.yaml:/config/config.yaml:ro 
    -v hybrids3-data:/data 
    psyb0t/hybrids3

Config unter /config/config.yaml, Daten unter /data, Port 8080. Läuft als UID 1000.

Config

master_key: "change-me-to-something-secret"
master_public_key: "master"
cleanup_interval: 1m
# path_prefix: /storage
buckets:
  uploads:
    public: true
    key: "uploads-secret"
    public_key: "uploads-id"
    ttl: 24h
    max_file_size: 50MB
  permanent:
    public: false
    key: "perm-secret"
    public_key: "permanent-id"
    ttl: 0
    max_file_size: 100MB

Jeder Bucket hat einen privaten key (wird nie übertragen, dient zum Prüfen der HMACs) und einen public_key (die S3-Access-Key-ID, unbedenklich in URLs). Genau diese Trennung lässt presignte URLs funktionieren: der öffentliche Schlüssel darf in der URL auftauchen, der private nie.
public: true heißt, dass GET, HEAD und LIST keine Auth verlangen. PUT und DELETE brauchen weiterhin einen Schlüssel. public: false heißt, dass alles Auth verlangt.

HTTP-API

Nacktes Bearer-Token im Authorization-Header:

# upload
curl -X PUT https://ciprian.51k.eu80/uploads/file.txt 
  -H "Authorization: Bearer uploads-secret" 
  -d "hello"
# read from public bucket — no auth
curl https://ciprian.51k.eu80/uploads/file.txt
# read from private bucket
curl https://ciprian.51k.eu80/permanent/doc.pdf 
  -H "Authorization: Bearer perm-secret"
# list objects with prefix filter
curl "https://ciprian.51k.eu80/uploads?prefix=images/&max-keys=50" 
  -H "Authorization: Bearer uploads-secret"

Anfragen mit einem AWS-Sig-V4-Authorization-Header bekommen S3-kompatible XML-Antworten. Alles andere bekommt JSON.

boto3

import boto3
from botocore.config import Config
s3 = boto3.client(
    "s3",
    endpoint_url="https://ciprian.51k.eu80",
    aws_access_key_id="uploads-id",         # public_key from config
    aws_secret_access_key="uploads-secret",  # key from config
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)
s3.put_object(Bucket="uploads", Key="file.txt", Body=b"hello")
s3.get_object(Bucket="uploads", Key="file.txt")
s3.list_objects_v2(Bucket="uploads", Prefix="images/")

Hinter Nginx

Der Teil, auf den es wirklich ankommt. Setz path_prefix: /storage in der Config, dann:

location /storage {
    proxy_pass http://hybrids3:8080;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Zwei Dinge musst du richtig machen: kein abschließender Slash bei proxy_pass (ein abschließender Slash sagt nginx, das Location-Präfix abzuschneiden, und das zerlegt SigV4), und $http_host statt $host ($host schneidet den Port ab, was auf nicht standardmäßigen Ports zu Signaturabweichungen führt).
Mit gesetztem path_prefix zeigt boto3 auf http://yourdomain/storage und alles funktioniert.

MCP

Ein MCP-Server läuft unter /mcp/. Sieben Tools: upload_object, download_object, delete_object, list_objects, list_buckets, object_info, presign_url. Häng irgendeinen MCP-kompatiblen Client dran:

{
  "mcpServers": {
    "hybrids3": {
      "type": "streamable-http",
      "url": "https://ciprian.51k.eu80/mcp/"
    }
  }
}

Auth auf Endpoint-Ebene (Authorization-Header oder Query-Parameter ?auth=) oder pro Tool über den Parameter auth_key. Nimm den Master-Key für vollen Zugriff, einen Bucket-Key, um die Verbindung auf einen Bucket zu beschränken.

Presignte URLs

Für private Buckets erzeugt das eine echte presignte AWS-Sig-V4-URL, signiert mit dem privaten Schlüssel des Buckets:

curl -X POST "https://ciprian.51k.eu80/presign/permanent/doc.pdf?expires=3600" 
  -H "Authorization: Bearer perm-secret"

Für öffentliche Buckets gibt es die nackte URL zurück, eine Signatur ist nicht nötig, da GET ohnehin keine Auth verlangt. Ablaufbereich von einer Sekunde bis sieben Tage, Standard 3600. Abgelaufene oder manipulierte URLs liefern 403.
Presigntes PUT. Seit v0.2.0 reicht ?method=PUT jemandem eine URL, mit der er genau einen Schlüssel hochladen kann, ohne je deinen Bucket-Key zu sehen. Öffentliche Buckets sind hier keine Abkürzung: anonyme Lesezugriffe sind erlaubt, anonyme Schreibzugriffe nie, ein presigntes PUT ist also immer signiert, selbst auf einem öffentlichen Bucket.

# generate an upload URL
curl -X POST "https://ciprian.51k.eu80/presign/uploads/inbox/report.pdf?method=PUT&expires=600" 
  -H "Authorization: Bearer uploads-secret"
# → {"url": "...X-Amz-Signature=...", "method": "PUT", "expires": 600}
# upload with the URL alone — no Authorization header
curl -X PUT "<url>" --data-binary @report.pdf

Eine presignte URL ist an ihr Verb gebunden. Eine GET-URL kann nicht zum PUT benutzt werden und umgekehrt, die Methode ist Teil der kanonischen Anfrage, die die Signatur abdeckt. Und genau das ist der Sinn: du kannst einem Dritten einen Briefkasten für genau einen Schlüssel hinhalten, für genau zehn Minuten, und er kann trotzdem nichts anderes lesen und nirgendwo sonst hochladen.
Das MCP-Tool presign_url nimmt denselben method-Parameter, ein Agent kann also auch Upload-URLs prägen.

Der Build ist dichtgemacht

Die Lieferkette wurde per Hash auf uv verriegelt, mit einer Altersschranke zu festem Datum, eine gestern veröffentlichte Abhängigkeit kann sich also nicht in den heutigen Build schleichen. Multi-Stage-Produktions-Dockerfile, Basis-Image und uv per Digest festgenagelt statt per Tag verfolgt.
Das Makefile ist der einzige Einstiegspunkt, und alles Entwicklungsbezogene läuft in einem abgeschotteten Dev-Container. Auf deinem Host wird nichts installiert. 165 Tests, 17 Unit-Tests, 148 Integrationstests.
Das Repo liefert außerdem einen Agenten-Skill und ein OpenClaw-Plugin unter .agents/, von der CI bei Tag-Pushes auf ClawHub veröffentlicht.

Grenzen

Kein Multipart-Upload, keine Objektversionierung, keine ACLs jenseits von öffentlich/privat auf Bucket-Ebene, keine Bucket-Erzeugung per API, keine CORS-Header, keine Replikation, keine Verschlüsselung im Ruhezustand. Wenn du irgendetwas davon brauchst, nimm etwas anderes.
Hol es dir: github.com/psyb0t/docker-hybrids3. Lizenziert unter WTFPL.

Wie Du Es In Deinen Agenten Installierst

Derselbe Skill, jetzt installierbar, ohne OpenClaw auch nur anzufassen. Alles unter .agents/ ist in einem einzigen Marktplatz katalogisiert, es sind also zwei Befehle:

claude plugin marketplace add psyb0t/agents
claude plugin install hybrids3@psyb0t

Codex benutzt denselben Marktplatz mit einem anderen Verb, codex plugin add hybrids3@psyb0t, weil es kein codex plugin install gibt. Es findet den Skill in einem Checkout des Repos auch von allein, da es .agents/skills/ nativ scannt, ganz ohne dass irgendetwas installiert ist. Es ist inzwischen auch in der offiziellen MCP Registry gelistet, ein Client, der Server von dort auflöst, kann es also finden, ohne eine URL in die Hand gedrückt zu bekommen.