diff --git a/config/config.exs b/config/config.exs
index e82c490e3..9cc558564 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -176,6 +176,13 @@ config :pleroma, :suggestions,
   limit: 23,
   web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
 
+config :pleroma, :http_security,
+  enabled: true,
+  sts: false,
+  sts_max_age: 31_536_000,
+  ct_max_age: 2_592_000,
+  referrer_policy: "same-origin"
+
 config :cors_plug,
   max_age: 86_400,
   methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
diff --git a/config/config.md b/config/config.md
index 51172fc4d..5b4110646 100644
--- a/config/config.md
+++ b/config/config.md
@@ -80,3 +80,10 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
 * ``unfollow_blocked``: Whether blocks result in people getting unfollowed
 * ``outgoing_blocks``: Whether to federate blocks to other instances
 * ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
+
+## :http_security
+* ``enabled``: Whether the managed content security policy is enabled
+* ``sts``: Whether to additionally send a `Strict-Transport-Security` header
+* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
+* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
+* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.
diff --git a/installation/caddyfile-pleroma.example b/installation/caddyfile-pleroma.example
index 305f2aa79..03ff000b6 100644
--- a/installation/caddyfile-pleroma.example
+++ b/installation/caddyfile-pleroma.example
@@ -21,28 +21,6 @@ example.tld  {
     ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256
   }
 
