diff --git a/CHANGELOG.md b/CHANGELOG.md
index 999bc9eb259793f692286753a8003cfc64c57831..e3f61bf099648161aea01066d117a904c6585863 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ## [Unreleased]
 
+### Added
+
+* Added a `federation.ignoredHosts` config option to block media from individual homeservers.
+
 ### Removed
 
 * IPFS support has been removed due to maintenance burden.
diff --git a/api/_responses/errors.go b/api/_responses/errors.go
index deb93b29af320d492c13bdeb4234769c9c02e938..1cee50216ea8075e8b2f45b993130472226f421a 100644
--- a/api/_responses/errors.go
+++ b/api/_responses/errors.go
@@ -40,6 +40,10 @@ func AuthFailed() *ErrorResponse {
 	return &ErrorResponse{common.ErrCodeUnknownToken, "Authentication Failed", common.ErrCodeUnknownToken}
 }
 
+func MediaBlocked() *ErrorResponse {
+	return &ErrorResponse{common.ErrCodeNotFound, "Media blocked or not found", common.ErrCodeForbidden}
+}
+
 func GuestAuthFailed() *ErrorResponse {
 	return &ErrorResponse{common.ErrCodeNoGuests, "Guests cannot use this endpoint", common.ErrCodeNoGuests}
 }
diff --git a/api/r0/download.go b/api/r0/download.go
index 5b988220de6df5399e445e0ebd471356bd9af2a1..83b9232a97c5d787c3a9b346cbe7680305378829 100644
--- a/api/r0/download.go
+++ b/api/r0/download.go
@@ -8,6 +8,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/_apimeta"
 	"github.com/turt2live/matrix-media-repo/api/_responses"
 	"github.com/turt2live/matrix-media-repo/api/_routers"
+	"github.com/turt2live/matrix-media-repo/util"
 
 	"github.com/sirupsen/logrus"
 	"github.com/turt2live/matrix-media-repo/common"
@@ -52,6 +53,11 @@ func DownloadMedia(r *http.Request, rctx rcontext.RequestContext, user _apimeta.
 		"allowRemote": downloadRemote,
 	})
 
+	if !util.IsGlobalAdmin(user.UserId) && util.IsHostIgnored(server) {
+		rctx.Log.Warn("Request blocked due to domain being ignored.")
+		return _responses.MediaBlocked()
+	}
+
 	streamedMedia, err := download_controller.GetMedia(server, mediaId, downloadRemote, false, rctx)
 	if err != nil {
 		if err == common.ErrMediaNotFound {
diff --git a/api/r0/thumbnail.go b/api/r0/thumbnail.go
index 7e56131e9fccb8de2c4143c7bc2fb2db6fc04426..86694315dad38f2ac079191c71d52808081b62e2 100644
--- a/api/r0/thumbnail.go
+++ b/api/r0/thumbnail.go
@@ -5,6 +5,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/_apimeta"
 	"github.com/turt2live/matrix-media-repo/api/_responses"
 	"github.com/turt2live/matrix-media-repo/api/_routers"
+	"github.com/turt2live/matrix-media-repo/util"
 
 	"net/http"
 	"strconv"
@@ -39,6 +40,11 @@ func ThumbnailMedia(r *http.Request, rctx rcontext.RequestContext, user _apimeta
 		"allowRemote": downloadRemote,
 	})
 
+	if !util.IsGlobalAdmin(user.UserId) && util.IsHostIgnored(server) {
+		rctx.Log.Warn("Request blocked due to domain being ignored.")
+		return _responses.MediaBlocked()
+	}
+
 	widthStr := r.URL.Query().Get("width")
 	heightStr := r.URL.Query().Get("height")
 	method := r.URL.Query().Get("method")
diff --git a/api/unstable/info.go b/api/unstable/info.go
index e47542e32b159b5f7074f01329e15fc9afdb4cba..3bdafb245ea1fc52c199886b36446109bbbe8572 100644
--- a/api/unstable/info.go
+++ b/api/unstable/info.go
@@ -12,6 +12,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/_apimeta"
 	"github.com/turt2live/matrix-media-repo/api/_responses"
 	"github.com/turt2live/matrix-media-repo/api/_routers"
+	"github.com/turt2live/matrix-media-repo/util"
 	"github.com/turt2live/matrix-media-repo/util/stream_util"
 
 	"github.com/disintegration/imaging"
@@ -73,6 +74,11 @@ func MediaInfo(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User
 		"allowRemote": downloadRemote,
 	})
 
