~jan0sch/smederee

Showing details for patch 85356c9326a1104b5f3a625f38b209f63b0ef87a.
2022-11-22 (Tue), 3:11 PM - Jens Grassel - 85356c9326a1104b5f3a625f38b209f63b0ef87a

CSRF: Store the CSRF key on disk and load it if present.

Because the key was generated on every server restart and the fallback logic
of redirecting users to the front page while deleting the cookie was

- annoying
- not working behind reverse proxies

the key is now stored on disk. If the file is found then the key is loaded
from it. If not then a new key is generated and written to the file.
The current logic writes the file either way (so it re-writes a loaded key)
to simplify branching logic.
Summary of changes
4 files modified with 41 lines added and 3 lines removed
  • CHANGELOG.md with 4 added and 0 removed lines
  • modules/hub/src/main/resources/reference.conf with 4 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/HubServer.scala with 27 added and 2 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala with 6 added and 1 removed lines
diff -rN -u old-smederee/CHANGELOG.md new-smederee/CHANGELOG.md
--- old-smederee/CHANGELOG.md	2025-02-01 13:03:32.204941216 +0000
+++ new-smederee/CHANGELOG.md	2025-02-01 13:03:32.204941216 +0000
@@ -20,6 +20,10 @@
 
 ## Unreleased
 
+### Changed
+
+- store the CSRF key on disk and load it if present
+
 ## 0.3.0 (2022-11-16)
 
 ### Added
diff -rN -u old-smederee/modules/hub/src/main/resources/reference.conf new-smederee/modules/hub/src/main/resources/reference.conf
--- old-smederee/modules/hub/src/main/resources/reference.conf	2025-02-01 13:03:32.204941216 +0000
+++ new-smederee/modules/hub/src/main/resources/reference.conf	2025-02-01 13:03:32.204941216 +0000
@@ -30,6 +30,10 @@
   # files of repositories).
   download-directory = /var/tmp/smederee/download
   download-directory = ${?SMEDEREE_DOWNLOAD_DIR}
+  # A file which contains the key used to build the CSRF protection.
+  # If it does not exist then it should be created with sensible permissions.
+  csrf-key-file = /var/tmp/smederee/csrf-key.bin
+  csrf-key-file = ${?SMEDEREE_CSRF_KEY_FILE}
 
   # Settings affecting how the service will communicate several information to
   # the "outside world" e.g. if it runs behind a reverse proxy.
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-02-01 13:03:32.204941216 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-02-01 13:03:32.204941216 +0000
@@ -265,6 +265,9 @@
   *   The hostname on which the service shall listen for requests.
   * @param port
   *   The TCP port number on which the service shall listen for requests.
+  * @param csrfKeyFile
+  *   A file which contains the key used to build the CSRF protection. If it does not exist then it should be created
+  *   with sensible permissions.
   * @param downloadDirectory
   *   A directory into which files are written that are supposed to be downloaded by users (e.g. distribution files of
   *   repositories).
@@ -287,6 +290,7 @@
 final case class ServiceConfig(
     host: Host,
     port: Port,
+    csrfKeyFile: Path,
     downloadDirectory: DirectoryPath,
     authentication: AuthenticationConfiguration,
     billing: BillingConfiguration,
@@ -318,9 +322,10 @@
     )
 
   given ConfigReader[ServiceConfig] =
-    ConfigReader.forProduct10(
+    ConfigReader.forProduct11(
       "host",
       "port",
+      "csrf-key-file",
       "download-directory",
       AuthenticationConfiguration.parentKey.toString,
       BillingConfiguration.parentKey.toString,
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 13:03:32.204941216 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 13:03:32.204941216 +0000
@@ -18,7 +18,7 @@
 package de.smederee.hub
 
 import java.nio.charset.StandardCharsets
-import java.nio.file._
+import java.nio.file.Files
 import java.nio.file.attribute.PosixFilePermissions
 
 import cats.arrow.FunctionK
@@ -42,6 +42,7 @@
 import org.http4s.server.staticcontent.resourceServiceBuilder
 import org.slf4j.LoggerFactory
 import pureconfig._
+import scodec.bits.ByteVector
 
 /** This is the main entry point for the hub service.
   *
@@ -63,6 +64,30 @@
     CSRF.defaultOriginCheck(request, linkConfig.host.toString, linkConfig.scheme, linkConfig.port.map(_.value))
   }
 
+  /** Try to load the CSRF key from the given path. If it doesn't exist or fails then a new key is generated and stored
+    * in the file.
+    *
+    * @param csrfKeyFile
+    *   A file which contains the key used to build the CSRF protection.
+    * @return
+    *   A byte vector containing the key used for CSRF protection.
+    */
+  private def loadOrCreateCsrfKey(csrfKeyFile: java.nio.file.Path): IO[ByteVector] =
+    for {
+      _ <- IO(
+        Files.createDirectories(
+          csrfKeyFile.getParent,
+          PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))
+        )
+      )
+      key <- Files.exists(csrfKeyFile) match {
+        case false => CSRF.generateSigningKey[IO]()
+        case true  => IO(ByteVector(Files.readAllBytes(csrfKeyFile)))
+      }
+      _ <- IO(Files.write(csrfKeyFile, key.toArray))
+      _ <- IO(Files.setPosixFilePermissions(csrfKeyFile, PosixFilePermissions.fromString("rw-------")))
+    } yield key
+
   def run(args: List[String]): IO[ExitCode] = {
     val databaseMigrator = new DatabaseMigrator[IO]
     for {
@@ -107,7 +132,7 @@
         configuration.database.pass
       )
       cryptoClock = java.time.Clock.systemUTC
-      csrfKey <- CSRF.generateSigningKey[IO]()
+      csrfKey <- loadOrCreateCsrfKey(configuration.service.csrfKeyFile)
       csrfOriginCheck = createCsrfOriginCheck(configuration.service.external)
       csrfBuilder     = CSRF[IO, IO](csrfKey, csrfOriginCheck)
       /* The idea behind the `onFailure` part of the CSRF protection middleware is