diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index c118d9790fdeebe7d94906e1d045ea455a1cc8ee..5e39c1af4d38f15985c4ac13e6348a3130702391 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -312,3 +312,7 @@ rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate)
 
 edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_default_dashboard")
 rules.add_perm("core.edit_default_dashboard", edit_default_dashboard_predicate)
+
+# Upload and browse files via CKEditor
+upload_files_ckeditor_predicate = has_person & has_global_perm("core.upload_files_ckeditor")
+rules.add_perm("core.upload_files_ckeditor", upload_files_ckeditor_predicate)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index e40f4bc0be352211edabdfd7300f6946b9e0dacf..c3705b3cacd498a849f05009ad72c610bfb2b894 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -113,6 +113,7 @@ INSTALLED_APPS = [
     "material",
     "pwa",
     "ckeditor",
+    "ckeditor_uploader",
     "django_js_reverse",
     "colorfield",
     "django_bleach",
@@ -653,6 +654,9 @@ CKEDITOR_CONFIGS = {
     }
 }
 
+# Upload path for CKEditor. Relative to MEDIA_ROOT.
+CKEDITOR_UPLOAD_PATH = "ckeditor_uploads/"
+
 # Which HTML tags are allowed
 BLEACH_ALLOWED_TAGS = ["p", "b", "i", "u", "em", "strong", "a", "div"]
 
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 4b0871da91a37115477a06f36b637cae6fa54560..eb5c6c41ac26173ebe81df366b19acf80d6ed234 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -8,8 +8,10 @@ from django.views.i18n import JavaScriptCatalog
 
 import calendarweek.django
 import debug_toolbar
+from ckeditor_uploader import views as ckeditor_uploader_views
 from django_js_reverse.views import urls_js
 from health_check.urls import urlpatterns as health_urls
+from rules.contrib.views import permission_required
 from two_factor.urls import urlpatterns as tf_urls
 
 from . import views
@@ -81,6 +83,16 @@ urlpatterns = [
     path("maintenance-mode/", include("maintenance_mode.urls")),
     path("impersonate/", include("impersonate.urls")),
     path("__i18n__/", include("django.conf.urls.i18n")),
+    path(
+        "ckeditor/upload/",
+        permission_required("core.ckeditor_upload_files")(ckeditor_uploader_views.upload),
+        name="ckeditor_upload",
+    ),
+    path(
+        "ckeditor/browse/",
+        permission_required("core.ckeditor_upload_files")(ckeditor_uploader_views.browse),
+        name="ckeditor_browse",
+    ),
     path("select2/", include("django_select2.urls")),
     path("jsreverse.js", urls_js, name="js_reverse"),
     path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"),