Zet nginx voor MinIO op een padprefix en probeer presigned URL’s te gebruiken. Ga je gang, ik wacht wel.
Het probleem is AWS Signature V4. De handtekening dekt het pad van het verzoek. nginx op /storage/ knipt dat prefix eraf voordat hij doorstuurt, dus boto3 ondertekent /storage/bucket/key, de server ziet /bucket/key, de HMAC klopt niet, je krijgt een 403. Dit is geen configuratiefout, zo werkt padprefix-proxying in nginx en zo werkt SigV4-verificatie. Ze zijn structureel onverenigbaar, tenzij je slim wordt met je proxyconfig of overstapt op een server die het anders aanpakt.
docker-hybrids3 pakt het anders aan. Het heeft een configoptie path_prefix, je zet hem op /storage en alle routes verhuizen native onder dat prefix. Geen padknipwerk bij de proxy. boto3 ondertekent /storage/bucket/key, nginx stuurt /storage/bucket/key door, de server ziet /storage/bucket/key. De handtekeningen kloppen. Dat is ook waarom ik alles achter één nginx-gateway draai (aigate doet dit) zonder met de proxyconfig te vechten.
Het andere dat me ertoe dreef het te bouwen: wanneer een AI-agent een bestand moet schrijven en je een URL moet teruggeven, wil je daar geen gedistribueerde objectopslag voor optrekken. Je wilt iets dat in twee minuten start en verder uit de weg blijft. HybridS3 heeft een MCP-server ingebouwd, de agent roept upload_object aan, krijgt een URL terug, klaar.
Wat Het Is
SQLite voor metadata, platte bestanden op schijf. boto3 werkt ertegen. Kale HTTP met curl werkt. Auth met bearer token, geen SigV4-ceremonie voor gewone HTTP-verzoeken. Drie interfaces, één container, geen webconsole, geen IAM, geen gedistribueerde modus.
Buckets worden in de config gedefinieerd, niet via de API aangemaakt. Je weet altijd precies wat er bestaat. Wil je een bucket? Zet hem in de YAML, herstart. Wil je TTL-verval? Zet ttl: 24h op de bucket en objecten verwijderen zichzelf na hun laatste schrijfactie. Geen lifecycle-policies, geen cronjobs.
Hem Draaien
docker run -d --name hybrids3
-p 8080:8080
-v ./config.yaml:/config/config.yaml:ro
-v hybrids3-data:/data
psyb0t/hybrids3Config op /config/config.yaml, data op /data, poort 8080. Draait 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: 100MBElke bucket heeft een privé key (wordt nooit verstuurd, dient om HMAC’s te controleren) en een public_key (de S3-toegangssleutel-ID, veilig om in URL’s te zetten). Precies die splitsing laat presigned URL’s werken: de publieke sleutel mag in de URL verschijnen, de privésleutel nooit.
public: true betekent dat GET, HEAD en LIST geen auth vereisen. PUT en DELETE hebben nog steeds een sleutel nodig. public: false betekent dat alles auth vereist.
HTTP-API
Kale bearer token in de 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"Verzoeken met een AWS Sig V4-Authorization-header krijgen S3-compatibele XML-antwoorden. Al het andere krijgt 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/")Achter Nginx
Het deel dat er echt toe doet. Zet path_prefix: /storage in de config, en dan:
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;
}Twee dingen die je goed moet doen: geen afsluitende slash op proxy_pass (een afsluitende slash vertelt nginx het locationprefix eraf te knippen, en dat sloopt SigV4), en $http_host in plaats van $host ($host knipt de poort eraf, wat op niet-standaard poorten tot niet-kloppende handtekeningen leidt).
Met path_prefix gezet wijst boto3 naar http://yourdomain/storage en werkt alles.
MCP
Er draait een MCP-server op /mcp/. Zeven tools: upload_object, download_object, delete_object, list_objects, list_buckets, object_info, presign_url. Hang er elke MCP-compatibele client aan:
{
"mcpServers": {
"hybrids3": {
"type": "streamable-http",
"url": "https://ciprian.51k.eu80/mcp/"
}
}
}Auth op endpointniveau (Authorization-header of queryparameter ?auth=) of per tool via de parameter auth_key. Gebruik de mastersleutel voor volledige toegang, een bucketsleutel om de verbinding tot één bucket te beperken.
Presigned URL’s
Voor privé buckets genereert dit een echte presigned AWS Sig V4-URL, ondertekend met de privésleutel van de bucket:
curl -X POST "https://ciprian.51k.eu80/presign/permanent/doc.pdf?expires=3600"
-H "Authorization: Bearer perm-secret"Voor publieke buckets geeft het de kale URL terug, een handtekening is niet nodig omdat GET sowieso geen auth vereist. Vervalbereik van één seconde tot zeven dagen, standaard 3600. Verlopen of geknoeide URL’s geven 403.
Presigned PUT. Sinds v0.2.0 geeft ?method=PUT iemand een URL waarmee hij precies één sleutel kan uploaden zonder ooit je bucketsleutel te zien. Publieke buckets zijn hier geen kortere weg: anonieme leesacties zijn toegestaan, anonieme schrijfacties nooit, dus een presigned PUT is altijd ondertekend, zelfs op een publieke 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.pdfEen presigned URL is gebonden aan zijn werkwoord. Een GET-URL kan niet gebruikt worden om te PUTten en andersom, de methode is onderdeel van het canonieke verzoek dat de handtekening dekt. En dat is precies het punt: je kunt een derde partij een brievenbus geven voor precies één sleutel, voor precies tien minuten, en ze kunnen nog steeds niets anders lezen en nergens anders uploaden.
De MCP-tool presign_url neemt dezelfde method-parameter, dus een agent kan ook upload-URL’s slaan.
De build zit op slot
De toeleveringsketen is op hash vergrendeld op uv met een leeftijdsdrempel op vaste datum, dus een gisteren gepubliceerde afhankelijkheid kan niet in de build van vandaag glippen. Multi-stage productie-Dockerfile, base image en uv allebei vastgepind op digest in plaats van op tag gevolgd.
De Makefile is het enige startpunt, en alles wat met ontwikkeling te maken heeft draait in een afgeschermde dev-container. Er wordt niets op je host geïnstalleerd. 165 tests, 17 unittests, 148 integratietests.
De repo levert ook een agentskill en een OpenClaw-plugin mee onder .agents/, door CI op ClawHub gepubliceerd bij tag-pushes.
Beperkingen
Geen multipart upload, geen objectversionering, geen ACL’s verder dan publiek/privé op bucketniveau, geen bucketaanmaak via de API, geen CORS-headers, geen replicatie, geen versleuteling in rust. Heb je iets daarvan nodig, gebruik dan iets anders.
Pak hem: github.com/psyb0t/docker-hybrids3. Onder WTFPL-licentie.
Hoe Je Het In Je Agent Installeert
Dezelfde skill, nu installeerbaar zonder ook maar in de buurt van OpenClaw te komen. Alles onder .agents/ staat gecatalogiseerd in één marketplace, dus het zijn twee commando’s:
claude plugin marketplace add psyb0t/agents
claude plugin install hybrids3@psyb0tCodex gebruikt dezelfde marketplace met een ander werkwoord, codex plugin add hybrids3@psyb0t, want codex plugin install bestaat niet. Het vindt de skill ook zelf in een checkout van de repo, aangezien het .agents/skills/ native scant zonder dat er ook maar iets geïnstalleerd is. Het staat inmiddels ook in de officiële MCP Registry, dus een client die servers daarvandaan oplost kan het vinden zonder dat je hem een URL aanreikt.