-  header / {
-    X-XSS-Protection "1; mode=block"
-    X-Frame-Options "DENY"
-    X-Content-Type-Options "nosniff"
-    Referrer-Policy "same-origin"
-    Strict-Transport-Security "max-age=31536000; includeSubDomains;"
-    Expect-CT "enforce, max-age=2592000"
-    Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://{host}; upgrade-insecure-requests;"
-  }
-
-  # If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines.
-  # If you want to allow all origins access, remove the origin lines.
-  # To use this directive, you need the http.cors plugin for Caddy.
-  cors / {
-    origin https://halcyon.example.tld
-    origin https://pinafore.example.tld
-    methods POST,PUT,DELETE,GET,PATCH,OPTIONS
-    allowed_headers Authorization,Content-Type,Idempotency-Key
-    exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id
-  }
-  # Stop removing lines here.
-
   # If you do not want to use the mediaproxy function, remove these lines.
   # To use this directive, you need the http.cache plugin for Caddy.
   cache {
diff --git a/installation/pleroma-apache.conf b/installation/pleroma-apache.conf
index fb777983e..d5e75044f 100644
--- a/installation/pleroma-apache.conf
+++ b/installation/pleroma-apache.conf
@@ -34,15 +34,6 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
     SSLCompression          off
     SSLSessionTickets       off
 
-    Header always set X-Xss-Protection "1; mode=block"
-    Header always set X-Frame-Options "DENY"
-    Header always set X-Content-Type-Options "nosniff"
-    Header always set Referrer-Policy same-origin
-    Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://${servername}; upgrade-insecure-requests;"
-
-    # Uncomment this only after you get HTTPS working.
-    # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
-
     RewriteEngine On
     RewriteCond %{HTTP:Connection} Upgrade [NC]
     RewriteCond %{HTTP:Upgrade} websocket [NC]
diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx
index 9b7419497..f0e684f2c 100644
--- a/installation/pleroma.nginx
+++ b/installation/pleroma.nginx
@@ -60,17 +60,6 @@ server {
     client_max_body_size 16m;
 
     location / {
-        add_header X-XSS-Protection "1; mode=block" always;
-        add_header X-Permitted-Cross-Domain-Policies "none" always;
-        add_header X-Frame-Options "DENY" always;
-        add_header X-Content-Type-Options "nosniff" always;
-        add_header Referrer-Policy "same-origin" always;
-        add_header X-Download-Options "noopen" always;
-        add_header Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action *; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://$server_name; upgrade-insecure-requests;" always;
-
-        # Uncomment this only after you get HTTPS working.
-        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl
index 74490be2a..63c1cb74d 100644
--- a/installation/pleroma.vcl
+++ b/installation/pleroma.vcl
@@ -119,13 +119,3 @@ sub vcl_pipe {
         set bereq.http.connection = req.http.connection;
     }
 }
-
-sub vcl_deliver {
-  set resp.http.X-Frame-Options = "DENY";
-  set resp.http.X-XSS-Protection = "1; mode=block";
-  set resp.http.X-Content-Type-Options = "nosniff";
-  set resp.http.Referrer-Policy = "same-origin";
-  set resp.http.Content-Security-Policy = "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://" + req.http.host + "; upgrade-insecure-requests;";
-  # Uncomment this only after you get HTTPS working.
-  # set resp.http.Strict-Transport-Security= "max-age=31536000; includeSubDomains";
-}
diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/sample_config.eex
index 3881ead26..462c34636 100644
--- a/lib/mix/tasks/sample_config.eex
+++ b/lib/mix/tasks/sample_config.eex
@@ -25,6 +25,10 @@ config :pleroma, Pleroma.Repo,
   hostname: "localhost",
   pool_size: 10
 
+# Enable Strict-Transport-Security once SSL is working:
+# config :pleroma, :http_security,
+#   sts: true
+
 # Configure S3 support if desired.
 # The public S3 endpoint is different depending on region and provider,
 # consult your S3 provider's documentation for details on what to use.
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
new file mode 100644
index 000000000..960c7f6bf
--- /dev/null
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -0,0 +1,59 @@
+defmodule Pleroma.Plugs.HTTPSecurityPlug do
+  alias Pleroma.Config
+  import Plug.Conn
+
+  def init(opts), do: opts
+
+  def call(conn, options) do
+    if Config.get([:http_security, :enabled]) do
+      conn =
+        merge_resp_headers(conn, headers())
+        |> maybe_send_sts_header(Config.get([:http_security, :sts]))
+    else
+      conn
+    end
+  end
+
+  defp headers do
+    referrer_policy = Config.get([:http_security, :referrer_policy])
+
+    [
+      {"x-xss-protection", "1; mode=block"},
+      {"x-permitted-cross-domain-policies", "none"},
+      {"x-frame-options", "DENY"},
+      {"x-content-type-options", "nosniff"},
+      {"referrer-policy", referrer_policy},
+      {"x-download-options", "noopen"},
+      {"content-security-policy", csp_string() <> ";"}
+    ]
+  end
+
+  defp csp_string do
+    [
+      "default-src 'none'",
+      "base-uri 'self'",
+      "form-action *",
+      "frame-ancestors 'none'",
+      "img-src 'self' data: https:",
+      "media-src 'self' https:",
+      "style-src 'self' 'unsafe-inline'",
+      "font-src 'self'",
+      "script-src 'self'",
+      "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
+      "upgrade-insecure-requests"
+    ]
+    |> Enum.join("; ")
+  end
+
+  defp maybe_send_sts_header(conn, true) do
+    max_age_sts = Config.get([:http_security, :sts_max_age])
+    max_age_ct = Config.get([:http_security, :ct_max_age])
+
+    merge_resp_headers(conn, [
+      {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"},
+      {"expect-ct", "enforce, max-age=#{max_age_ct}"}
+    ])
+  end
+
+  defp maybe_send_sts_header(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index cb5de087b..7783b8e5c 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.Endpoint do
   # You should set gzip to true if you are running phoenix.digest
   # when deploying your static files in production.
   plug(CORSPlug)
+  plug(Pleroma.Plugs.HTTPSecurityPlug)
 
   plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
 
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
new file mode 100644
index 000000000..55040a108
--- /dev/null
+++ b/test/plugs/http_security_plug_test.exs
@@ -0,0 +1,77 @@
+defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
+  use Pleroma.Web.ConnCase
+  alias Pleroma.Config
+  alias Plug.Conn
+
+  test "it sends CSP headers when enabled", %{conn: conn} do
+    Config.put([:http_security, :enabled], true)
+
+    conn =
+      conn
+      |> get("/api/v1/instance")
+
+    refute Conn.get_resp_header(conn, "x-xss-protection") == []
+    refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
+    refute Conn.get_resp_header(conn, "x-frame-options") == []
+    refute Conn.get_resp_header(conn, "x-content-type-options") == []
+    refute Conn.get_resp_header(conn, "x-download-options") == []
+    refute Conn.get_resp_header(conn, "referrer-policy") == []
+    refute Conn.get_resp_header(conn, "content-security-policy") == []
+  end
+
+  test "it does not send CSP headers when disabled", %{conn: conn} do
+    Config.put([:http_security, :enabled], false)
+
+    conn =
+      conn
+      |> get("/api/v1/instance")
+
+    assert Conn.get_resp_header(conn, "x-xss-protection") == []
+    assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
+    assert Conn.get_resp_header(conn, "x-frame-options") == []
+    assert Conn.get_resp_header(conn, "x-content-type-options") == []
+    assert Conn.get_resp_header(conn, "x-download-options") == []
+    assert Conn.get_resp_header(conn, "referrer-policy") == []
+    assert Conn.get_resp_header(conn, "content-security-policy") == []
+  end
+
+  test "it sends STS headers when enabled", %{conn: conn} do
+    Config.put([:http_security, :enabled], true)
+    Config.put([:http_security, :sts], true)
+
+    conn =
+      conn
+      |> get("/api/v1/instance")
+
+    refute Conn.get_resp_header(conn, "strict-transport-security") == []
+    refute Conn.get_resp_header(conn, "expect-ct") == []
+  end
+
+  test "it does not send STS headers when disabled", %{conn: conn} do
+    Config.put([:http_security, :enabled], true)
+    Config.put([:http_security, :sts], false)
+
+    conn =
+      conn
+      |> get("/api/v1/instance")
+
+    assert Conn.get_resp_header(conn, "strict-transport-security") == []
+    assert Conn.get_resp_header(conn, "expect-ct") == []
+  end
+
+  test "referrer-policy header reflects configured value", %{conn: conn} do
+    conn =
+      conn
+      |> get("/api/v1/instance")
+
+    assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"]
+
+    Config.put([:http_security, :referrer_policy], "no-referrer")
+
+    conn =
+      build_conn()
+      |> get("/api/v1/instance")
+
+    assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"]
+  end
+end