+	if !util.IsGlobalAdmin(user.UserId) && util.IsHostIgnored(server) {
+		rctx.Log.Warn("Request blocked due to domain being ignored.")
+		return _responses.MediaBlocked()
+	}
+
 	streamedMedia, err := download_controller.GetMedia(server, mediaId, downloadRemote, true, rctx)
 	if err != nil {
 		if err == common.ErrMediaNotFound {
diff --git a/api/unstable/local_copy.go b/api/unstable/local_copy.go
index 74166e60af6778a7cf350aaeea6bbdc54d2d3b2e..fa5a1f03d8b383332ab8e6547e4280e8e765e38e 100644
--- a/api/unstable/local_copy.go
+++ b/api/unstable/local_copy.go
@@ -5,6 +5,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/_apimeta"
 	"github.com/turt2live/matrix-media-repo/api/_responses"
 	"github.com/turt2live/matrix-media-repo/api/_routers"
+	"github.com/turt2live/matrix-media-repo/util"
 	"github.com/turt2live/matrix-media-repo/util/stream_util"
 
 	"net/http"
@@ -42,6 +43,11 @@ func LocalCopy(r *http.Request, rctx rcontext.RequestContext, user _apimeta.User
 		"allowRemote": downloadRemote,
 	})
 
+	if !util.IsGlobalAdmin(user.UserId) && util.IsHostIgnored(server) {
+		rctx.Log.Warn("Request blocked due to domain being ignored.")
+		return _responses.MediaBlocked()
+	}
+
 	// TODO: There's a lot of room for improvement here. Instead of re-uploading media, we should just update the DB.
 
 	streamedMedia, err := download_controller.GetMedia(server, mediaId, downloadRemote, true, rctx)
diff --git a/common/config/models_main.go b/common/config/models_main.go
index bde7479dd82ab2a4b56954f7b4b8086e76b3e655..d8f5948aaa41de78d8c99f755b3b8243aedb268d 100644
--- a/common/config/models_main.go
+++ b/common/config/models_main.go
@@ -64,7 +64,8 @@ type SharedSecretConfig struct {
 }
 
 type FederationConfig struct {
-	BackoffAt int `yaml:"backoffAt"`
+	BackoffAt    int      `yaml:"backoffAt"`
+	IgnoredHosts []string `yaml:"ignoredHosts,flow"`
 }
 
 type PluginConfig struct {
diff --git a/config.sample.yaml b/config.sample.yaml
index 9837491e6ecb6540a82693c56b88feca75ab0bdd..e3570bb466fda47ce249774f9748f04a6ce54fbc 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -41,6 +41,18 @@ federation:
   # the remote server do not count towards this.
   backoffAt: 20
 
+  # The domains the media repo should never serve media for. Existing media already stored from
+  # these domains will remain, however will not be downloadable without a data export. Media
+  # repo administrators will bypass this check. Admin APIs will still work for media on these
+  # domains.
+  #
+  # This will not prevent the listed domains from accessing media on this media repo - it only
+  # stops users on *this* media repo from accessing media originally uploaded to the listed domains.
+  #
+  # Note: Adding domains controlled by the media repo itself to this list is not advisable.
+  ignoredHosts:
+    - example.org
+
 # The database configuration for the media repository
 # Do NOT put your homeserver's existing database credentials here. Create a new database and
 # user instead. Using the same server is fine, just not the same username and database.
diff --git a/util/config.go b/util/config.go
index 18bd40e1feedeaa8a1b67b56a20916a3c95a6ea5..306495da2906d569430cacc3225f9cf731473281 100644
--- a/util/config.go
+++ b/util/config.go
@@ -1,6 +1,8 @@
 package util
 
 import (
+	"strings"
+
 	"github.com/turt2live/matrix-media-repo/common/config"
 )
 
@@ -18,3 +20,13 @@ func IsGlobalAdmin(userId string) bool {
 
 	return false
 }
+
+func IsHostIgnored(serverName string) bool {
+	serverName = strings.ToLower(serverName)
+	for _, host := range config.Get().Federation.IgnoredHosts {
+		if strings.ToLower(host) == serverName {
+			return true
+		}
+	}
+	return false
+}