mirror of
https://github.com/goauthentik/authentik
synced 2026-05-15 11:26:31 +02:00
Compare commits
1 Commits
website/do
...
sdko/s3-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c325cb32 |
@@ -1,19 +1,70 @@
|
||||
from collections.abc import Generator, Iterator
|
||||
from contextlib import contextmanager
|
||||
from tempfile import SpooledTemporaryFile
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import ClientError
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import SECP256R1, EllipticCurvePrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import connection
|
||||
from django.db.models import Q
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.admin.files.backends.base import ManageableBackend, get_content_type
|
||||
from authentik.admin.files.backends.s3_urls import S3UrlOptions, s3_file_url
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
_CLOUDFRONT_RSA_KEY_SIZE = 2048
|
||||
|
||||
|
||||
def _validate_cloudfront_private_key(private_key) -> None:
|
||||
"""Validate a private key against CloudFront signed URL key requirements."""
|
||||
if isinstance(private_key, RSAPrivateKey):
|
||||
if private_key.key_size != _CLOUDFRONT_RSA_KEY_SIZE:
|
||||
raise ImproperlyConfigured(
|
||||
"CloudFront URL signing keypair must contain a 2048-bit RSA private key, "
|
||||
"or an ECDSA P-256 private key."
|
||||
)
|
||||
return
|
||||
if isinstance(private_key, EllipticCurvePrivateKey):
|
||||
if not isinstance(private_key.curve, SECP256R1):
|
||||
raise ImproperlyConfigured(
|
||||
"CloudFront URL signing keypair must contain an ECDSA P-256 private key, "
|
||||
"or a 2048-bit RSA private key."
|
||||
)
|
||||
return
|
||||
raise ImproperlyConfigured(
|
||||
"CloudFront URL signing keypair must contain an RSA or ECDSA private key."
|
||||
)
|
||||
|
||||
|
||||
def _cloudfront_private_key_from_keypair(selector: str) -> str:
|
||||
"""Return the PEM private key for a CloudFront signing Certificate-Key Pair."""
|
||||
query = Q(name=selector)
|
||||
try:
|
||||
query |= Q(kp_uuid=UUID(str(selector)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
keypair = CertificateKeyPair.objects.filter(query).first()
|
||||
if keypair is None:
|
||||
raise ImproperlyConfigured(
|
||||
"CloudFront URL signing keypair was not found. Configure "
|
||||
"storage.s3.cloudfront_keypair with a Certificate-Key Pair name or UUID."
|
||||
)
|
||||
if not keypair.key_data:
|
||||
raise ImproperlyConfigured("CloudFront URL signing keypair must include a private key.")
|
||||
private_key = keypair.private_key
|
||||
_validate_cloudfront_private_key(private_key)
|
||||
return keypair.key_data
|
||||
|
||||
|
||||
class S3Backend(ManageableBackend):
|
||||
"""S3-compatible object storage backend.
|
||||
@@ -33,7 +84,7 @@ class S3Backend(ManageableBackend):
|
||||
self._config = {}
|
||||
self._session = None
|
||||
|
||||
def _get_config(self, key: str, default: str | None) -> tuple[str | None, bool]:
|
||||
def _get_config(self, key: str, default: Any = None) -> tuple[Any, bool]:
|
||||
unset = object()
|
||||
current = self._config.get(key, unset)
|
||||
refreshed = CONFIG.refresh(
|
||||
@@ -45,6 +96,18 @@ class S3Backend(ManageableBackend):
|
||||
self._config[key] = refreshed
|
||||
return (refreshed, current != refreshed)
|
||||
|
||||
def _get_config_value(self, key: str, default: Any = None) -> Any:
|
||||
return CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.{key}",
|
||||
CONFIG.get(f"storage.{self.name}.{key}", default),
|
||||
)
|
||||
|
||||
def _get_config_bool(self, key: str, default: bool = False) -> bool:
|
||||
return CONFIG.get_bool(
|
||||
f"storage.{self.usage.value}.{self.name}.{key}",
|
||||
CONFIG.get_bool(f"storage.{self.name}.{key}", default),
|
||||
)
|
||||
|
||||
@property
|
||||
def base_path(self) -> str:
|
||||
"""S3 key prefix: {usage}/{schema}/"""
|
||||
@@ -52,10 +115,23 @@ class S3Backend(ManageableBackend):
|
||||
|
||||
@property
|
||||
def bucket_name(self) -> str:
|
||||
return CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.bucket_name",
|
||||
CONFIG.get(f"storage.{self.name}.bucket_name"),
|
||||
)
|
||||
return self._get_config_value("bucket_name")
|
||||
|
||||
@property
|
||||
def object_acl(self) -> str | None:
|
||||
"""ACL applied to uploaded objects, or None to omit ACL entirely."""
|
||||
object_acl = self._get_config_value("object_acl", "private")
|
||||
if object_acl in (None, ""):
|
||||
return None
|
||||
return object_acl
|
||||
|
||||
@property
|
||||
def cloudfront_private_key(self) -> str | None:
|
||||
"""Private key loaded from an authentik Certificate-Key Pair."""
|
||||
keypair = self._get_config_value("cloudfront_keypair", None)
|
||||
if keypair in (None, ""):
|
||||
return None
|
||||
return _cloudfront_private_key_from_keypair(str(keypair))
|
||||
|
||||
@property
|
||||
def session(self) -> boto3.Session:
|
||||
@@ -84,26 +160,11 @@ class S3Backend(ManageableBackend):
|
||||
@property
|
||||
def client(self):
|
||||
"""Create S3 client with configured endpoint and region."""
|
||||
endpoint_url = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.endpoint",
|
||||
CONFIG.get(f"storage.{self.name}.endpoint", None),
|
||||
)
|
||||
use_ssl = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.use_ssl",
|
||||
CONFIG.get(f"storage.{self.name}.use_ssl", True),
|
||||
)
|
||||
region_name = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.region",
|
||||
CONFIG.get(f"storage.{self.name}.region", None),
|
||||
)
|
||||
addressing_style = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.addressing_style",
|
||||
CONFIG.get(f"storage.{self.name}.addressing_style", "auto"),
|
||||
)
|
||||
signature_version = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.signature_version",
|
||||
CONFIG.get(f"storage.{self.name}.signature_version", "s3v4"),
|
||||
)
|
||||
endpoint_url = self._get_config_value("endpoint", None)
|
||||
use_ssl = self._get_config_value("use_ssl", True)
|
||||
region_name = self._get_config_value("region", None)
|
||||
addressing_style = self._get_config_value("addressing_style", "auto")
|
||||
signature_version = self._get_config_value("signature_version", "s3v4")
|
||||
# Keep signature_version pass-through and let boto3/botocore handle it.
|
||||
# In boto3's S3 configuration docs, `s3v4` (default) and deprecated `s3`
|
||||
# are the documented values:
|
||||
@@ -148,75 +209,29 @@ class S3Backend(ManageableBackend):
|
||||
request: HttpRequest | None = None,
|
||||
use_cache: bool = True,
|
||||
) -> str:
|
||||
"""Generate presigned URL for file access."""
|
||||
use_https = CONFIG.get_bool(
|
||||
f"storage.{self.usage.value}.{self.name}.secure_urls",
|
||||
CONFIG.get_bool(f"storage.{self.name}.secure_urls", True),
|
||||
)
|
||||
"""Generate a signed or unsigned URL for file access."""
|
||||
use_https = self._get_config_bool("secure_urls", True)
|
||||
querystring_auth = self._get_config_bool("querystring_auth", True)
|
||||
|
||||
expires_in = int(
|
||||
timedelta_from_string(
|
||||
CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.url_expiry",
|
||||
CONFIG.get(f"storage.{self.name}.url_expiry", "minutes=15"),
|
||||
)
|
||||
self._get_config_value("url_expiry", "minutes=15")
|
||||
).total_seconds()
|
||||
)
|
||||
|
||||
def _file_url(name: str, request: HttpRequest | None) -> str:
|
||||
client = self.client
|
||||
params = {
|
||||
"Bucket": self.bucket_name,
|
||||
"Key": f"{self.base_path}/{name}",
|
||||
}
|
||||
|
||||
operation_name = "GetObject"
|
||||
operation_model = client.meta.service_model.operation_model(operation_name)
|
||||
request_dict = client._convert_to_request_dict(
|
||||
params,
|
||||
operation_model,
|
||||
endpoint_url=client.meta.endpoint_url,
|
||||
context={"is_presign_request": True},
|
||||
)
|
||||
|
||||
# Support custom domain for S3-compatible storage (so not AWS)
|
||||
# Well, can't you do custom domains on AWS as well?
|
||||
custom_domain = CONFIG.get(
|
||||
f"storage.{self.usage.value}.{self.name}.custom_domain",
|
||||
CONFIG.get(f"storage.{self.name}.custom_domain", None),
|
||||
)
|
||||
if custom_domain:
|
||||
scheme = "https" if use_https else "http"
|
||||
path = request_dict["url_path"]
|
||||
|
||||
# When using path-style addressing, the presigned URL contains the bucket
|
||||
# name in the path (e.g., /bucket-name/key). Since custom_domain must
|
||||
# include the bucket name (per docs), strip it from the path to avoid
|
||||
# duplication. See: https://github.com/goauthentik/authentik/issues/19521
|
||||
# Check with trailing slash to ensure exact bucket name match
|
||||
if path.startswith(f"/{self.bucket_name}/"):
|
||||
path = path.removeprefix(f"/{self.bucket_name}")
|
||||
|
||||
# Normalize to avoid double slashes
|
||||
custom_domain = custom_domain.rstrip("/")
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
custom_base = urlsplit(f"{scheme}://{custom_domain}")
|
||||
|
||||
# Sign the final public URL instead of signing the internal S3 endpoint and
|
||||
# rewriting it afterwards. Presigned SigV4 URLs include the host header in the
|
||||
# canonical request, so post-sign host changes break strict backends like RustFS.
|
||||
public_path = f"{custom_base.path.rstrip('/')}{path}" if custom_base.path else path
|
||||
request_dict["url_path"] = public_path
|
||||
request_dict["url"] = urlunsplit(
|
||||
(custom_base.scheme, custom_base.netloc, public_path, "", "")
|
||||
)
|
||||
|
||||
return client._request_signer.generate_presigned_url(
|
||||
request_dict,
|
||||
operation_name,
|
||||
expires_in=expires_in,
|
||||
return s3_file_url(
|
||||
client=self.client,
|
||||
bucket_name=self.bucket_name,
|
||||
key=f"{self.base_path}/{name}",
|
||||
options=S3UrlOptions(
|
||||
expires_in=expires_in,
|
||||
custom_domain=self._get_config_value("custom_domain", None),
|
||||
use_https=use_https,
|
||||
querystring_auth=querystring_auth,
|
||||
cloudfront_key_id=self._get_config_value("cloudfront_key_id", None),
|
||||
cloudfront_private_key=self.cloudfront_private_key,
|
||||
),
|
||||
)
|
||||
|
||||
if use_cache:
|
||||
@@ -226,12 +241,15 @@ class S3Backend(ManageableBackend):
|
||||
|
||||
def save_file(self, name: str, content: bytes) -> None:
|
||||
"""Save file to S3."""
|
||||
extra_args = {}
|
||||
if self.object_acl is not None:
|
||||
extra_args["ACL"] = self.object_acl
|
||||
self.client.put_object(
|
||||
Bucket=self.bucket_name,
|
||||
Key=f"{self.base_path}/{name}",
|
||||
Body=content,
|
||||
ACL="private",
|
||||
ContentType=get_content_type(name),
|
||||
**extra_args,
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
@@ -241,14 +259,14 @@ class S3Backend(ManageableBackend):
|
||||
with SpooledTemporaryFile(max_size=5 * 1024 * 1024, suffix=".S3File") as file:
|
||||
yield file
|
||||
file.seek(0)
|
||||
extra_args = {"ContentType": get_content_type(name)}
|
||||
if self.object_acl is not None:
|
||||
extra_args["ACL"] = self.object_acl
|
||||
self.client.upload_fileobj(
|
||||
Fileobj=file,
|
||||
Bucket=self.bucket_name,
|
||||
Key=f"{self.base_path}/{name}",
|
||||
ExtraArgs={
|
||||
"ACL": "private",
|
||||
"ContentType": get_content_type(name),
|
||||
},
|
||||
ExtraArgs=extra_args,
|
||||
)
|
||||
|
||||
def delete_file(self, name: str) -> None:
|
||||
|
||||
133
authentik/admin/files/backends/s3_urls.py
Normal file
133
authentik/admin/files/backends/s3_urls.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""URL helpers for S3-compatible file storage."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from botocore.signers import CloudFrontSigner
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, padding
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class S3UrlOptions:
|
||||
"""Options used to generate a browser-facing S3 file URL."""
|
||||
|
||||
expires_in: int
|
||||
custom_domain: str | None
|
||||
use_https: bool
|
||||
querystring_auth: bool
|
||||
cloudfront_key_id: str | None = None
|
||||
cloudfront_private_key: str | None = None
|
||||
|
||||
|
||||
def get_object_request_dict(client, bucket_name: str, key: str) -> dict:
|
||||
"""Build botocore's request dict for an S3 GetObject request."""
|
||||
operation_name = "GetObject"
|
||||
operation_model = client.meta.service_model.operation_model(operation_name)
|
||||
return client._convert_to_request_dict(
|
||||
{
|
||||
"Bucket": bucket_name,
|
||||
"Key": key,
|
||||
},
|
||||
operation_model,
|
||||
endpoint_url=client.meta.endpoint_url,
|
||||
context={"is_presign_request": True},
|
||||
)
|
||||
|
||||
|
||||
def apply_custom_domain(
|
||||
request_dict: dict,
|
||||
bucket_name: str,
|
||||
custom_domain: str | None,
|
||||
use_https: bool,
|
||||
) -> dict:
|
||||
"""Apply a public custom domain to an S3 request dict."""
|
||||
if not custom_domain:
|
||||
return request_dict
|
||||
|
||||
scheme = "https" if use_https else "http"
|
||||
path = request_dict["url_path"]
|
||||
|
||||
# When using path-style addressing, the presigned URL contains the bucket
|
||||
# name in the path (e.g., /bucket-name/key). Since custom domains for
|
||||
# path-style providers also include the bucket name, strip it from the
|
||||
# generated path to avoid duplication. See:
|
||||
# https://github.com/goauthentik/authentik/issues/19521
|
||||
if path.startswith(f"/{bucket_name}/"):
|
||||
path = path.removeprefix(f"/{bucket_name}")
|
||||
|
||||
custom_base = urlsplit(f"{scheme}://{custom_domain.rstrip('/')}")
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
# Sign the final public URL instead of signing the internal S3 endpoint and
|
||||
# rewriting it afterwards. Presigned SigV4 URLs include the host header in
|
||||
# the canonical request, so post-sign host changes break strict backends.
|
||||
public_path = f"{custom_base.path.rstrip('/')}{path}" if custom_base.path else path
|
||||
request_dict["url_path"] = public_path
|
||||
request_dict["url"] = urlunsplit((custom_base.scheme, custom_base.netloc, public_path, "", ""))
|
||||
return request_dict
|
||||
|
||||
|
||||
def cloudfront_signed_url(
|
||||
url: str,
|
||||
expires_in: int,
|
||||
key_id: str,
|
||||
private_key: str,
|
||||
) -> str:
|
||||
"""Sign a CloudFront viewer URL using a canned policy."""
|
||||
private_key = private_key.replace("\\n", "\n")
|
||||
key = serialization.load_pem_private_key(private_key.encode("utf-8"), password=None)
|
||||
|
||||
def signer(message: bytes) -> bytes:
|
||||
if isinstance(key, RSAPrivateKey):
|
||||
return key.sign(message, padding.PKCS1v15(), hashes.SHA1())
|
||||
if isinstance(key, EllipticCurvePrivateKey):
|
||||
return key.sign(message, ec.ECDSA(hashes.SHA256()))
|
||||
raise ValueError("CloudFront URL signing requires an RSA or ECDSA private key")
|
||||
|
||||
cloudfront_signer = CloudFrontSigner(key_id, signer)
|
||||
return cloudfront_signer.generate_presigned_url(
|
||||
url,
|
||||
date_less_than=datetime.now(UTC) + timedelta(seconds=expires_in),
|
||||
)
|
||||
|
||||
|
||||
def s3_file_url(
|
||||
client,
|
||||
bucket_name: str,
|
||||
key: str,
|
||||
options: S3UrlOptions,
|
||||
) -> str:
|
||||
"""Build a signed or unsigned browser-facing URL for an S3 object."""
|
||||
request_dict = get_object_request_dict(client, bucket_name, key)
|
||||
request_dict = apply_custom_domain(
|
||||
request_dict,
|
||||
bucket_name,
|
||||
options.custom_domain,
|
||||
options.use_https,
|
||||
)
|
||||
|
||||
if options.querystring_auth:
|
||||
return client._request_signer.generate_presigned_url(
|
||||
request_dict,
|
||||
"GetObject",
|
||||
expires_in=options.expires_in,
|
||||
)
|
||||
|
||||
if options.cloudfront_key_id or options.cloudfront_private_key:
|
||||
if not options.cloudfront_key_id or not options.cloudfront_private_key:
|
||||
raise ValueError("CloudFront URL signing requires both key_id and private_key")
|
||||
if not options.custom_domain:
|
||||
raise ValueError("CloudFront URL signing requires a custom domain")
|
||||
return cloudfront_signed_url(
|
||||
request_dict["url"],
|
||||
options.expires_in,
|
||||
options.cloudfront_key_id,
|
||||
options.cloudfront_private_key,
|
||||
)
|
||||
|
||||
return request_dict["url"]
|
||||
@@ -1,14 +1,108 @@
|
||||
from unittest import TestCase as UnitTestCase
|
||||
from unittest import skipUnless
|
||||
from unittest.mock import Mock, PropertyMock, patch
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
from botocore.exceptions import UnsupportedSignatureVersionError
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.admin.files.backends.s3 import S3Backend
|
||||
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
|
||||
from authentik.admin.files.usage import FileUsage
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class TestS3BackendUploadArgs(UnitTestCase):
|
||||
"""Test S3 upload arguments that don't require a live S3 service."""
|
||||
|
||||
@CONFIG.patch("storage.s3.bucket_name", "test-bucket")
|
||||
def test_save_file_includes_private_acl_by_default(self):
|
||||
client = Mock()
|
||||
backend = S3Backend(FileUsage.MEDIA)
|
||||
|
||||
with (
|
||||
patch.object(S3Backend, "client", new_callable=PropertyMock, return_value=client),
|
||||
patch("authentik.admin.files.backends.s3.connection") as connection_mock,
|
||||
):
|
||||
connection_mock.schema_name = "public"
|
||||
backend.save_file("test.png", b"test")
|
||||
|
||||
client.put_object.assert_called_once()
|
||||
self.assertEqual(client.put_object.call_args.kwargs["ACL"], "private")
|
||||
|
||||
@CONFIG.patch("storage.s3.bucket_name", "test-bucket")
|
||||
@CONFIG.patch("storage.s3.bucket_name", "test-bucket")
|
||||
@CONFIG.patch("storage.s3.object_acl", "")
|
||||
def test_save_file_stream_omits_acl_when_empty_string(self):
|
||||
client = Mock()
|
||||
backend = S3Backend(FileUsage.MEDIA)
|
||||
|
||||
with (
|
||||
patch.object(S3Backend, "client", new_callable=PropertyMock, return_value=client),
|
||||
patch("authentik.admin.files.backends.s3.connection") as connection_mock,
|
||||
):
|
||||
connection_mock.schema_name = "public"
|
||||
with backend.save_file_stream("test.csv") as file:
|
||||
file.write(b"test")
|
||||
|
||||
client.upload_fileobj.assert_called_once()
|
||||
self.assertNotIn("ACL", client.upload_fileobj.call_args.kwargs["ExtraArgs"])
|
||||
|
||||
|
||||
class TestS3BackendCloudFrontKeypair(UnitTestCase):
|
||||
"""Test CloudFront signing keypair resolution without a live S3 service."""
|
||||
|
||||
def _keypair(self, key_size: int = 2048, private_key=None):
|
||||
private_key = private_key or rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=key_size
|
||||
)
|
||||
keypair = Mock()
|
||||
keypair.key_data = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
).decode("utf-8")
|
||||
keypair.private_key = private_key
|
||||
return keypair
|
||||
|
||||
@CONFIG.patch("storage.s3.cloudfront_keypair", "missing")
|
||||
def test_cloudfront_private_key_requires_existing_keypair(self):
|
||||
backend = S3Backend(FileUsage.MEDIA)
|
||||
|
||||
with patch(
|
||||
"authentik.admin.files.backends.s3.CertificateKeyPair.objects.filter"
|
||||
) as filter_mock:
|
||||
filter_mock.return_value.first.return_value = None
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
_ = backend.cloudfront_private_key
|
||||
|
||||
@CONFIG.patch("storage.s3.cloudfront_keypair", "cloudfront")
|
||||
def test_cloudfront_private_key_requires_rsa_2048(self):
|
||||
backend = S3Backend(FileUsage.MEDIA)
|
||||
keypair = self._keypair(key_size=4096)
|
||||
|
||||
with patch(
|
||||
"authentik.admin.files.backends.s3.CertificateKeyPair.objects.filter"
|
||||
) as filter_mock:
|
||||
filter_mock.return_value.first.return_value = keypair
|
||||
with self.assertRaisesRegex(ImproperlyConfigured, "2048-bit"):
|
||||
_ = backend.cloudfront_private_key
|
||||
|
||||
@CONFIG.patch("storage.s3.cloudfront_keypair", "cloudfront")
|
||||
def test_cloudfront_private_key_allows_ecdsa_p256(self):
|
||||
backend = S3Backend(FileUsage.MEDIA)
|
||||
keypair = self._keypair(private_key=ec.generate_private_key(ec.SECP256R1()))
|
||||
|
||||
with patch(
|
||||
"authentik.admin.files.backends.s3.CertificateKeyPair.objects.filter"
|
||||
) as filter_mock:
|
||||
filter_mock.return_value.first.return_value = keypair
|
||||
self.assertEqual(backend.cloudfront_private_key, keypair.key_data)
|
||||
|
||||
|
||||
@skipUnless(s3_test_server_available(), "S3 test server not available")
|
||||
class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
"""Test S3 backend functionality"""
|
||||
@@ -83,6 +177,25 @@ class TestS3Backend(FileTestS3BackendMixin, TestCase):
|
||||
self.assertIn("X-Amz-Signature=", url)
|
||||
self.assertIn("test.png", url)
|
||||
|
||||
@CONFIG.patch("storage.s3.querystring_auth", False)
|
||||
@CONFIG.patch("storage.s3.custom_domain", "assets.example.test")
|
||||
def test_file_url_unsigned_custom_domain(self):
|
||||
"""Test file_url can generate stable unsigned CDN URLs."""
|
||||
url = self.media_s3_backend.file_url("test.png", use_cache=False)
|
||||
|
||||
self.assertEqual(url, "https://assets.example.test/media/public/test.png")
|
||||
|
||||
@CONFIG.patch("storage.s3.querystring_auth", True)
|
||||
@CONFIG.patch("storage.media.s3.querystring_auth", False)
|
||||
@CONFIG.patch("storage.media.s3.custom_domain", "assets.example.test")
|
||||
def test_file_url_querystring_auth_usage_override(self):
|
||||
"""Test usage-specific querystring_auth overrides global config."""
|
||||
media_url = self.media_s3_backend.file_url("test.png", use_cache=False)
|
||||
reports_url = self.reports_s3_backend.file_url("test.csv", use_cache=False)
|
||||
|
||||
self.assertEqual(media_url, "https://assets.example.test/media/public/test.png")
|
||||
self.assertIn("X-Amz-Signature=", reports_url)
|
||||
|
||||
def test_client_signature_version_default_v4(self):
|
||||
"""Test S3 client defaults to v4 signature when not configured."""
|
||||
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4")
|
||||
|
||||
77
authentik/admin/files/backends/tests/test_s3_urls.py
Normal file
77
authentik/admin/files/backends/tests/test_s3_urls.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from unittest import TestCase
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
from authentik.admin.files.backends.s3_urls import S3UrlOptions, s3_file_url
|
||||
|
||||
|
||||
def ec_private_key_pem() -> str:
|
||||
key = ec.generate_private_key(ec.SECP256R1())
|
||||
return key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
).decode("utf-8")
|
||||
|
||||
|
||||
def s3_client(addressing_style: str = "path"):
|
||||
return boto3.Session(
|
||||
aws_access_key_id="accessKey1",
|
||||
aws_secret_access_key="secretKey1",
|
||||
).client(
|
||||
"s3",
|
||||
endpoint_url="http://localhost:8020",
|
||||
region_name="us-east-1",
|
||||
config=Config(
|
||||
signature_version="s3v4",
|
||||
s3={"addressing_style": addressing_style},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestS3Urls(TestCase):
|
||||
def test_unsigned_custom_domain_with_bucket_path(self):
|
||||
"""Path-style custom domains keep the bucket path once."""
|
||||
url = s3_file_url(
|
||||
client=s3_client(),
|
||||
bucket_name="authentik-data",
|
||||
key="media/public/logo.png",
|
||||
options=S3UrlOptions(
|
||||
expires_in=900,
|
||||
custom_domain="s3.example.com/authentik-data",
|
||||
use_https=True,
|
||||
querystring_auth=False,
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(url, "https://s3.example.com/authentik-data/media/public/logo.png")
|
||||
|
||||
def test_cloudfront_signed_custom_domain_url(self):
|
||||
"""CloudFront signing signs the viewer-facing custom domain URL."""
|
||||
url = s3_file_url(
|
||||
client=s3_client(),
|
||||
bucket_name="authentik-data",
|
||||
key="media/public/logo.png",
|
||||
options=S3UrlOptions(
|
||||
expires_in=900,
|
||||
custom_domain="assets.example.com",
|
||||
use_https=True,
|
||||
querystring_auth=False,
|
||||
cloudfront_key_id="K1234567890",
|
||||
cloudfront_private_key=ec_private_key_pem(),
|
||||
),
|
||||
)
|
||||
|
||||
parts = urlsplit(url)
|
||||
params = parse_qs(parts.query)
|
||||
self.assertEqual(parts.scheme, "https")
|
||||
self.assertEqual(parts.netloc, "assets.example.com")
|
||||
self.assertEqual(parts.path, "/media/public/logo.png")
|
||||
self.assertEqual(params["Key-Pair-Id"], ["K1234567890"])
|
||||
self.assertIn("Expires", params)
|
||||
self.assertIn("Signature", params)
|
||||
self.assertNotIn("X-Amz-Signature", params)
|
||||
@@ -190,8 +190,12 @@ storage:
|
||||
# access_key: ""
|
||||
# secret_key: ""
|
||||
# bucket_name: "authentik-data"
|
||||
# object_acl: "private"
|
||||
# How to render file URLs
|
||||
# custom_domain: null
|
||||
# querystring_auth: True
|
||||
# cloudfront_key_id: null
|
||||
# cloudfront_keypair: null
|
||||
secure_urls: True
|
||||
url_expiry: "minutes=15"
|
||||
# Usage based settings. Same schema as global settings, overrides global settings
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Files
|
||||
|
||||
Image files are used in authentik to add icons to new applications or sources, and to define the ["branded" look](../sys-mgmt/brands/index.md#branding-settings) of the authentik interface, with your company's logo and title, a favicon, or a background image for the flows.
|
||||
|
||||
authentik provides a centralized file management system for storing and organizing these files. Files can be uploaded and managed from **Customization** > **Files** in the Admin interface. By default, files are stored on disk in the `/data` directory, but [S3 storage](../sys-mgmt/ops/storage-s3.md) can also be configured.
|
||||
authentik provides a centralized file management system for storing and organizing these files. Files can be uploaded and managed from **Customization** > **Files** in the Admin interface. By default, files are stored on disk in the `/data` directory, but [S3 storage](../sys-mgmt/ops/storage-s3/index.md) can also be configured.
|
||||
|
||||
If file uploads are missing or unavailable after an upgrade, see [Errors when uploading icons](../troubleshooting/image_upload.md).
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ Defaults to `minutes=15`.
|
||||
|
||||
### S3 storage backend settings
|
||||
|
||||
For more information on S3 storage, see [S3 storage setup](../../sys-mgmt/ops/storage-s3.md).
|
||||
For more information on S3 storage, see [S3 storage setup](../../sys-mgmt/ops/storage-s3/index.md).
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__REGION`
|
||||
|
||||
@@ -502,16 +502,50 @@ Defaults to not set.
|
||||
|
||||
Name of the bucket to use to store files.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__OBJECT_ACL`
|
||||
|
||||
Canned ACL to apply to uploaded objects.
|
||||
|
||||
Defaults to `private`.
|
||||
|
||||
Set to an empty value or `null` to omit the ACL argument. See the [S3 storage setup](../../sys-mgmt/ops/storage-s3/index.md#bucket-creation) docs for AWS buckets with ACLs disabled.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN`
|
||||
|
||||
Domain to use to create URLs for users. Mainly useful for non-AWS providers.
|
||||
Domain to use to create URLs for users. This can be a CloudFront distribution domain, a CloudFront alternate domain name, a virtual-hosted S3 domain, or a path-style S3-compatible provider domain.
|
||||
|
||||
May include a port. Must include the bucket.
|
||||
May include a port. Include the bucket only when the public URL path needs it, for example with path-style S3-compatible providers.
|
||||
|
||||
Example: `s3.company:8080/authentik-data`.
|
||||
|
||||
Defaults to not set.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH`
|
||||
|
||||
Whether generated file URLs should be S3 presigned URLs.
|
||||
|
||||
Defaults to `true`.
|
||||
|
||||
Set to `false` when files are delivered by a public CDN, by CloudFront with Origin Access Control, or by another layer that performs viewer authorization.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__CLOUDFRONT_KEY_ID`
|
||||
|
||||
CloudFront public key ID to use for signed viewer URLs.
|
||||
|
||||
Only used when `AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false` and `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN` is set.
|
||||
|
||||
Defaults to not set.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__CLOUDFRONT_KEYPAIR`
|
||||
|
||||
Name or UUID of the authentik Certificate-Key Pair to use for CloudFront signed viewer URLs.
|
||||
|
||||
Only used when `AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false` and `AUTHENTIK_STORAGE__S3__CLOUDFRONT_KEY_ID` is also set.
|
||||
|
||||
See [CloudFront signed viewer URLs](../../sys-mgmt/ops/storage-s3/cloudfront.md#configure-cloudfront-signed-viewer-urls) for supported key types and setup steps.
|
||||
|
||||
Defaults to not set.
|
||||
|
||||
#### `AUTHENTIK_STORAGE__S3__SECURE_URLS`
|
||||
|
||||
Whether URLs created use HTTPS or HTTP.
|
||||
|
||||
@@ -41,7 +41,7 @@ Other optional pre-installation configurations that you might have already compl
|
||||
- [Configured your global email settings](../email/#configure-global-email-settings).
|
||||
- [Configured your PostgreSQL settings](../configuration/configuration.mdx#postgresql-settings) (read-replica, connections, etc.).
|
||||
- Configured a [reverse proxy](../reverse-proxy.md).
|
||||
- Configured your [media storage settings](../../install-config/configuration/configuration.mdx#media-storage-settings) or optionally [AWS S3 file storage](../../sys-mgmt/ops/storage-s3.md).
|
||||
- Configured your [media storage settings](../../install-config/configuration/configuration.mdx#media-storage-settings) or optionally [AWS S3 file storage](../../sys-mgmt/ops/storage-s3/index.md).
|
||||
- Added additional [custom configurations environment variables](../configuration/#set-your-environment-variables).
|
||||
- [Verified](../configuration/#verify-your-configuration-settings) your configuration settings.
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ slug: /releases/2024.2
|
||||
|
||||
- **S3 file storage**
|
||||
|
||||
Media files can now be stored on S3. Follow the [setup guide](../../sys-mgmt/ops/storage-s3.md) to get started.
|
||||
Media files can now be stored on S3. Follow the [setup guide](../../sys-mgmt/ops/storage-s3/index.md) to get started.
|
||||
|
||||
- **_Pretend user exists_ option for Identification stage**
|
||||
|
||||
|
||||
@@ -692,7 +692,20 @@ const items = [
|
||||
items: [
|
||||
"sys-mgmt/ops/monitoring",
|
||||
"sys-mgmt/ops/worker",
|
||||
"sys-mgmt/ops/storage-s3",
|
||||
{
|
||||
type: "category",
|
||||
label: "S3 storage",
|
||||
collapsed: true,
|
||||
link: {
|
||||
type: "doc",
|
||||
id: "sys-mgmt/ops/storage-s3/index",
|
||||
},
|
||||
items: [
|
||||
"sys-mgmt/ops/storage-s3/index",
|
||||
"sys-mgmt/ops/storage-s3/standard",
|
||||
"sys-mgmt/ops/storage-s3/cloudfront",
|
||||
],
|
||||
},
|
||||
"sys-mgmt/ops/geoip",
|
||||
"sys-mgmt/ops/backup-restore",
|
||||
],
|
||||
|
||||
97
website/docs/sys-mgmt/ops/storage-s3/cloudfront.md
Normal file
97
website/docs/sys-mgmt/ops/storage-s3/cloudfront.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: CloudFront delivery
|
||||
---
|
||||
|
||||
CloudFront can sit in front of the S3 bucket while authentik continues to write directly to S3. Use `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN` to make authentik return URLs for your CloudFront distribution domain or alternate domain name.
|
||||
|
||||
Complete the shared [S3 storage setup](index.md) first.
|
||||
|
||||
## Configure CloudFront delivery
|
||||
|
||||
Configure CloudFront and any origin access settings in AWS. If you protect the S3 origin with Origin Access Control (OAC), configure the CloudFront-to-S3 bucket policy in AWS as well. This is separate from the browser-facing URLs that authentik returns.
|
||||
|
||||
This setup is also the base for [CloudFront signed viewer URLs](#configure-cloudfront-signed-viewer-urls), because authentik signs the URL built from `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN`.
|
||||
|
||||
Configure authentik to write directly to S3 and return CloudFront URLs:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__BACKEND=s3
|
||||
AUTHENTIK_STORAGE__S3__REGION=ca-central-1
|
||||
AUTHENTIK_STORAGE__S3__BUCKET_NAME=authentik-data
|
||||
AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=assets.authentik.company
|
||||
AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false
|
||||
AUTHENTIK_STORAGE__S3__OBJECT_ACL=
|
||||
```
|
||||
|
||||
`AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN` can be a CloudFront distribution domain, such as `d111111abcdef8.cloudfront.net`, or an alternate domain name, such as `assets.authentik.company`.
|
||||
|
||||
Set `AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false` for CloudFront delivery. This turns off S3 presigned browser URLs. CloudFront access control, if used, is handled by CloudFront, not by S3 presigned URLs from authentik.
|
||||
|
||||
You can use the same options globally with `AUTHENTIK_STORAGE__S3__...`, or per usage with `AUTHENTIK_STORAGE__MEDIA__S3__...` and `AUTHENTIK_STORAGE__REPORTS__S3__...`.
|
||||
|
||||
## Configure CloudFront signed viewer URLs
|
||||
|
||||
CloudFront signed viewer URLs let authentik return temporary CloudFront URLs for distributions that restrict viewer access with trusted key groups.
|
||||
|
||||
### Create or import the signing key
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **System** > **Certificates**.
|
||||
3. Create or upload a Certificate-Key Pair.
|
||||
4. Use an ECDSA P-256 private key, or import a 2048-bit RSA private key.
|
||||
|
||||
**TODO:** authentik currently cannot choose custom RSA key sizes when generating Certificate-Key Pairs. Until that is fixed, use authentik's ECDSA option or import an externally generated RSA 2048 key pair.
|
||||
|
||||
### Upload the public key to CloudFront
|
||||
|
||||
CloudFront needs the public key, while authentik stores and uses the private key from the Certificate-Key Pair.
|
||||
|
||||
**TODO:** authentik currently cannot download only the raw public key from a Certificate-Key Pair. Until that is fixed, download the certificate and extract the public key:
|
||||
|
||||
```bash
|
||||
openssl x509 -pubkey -noout -in cloudfront-certificate.pem > cloudfront-public-key.pem
|
||||
```
|
||||
|
||||
If you generate an RSA 2048 key pair outside authentik, you can create the public key directly:
|
||||
|
||||
```bash
|
||||
openssl genrsa -out cloudfront-private-key.pem 2048
|
||||
openssl rsa -pubout -in cloudfront-private-key.pem -out cloudfront-public-key.pem
|
||||
```
|
||||
|
||||
Pair that private key with a self-signed certificate before importing it into authentik:
|
||||
|
||||
```bash
|
||||
openssl req -new -x509 \
|
||||
-key cloudfront-private-key.pem \
|
||||
-out cloudfront-certificate.pem \
|
||||
-days 3650 \
|
||||
-subj "/CN=authentik CloudFront media signing"
|
||||
```
|
||||
|
||||
1. Log in to the AWS console and open **CloudFront**.
|
||||
2. Navigate to **Public keys** and create a public key from `cloudfront-public-key.pem`.
|
||||
3. Copy the CloudFront public key ID.
|
||||
4. Navigate to **Key groups** and create a key group that includes the public key.
|
||||
5. Edit the behavior that serves authentik files, enable **Restrict viewer access**, and select the trusted key group.
|
||||
|
||||
### Configure authentik
|
||||
|
||||
Add the CloudFront key ID and the authentik Certificate-Key Pair name or UUID:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__BACKEND=s3
|
||||
AUTHENTIK_STORAGE__S3__REGION=ca-central-1
|
||||
AUTHENTIK_STORAGE__S3__BUCKET_NAME=authentik-data
|
||||
AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=assets.authentik.company
|
||||
AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false
|
||||
AUTHENTIK_STORAGE__S3__CLOUDFRONT_KEY_ID=K1234567890
|
||||
AUTHENTIK_STORAGE__S3__CLOUDFRONT_KEYPAIR=cloudfront-media-signing
|
||||
AUTHENTIK_STORAGE__S3__OBJECT_ACL=
|
||||
```
|
||||
|
||||
CloudFront signed URLs use the same `AUTHENTIK_STORAGE__S3__URL_EXPIRY` duration as other generated storage URLs.
|
||||
|
||||
`AUTHENTIK_STORAGE__S3__QUERYSTRING_AUTH=false` disables S3 `X-Amz-*` presigned URLs. CloudFront signed viewer URLs still include CloudFront query parameters such as `Expires`, `Signature`, and `Key-Pair-Id`.
|
||||
|
||||
For more options, see the [configuration reference](../../../install-config/configuration/configuration.mdx#s3-storage-backend-settings).
|
||||
@@ -2,6 +2,13 @@
|
||||
title: S3 storage setup
|
||||
---
|
||||
|
||||
authentik can store managed files in an S3 bucket instead of the local `/data` directory.
|
||||
|
||||
Use this page for shared bucket setup. Then configure authentik with one of these guides:
|
||||
|
||||
- [S3 providers and custom domains](standard.md), for private S3 with presigned URLs, S3-compatible providers, and custom domains.
|
||||
- [CloudFront delivery](cloudfront.md), for AWS S3 buckets delivered through CloudFront, with optional CloudFront signed viewer URLs.
|
||||
|
||||
## Preparation
|
||||
|
||||
First, create a user on your S3 storage provider and get access credentials (hereafter referred to as `access_key` and `secret_key`).
|
||||
@@ -14,11 +21,9 @@ The domain you use to access authentik is referred to as `authentik.company` in
|
||||
|
||||
You will also need the AWS CLI available locally.
|
||||
|
||||
## S3 configuration
|
||||
## Bucket creation
|
||||
|
||||
### Bucket creation
|
||||
|
||||
Create the bucket that authentik will use for media files:
|
||||
Create the bucket that authentik will use for managed files:
|
||||
|
||||
```bash
|
||||
AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3api --endpoint-url=https://s3.provider create-bucket --bucket=authentik-data --acl=private
|
||||
@@ -26,9 +31,9 @@ AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3api --endpoi
|
||||
|
||||
If using AWS S3, you can omit `--endpoint-url`, but you may need to specify `--region`. Some regions require `--create-bucket-configuration LocationConstraint=<region>`.
|
||||
|
||||
The bucket ACL is set to private. Depending on your provider you can alternatively disable ACLs and rely on bucket policies.
|
||||
The bucket ACL is set to private. For AWS buckets with Object Ownership set to **Bucket owner enforced**, ACLs are disabled; omit `--acl private` when creating the bucket and set `AUTHENTIK_STORAGE__S3__OBJECT_ACL=` in authentik.
|
||||
|
||||
### Bucket policy
|
||||
## Bucket policy
|
||||
|
||||
The following actions need to be allowed on the bucket:
|
||||
|
||||
@@ -73,11 +78,11 @@ The following policy can be used in AWS:
|
||||
}
|
||||
```
|
||||
|
||||
### CORS policy
|
||||
## CORS policy
|
||||
|
||||
Apply a CORS policy to the bucket, allowing the authentik web interface to access images directly.
|
||||
|
||||
Save the following as `cors.json` (use your deployment’s origin; include scheme and port if non‑standard):
|
||||
Save the following as `cors.json` (use your deployment’s origin; include scheme and port if non-standard):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -100,7 +105,7 @@ Apply the policy to the bucket:
|
||||
AWS_ACCESS_KEY_ID=access_key AWS_SECRET_ACCESS_KEY=secret_key aws s3api --endpoint-url=https://s3.provider put-bucket-cors --bucket=authentik-data --cors-configuration=file://cors.json
|
||||
```
|
||||
|
||||
### Content-Type
|
||||
## Content-Type
|
||||
|
||||
Browsers rely on the HTTP `Content-Type` header to determine how to handle files; render HTML, display an image, or perform another action.
|
||||
|
||||
@@ -120,52 +125,6 @@ aws s3 cp \
|
||||
The `Content-Type` header is not set when files are programmatically uploaded to S3 via Terraform.
|
||||
:::
|
||||
|
||||
### Configuring authentik
|
||||
|
||||
Add the following to your `.env` file:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__BACKEND=s3
|
||||
AUTHENTIK_STORAGE__S3__ACCESS_KEY=access_key
|
||||
AUTHENTIK_STORAGE__S3__SECRET_KEY=secret_key
|
||||
AUTHENTIK_STORAGE__S3__BUCKET_NAME=authentik-data
|
||||
```
|
||||
|
||||
If you are using AWS S3, add:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__REGION=us-east-1 # Use the region of the bucket
|
||||
```
|
||||
|
||||
If you are using an S3‑compatible provider (non‑AWS), add:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__ENDPOINT=https://s3.provider
|
||||
AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=s3.provider/authentik-media
|
||||
```
|
||||
|
||||
If your provider only supports legacy S3 signatures, also set:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__SIGNATURE_VERSION=s3
|
||||
```
|
||||
|
||||
By default, authentik uses signature version `s3v4`.
|
||||
|
||||
The `AUTHENTIK_STORAGE__S3__ENDPOINT` setting controls how authentik communicates with the S3 provider. When set, it overrides region/`USE_SSL`.
|
||||
|
||||
The `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN` setting controls how media URLs are built for the web interface. It must include the bucket name and must not include a scheme.
|
||||
|
||||
For a path-style domain, set `AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=s3.provider/authentik-media`. The object `application-icons/application.png` will be available at `https://s3.provider/authentik-media/application-icons/application.png`.
|
||||
|
||||
Whether URLs use HTTPS is controlled by `AUTHENTIK_STORAGE__S3__SECURE_URLS` (defaults to `true`). Depending on your provider, you can also use a virtual hosted-style domain such as `authentik-data.s3.provider`.
|
||||
|
||||
:::info
|
||||
You can omit `ACCESS_KEY` and `SECRET_KEY` when using AWS SDK authentication (instance roles or profiles). See `AUTHENTIK_STORAGE__S3__SESSION_PROFILE` and related options in the configuration reference](../../install-config/configuration/configuration.mdx#storage-settings).
|
||||
:::
|
||||
|
||||
For more options (including `AUTHENTIK_STORAGE__S3__USE_SSL`, session profiles, and security tokens), see the [configuration reference](../../install-config/configuration/configuration.mdx#storage-settings).
|
||||
|
||||
## Migrating between storage backends
|
||||
|
||||
The following assumes the local storage path is `/data` and the bucket is `authentik-data`. Ensure your `aws` CLI is configured to talk to your provider (add `--endpoint-url` or `--region` as needed).
|
||||
68
website/docs/sys-mgmt/ops/storage-s3/standard.md
Normal file
68
website/docs/sys-mgmt/ops/storage-s3/standard.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: S3 providers and custom domains
|
||||
---
|
||||
|
||||
This page covers the default S3 storage mode: authentik stores objects in S3 and returns S3 URLs to browsers. By default, these URLs are S3 presigned URLs with `X-Amz-*` query parameters.
|
||||
|
||||
Complete the shared [S3 storage setup](index.md) first.
|
||||
|
||||
## Private S3 with presigned URLs
|
||||
|
||||
Add the following to your `.env` file:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__BACKEND=s3
|
||||
AUTHENTIK_STORAGE__S3__ACCESS_KEY=access_key
|
||||
AUTHENTIK_STORAGE__S3__SECRET_KEY=secret_key
|
||||
AUTHENTIK_STORAGE__S3__BUCKET_NAME=authentik-data
|
||||
```
|
||||
|
||||
If you are using AWS S3, add:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__REGION=us-east-1 # Use the region of the bucket
|
||||
```
|
||||
|
||||
This is the default access mode. authentik stores private objects and returns short-lived S3 presigned URLs with `X-Amz-*` query parameters.
|
||||
|
||||
If your provider only supports legacy S3 signatures, also set:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__SIGNATURE_VERSION=s3
|
||||
```
|
||||
|
||||
By default, authentik uses signature version `s3v4`.
|
||||
|
||||
## S3-compatible providers
|
||||
|
||||
If you are using an S3-compatible provider (non-AWS), add:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__ENDPOINT=https://s3.provider
|
||||
```
|
||||
|
||||
`AUTHENTIK_STORAGE__S3__ENDPOINT` controls how authentik communicates with the S3 provider. When set, it overrides `AUTHENTIK_STORAGE__S3__REGION` and `AUTHENTIK_STORAGE__S3__USE_SSL`.
|
||||
|
||||
## Custom domains
|
||||
|
||||
`AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN` controls how file URLs are rendered for browsers. It must not include a scheme.
|
||||
|
||||
For a path-style provider domain, include the bucket in the custom domain:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=s3.provider/authentik-data
|
||||
```
|
||||
|
||||
The object `application-icons/application.png` will be available at `https://s3.provider/authentik-data/media/public/application-icons/application.png`.
|
||||
|
||||
For a virtual-hosted provider domain, use:
|
||||
|
||||
```env
|
||||
AUTHENTIK_STORAGE__S3__CUSTOM_DOMAIN=authentik-data.s3.provider
|
||||
```
|
||||
|
||||
The object will be available at `https://authentik-data.s3.provider/media/public/application-icons/application.png`.
|
||||
|
||||
Whether URLs use HTTPS is controlled by `AUTHENTIK_STORAGE__S3__SECURE_URLS` (defaults to `true`).
|
||||
|
||||
For more options, see the [configuration reference](../../../install-config/configuration/configuration.mdx#s3-storage-backend-settings).
|
||||
Reference in New Issue
Block a user