From 9806b626401eca18346e06c43f17c0e9610f9c86 Mon Sep 17 00:00:00 2001 From: benaryorg Date: Mon, 29 Sep 2025 03:38:53 +0000 Subject: [PATCH 1/2] storages: s3 custom domain signature fix S3 signatures are fickle since there are several hashes of components which must ultimately be correct for the signature to match with no room for error. Since both the *host* header as well as the path, the former by default, the latter necessarily. The issue with custom domains is therefore that the *host* header mismatches unless the signature was created for that specific domain. This is complicated further by *boto3* making it incredibly difficult to actually sign a link with the path *not* containing the bucket prefix. Therefore setting the endpoint alone does not work with a truly custom domain (i.e. one where the data resides on the root of the domain). Changing this to a rather hacky workaround accessing fields of the internal data structures and grabbing the signer directly allows for signing the request. This comes with several caveats such as the parameters going largely untouched, however in preliminary testing it works fine. There are no superfluous slashes (i.e. no `https://example.com//file.svg`), no superfluous prefixes (i.e. `https://example.com/example.com/example.com/file.svg`), and there are no signature errors in general. Uploads still work. Reminder that the application cache may need clearing before the changes reflect in the non-admin UI. Relates: - [issue 13463](https://redirect.github.com/goauthentik/authentik/issues/13463) - [pr 14706 ](https://redirect.github.com/goauthentik/authentik/pull/14706) Signed-off-by: benaryorg --- authentik/root/storages.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/authentik/root/storages.py b/authentik/root/storages.py index e76efb337440..c0e86d7bea6f 100644 --- a/authentik/root/storages.py +++ b/authentik/root/storages.py @@ -9,6 +9,7 @@ from django.db import connection from storages.backends.s3 import S3Storage as BaseS3Storage from storages.utils import clean_name, safe_join +import botocore from authentik.lib.config import CONFIG @@ -97,16 +98,11 @@ def url(self, name, parameters=None, expire=None, http_method=None): ) if self.custom_domain: - # Key parameter can't be empty. Use "/" and remove it later. - params["Key"] = "/" - root_url_signed = self.bucket.meta.client.generate_presigned_url( - "get_object", Params=params, ExpiresIn=expire - ) - # Remove signing parameter and previously added key "/". - root_url = self._strip_signing_parameters(root_url_signed)[:-1] - # Replace bucket domain with custom domain. - custom_url = f"{self.url_protocol}//{self.custom_domain}/" - url = url.replace(root_url, custom_url) + custom_url = f"{self.url_protocol}//{self.custom_domain}/{name}" + request = botocore.awsrequest.AWSRequest(http_method or "GET", custom_url) + self.bucket.meta.client._request_signer.sign("GetObject", request, signing_type="presign-url", expires_in=expire) + prepared_request = request.prepare() + url = prepared_request.url if self.querystring_auth: return url From 217964c92634a49e2efa956f9b8608c0f1a7348f Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 10 Oct 2025 13:39:58 +0200 Subject: [PATCH 2/2] format Signed-off-by: Jens Langhammer --- authentik/root/storages.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/authentik/root/storages.py b/authentik/root/storages.py index c0e86d7bea6f..69d14adfc966 100644 --- a/authentik/root/storages.py +++ b/authentik/root/storages.py @@ -3,13 +3,13 @@ import os from urllib.parse import parse_qsl, urlsplit +from botocore.awsrequest import AWSRequest from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.core.files.storage import FileSystemStorage from django.db import connection from storages.backends.s3 import S3Storage as BaseS3Storage from storages.utils import clean_name, safe_join -import botocore from authentik.lib.config import CONFIG @@ -99,8 +99,10 @@ def url(self, name, parameters=None, expire=None, http_method=None): if self.custom_domain: custom_url = f"{self.url_protocol}//{self.custom_domain}/{name}" - request = botocore.awsrequest.AWSRequest(http_method or "GET", custom_url) - self.bucket.meta.client._request_signer.sign("GetObject", request, signing_type="presign-url", expires_in=expire) + request = AWSRequest(http_method or "GET", custom_url) + self.bucket.meta.client._request_signer.sign( + "GetObject", request, signing_type="presign-url", expires_in=expire + ) prepared_request = request.prepare() url = prepared_request.url