fix(objstore): sign funkwhale's media URLs against the host the browser dials

funkwhale has two S3 surfaces sharing one private bucket. Audio goes out as X-Accel-Redirect and the front nginx fetches it server-side, but attachments (instance logo, avatars, covers) land in the API JSON as absolute URLs the BROWSER dials. AWS_S3_ENDPOINT_URL drives the boto3 client, so with the internal endpoint django-storages signed URLs for http://seaweedfs:8333, a host no browser can reach.

Point the client at the new bucket-less public_endpoint key. Measured against the live gateway: a URL signed for seaweedfs:8333 and sent to the public host answers 403 SignatureDoesNotMatch, while one signed for the public host reaches bucket lookup — query-string SigV4 binds the Host, so the signing host and the fetch host must be the same string. AWS_S3_CUSTOM_DOMAIN cannot serve this: storages/backends/s3.py:689 returns UNSIGNED without a CloudFront signer, and the bucket also holds the music library that music/views.py exists to gate.

AWS_CA_BUNDLE follows, because the public endpoint is TLS on the local CA and botocore reads its trust store only from that variable (configprovider.py:59) — sys-svc-compose-ca injects SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS, never this one. It takes the bundle rather than the bare root cert, since AWS_CA_BUNDLE replaces the default store.

AWS_QUERYSTRING_AUTH=True fixes a pre-existing break: upstream defaults it to `not PROXY_MEDIA`, which produced unsigned URLs against a private bucket. The new front nginx.conf.j2 serves /_protected/media, which the stock template resolves with `alias ${MEDIA_ROOT}/$1;` and therefore 404s for every S3-backed listen.

web-svc-seaweedfs keeps no onion vhost. Its tor-contract reason now states the mechanical cause instead of the old claim that the role is never a Tor-browser surface, which decidim and funkwhale have made false: the node hidden service publishes http only while sys-front-tls keys listen/ssl on application_id, so an onion vhost would listen on 443 against a port-80 hidden service. Attachment fetches therefore still fail on an onion pick, and the spec is left to fail there rather than record the miss and pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-15 22:14:38 +02:00
1 parent 40001f0e9a
commit 1e61debad2
10 files changed
+203 -6

No files matched your search

@@ -104,6 +104,7 @@ class ObjstoreLookupTests(unittest.TestCase):
self.assertEqual(out["url"], "")
self.assertEqual(out["public_domain"], "")
self.assertEqual(out["public_bucket"], "")
self.assertEqual(out["public_endpoint"], "")
self.assertEqual(out["public_url"], "")
self.assertEqual(out["reach_host"], "127.0.0.1")
@@ -139,6 +140,7 @@ class ObjstoreLookupTests(unittest.TestCase):
self.assertEqual(out["url"], "http://seaweedfs-central:8334")
self.assertEqual(out["public_domain"], "api.seaweedfs.s3.example.com")
self.assertEqual(out["public_bucket"], "api.seaweedfs.s3.example.com/foo")
self.assertEqual(out["public_endpoint"], "https://api.seaweedfs.s3.example.com")
self.assertEqual(out["public_url"], "https://api.seaweedfs.s3.example.com/foo")
self.assertEqual(
@@ -146,6 +148,28 @@ class ObjstoreLookupTests(unittest.TestCase):
"http://seaweedfs-central:8334",
)
def test_named_canonical_siblings_do_not_win_over_the_api(self):
applications = {
"web-app-foo": {
"services": {"minio": {"enabled": True, "shared": True}},
"secrets": {"credentials": {"objstore_secret_key": "sk"}},
},
"web-app-minio": {
"services": {"minio": {"name": "minio-central"}},
"domains": {
"canonical": {
"console": "console.minio.s3.example.com",
"api": "api.minio.s3.example.com",
}
},
},
}
out = self._run(["web-app-foo"], applications)[0]
self.assertEqual(out["public_domain"], "api.minio.s3.example.com")
self.assertEqual(out["public_endpoint"], "https://api.minio.s3.example.com")
def test_credentialed_url_percent_encodes_both_halves(self):
applications = {
"web-app-foo": {