diff --git a/flake.nix b/flake.nix
index 0869c01..bb2a69d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -34,6 +34,7 @@
       allModules = [
         modules/blocks/authelia.nix
         modules/blocks/davfs.nix
+        modules/blocks/hardcodedsecret.nix
         modules/blocks/ldap.nix
         modules/blocks/monitoring.nix
         modules/blocks/nginx.nix
diff --git a/lib/default.nix b/lib/default.nix
index d55debf..9e9b004 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,7 +1,7 @@
 { pkgs, lib }:
 let
   inherit (builtins) isAttrs hasAttr;
-  inherit (lib) concatStringsSep;
+  inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList;
 in
 rec {
   # Replace secrets in a file.
@@ -34,14 +34,30 @@ rec {
   replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }:
     let
       templatePath = resultPath + ".template";
-      sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
-      sedCmd = if replacements == {}
+
+      t = { transform ? null, ... }: if isNull transform then x: x else transform;
+
+      genReplacement = secret:
+        lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
+
+      # We check that the files containing the secrets have the
+      # correct permissions for us to read them in this separate
+      # step. Otherwise, the $(cat ...) commands inside the sed
+      # replacements could fail but not fail individually but
+      # not fail the whole script.
+      checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements;
+
+      sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements);
+
+      sedCmd = if replacements == []
                then "cat"
                else "${pkgs.gnused}/bin/sed ${sedPatterns}";
     in
       ''
       set -euo pipefail
 
+      ${checkPermissions}
+
       mkdir -p $(dirname ${templatePath})
       ln -fs ${file} ${templatePath}
       rm -f ${resultPath}
@@ -71,8 +87,8 @@ rec {
     };
   };
 
-  secretName = name:
-      "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) name)}%";
+  secretName = names:
+    "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
 
   withReplacements = attrs:
     let
@@ -91,15 +107,8 @@ rec {
         else value // { name = name; };
 
       secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
-
-      allSecrets = collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
-
-      t = { transform ? null, ... }: if isNull transform then x: x else transform;
-
-      genReplacement = secret:
-        lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
     in
-      lib.attrsets.listToAttrs (map genReplacement allSecrets);
+      collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
       
   # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
   mapAttrsRecursiveCond =
@@ -238,7 +247,7 @@ rec {
       results = pkgs.lib.runTests tests;
     in
     if results != [ ] then
-      builtins.throw (builtins.concatStringsSep "\n" (map resultToString (lib.traceValSeqN 3 results)))
+      builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeqN 3 results)))
     else
       pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
 
diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix
index d27db34..54df73d 100644
--- a/modules/blocks/authelia.nix
+++ b/modules/blocks/authelia.nix
@@ -67,33 +67,45 @@ in
       description = "Secrets needed by Authelia";
       type = lib.types.submodule {
         options = {
-          jwtSecretFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the JWT secret.";
+          jwtSecret = contracts.secret.mkOption {
+            description = "JWT secret.";
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
-          ldapAdminPasswordFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the LDAP admin user password.";
+          ldapAdminPassword = contracts.secret.mkOption {
+            description = "LDAP admin user password.";
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
-          sessionSecretFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the session secret.";
+          sessionSecret = contracts.secret.mkOption {
+            description = "Session secret.";
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
-          storageEncryptionKeyFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the storage encryption key.";
+          storageEncryptionKey = contracts.secret.mkOption {
+            description = "Storage encryption key.";
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
-          identityProvidersOIDCHMACSecretFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the identity provider OIDC HMAC secret.";
+          identityProvidersOIDCHMACSecret = contracts.secret.mkOption {
+            description = "Identity provider OIDC HMAC secret.";
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
-          identityProvidersOIDCIssuerPrivateKeyFile = lib.mkOption {
-            type = lib.types.path;
+          identityProvidersOIDCIssuerPrivateKey = contracts.secret.mkOption {
             description = ''
-              File containing the identity provider OIDC issuer private key.
+              Identity provider OIDC issuer private key.
 
               Generate one with `nix run nixpkgs#openssl -- genrsa -out keypair.pem 2048`
             '';
+            mode = "0400";
+            owner = cfg.autheliaUser;
+            restartUnits = [ "authelia-${fqdn}" ];
           };
         };
       };
@@ -207,9 +219,11 @@ in
               type = lib.types.str;
               description = "Username to connect to the SMTP host.";
             };
-            passwordFile = lib.mkOption {
-              type = lib.types.str;
+            password = contracts.secret.mkOption {
               description = "File containing the password to connect to the SMTP host.";
+              mode = "0400";
+              owner = cfg.autheliaUser;
+              restartUnits = [ "authelia-${fqdn}" ];
             };
           };
         }))
@@ -282,19 +296,20 @@ in
       user = cfg.autheliaUser;
 
       secrets = {
-        inherit (cfg.secrets) jwtSecretFile storageEncryptionKeyFile;
+        jwtSecretFile = cfg.secrets.jwtSecret.result.path;
+        storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
       };
       # See https://www.authelia.com/configuration/methods/secrets/
       environmentVariables = {
-        AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPasswordFile;
-        AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecretFile;
+        AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path;
+        AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecret.result.path;
         # Not needed since we use peer auth.
         # AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
-        AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKeyFile;
-        AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecretFile;
-        AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKeyFile;
+        AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKey.result.path;
+        AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
+        AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
 
-        AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.passwordFile);
+        AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
       };
       settings = {
         server.address = "tcp://127.0.0.1:9091";
diff --git a/modules/blocks/hardcodedsecret.nix b/modules/blocks/hardcodedsecret.nix
new file mode 100644
index 0000000..6e62cef
--- /dev/null
+++ b/modules/blocks/hardcodedsecret.nix
@@ -0,0 +1,95 @@
+{ config, options, lib, pkgs, ... }:
+let
+  cfg = config.shb.hardcodedsecret;
+  opt = options.shb.hardcodedsecret;
+
+  inherit (lib) mapAttrs' mkOption nameValuePair;
+  inherit (lib.types) attrsOf listOf path nullOr str submodule;
+  inherit (pkgs) writeText;
+in
+{
+  options.shb.hardcodedsecret = mkOption {
+    default = {};
+    type = attrsOf (submodule ({ name, ... }: {
+      options = {
+        mode = mkOption {
+          description = ''
+            Mode of the secret file.
+          '';
+          type = str;
+          default = "0400";
+        };
+
+        owner = mkOption {
+          description = ''
+            Linux user owning the secret file.
+          '';
+          type = str;
+          default = "root";
+        };
+
+        group = mkOption {
+          description = ''
+            Linux group owning the secret file.
+          '';
+          type = str;
+          default = "root";
+        };
+
+        restartUnits = mkOption {
+          description = ''
+            Systemd units to restart after the secret is updated.
+          '';
+          type = listOf str;
+          default = [];
+        };
+
+        path = mkOption {
+          type = path;
+          description = ''
+            Path to the file containing the secret generated out of band.
+
+            This path will exist after deploying to a target host,
+            it is not available through the nix store.
+          '';
+          default = "/run/hardcodedsecrets/hardcodedsecret_${name}";
+        };
+
+        content = mkOption {
+          type = nullOr str;
+          description = ''
+            Content of the secret.
+
+            This will be stored in the nix store and should only be used for testing or maybe in dev.
+          '';
+          default = null;
+        };
+
+        source = mkOption {
+          type = nullOr str;
+          description = ''
+            Source of the content of the secret.
+          '';
+          default = null;
+        };
+      };
+    }));
+  };
+
+  config = {
+    system.activationScripts = mapAttrs' (n: cfg':
+      let
+        source = if cfg'.source != null
+                 then cfg'.source
+                 else writeText "hardcodedsecret_${n}_content" cfg'.content;
+      in
+        nameValuePair "hardcodedsecret_${n}" ''
+          mkdir -p "$(dirname "${cfg'.path}")"
+          touch "${cfg'.path}"
+          chmod ${cfg'.mode} "${cfg'.path}"
+          chown ${cfg'.owner}:${cfg'.group} "${cfg'.path}"
+          cp ${source} "${cfg'.path}"
+        ''
+    ) cfg;
+  };
+}
diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix
index 74c048e..96fb453 100644
--- a/modules/services/jellyfin.nix
+++ b/modules/services/jellyfin.nix
@@ -67,9 +67,12 @@ in
             default = "jellyfin_admin";
           };
 
-          passwordFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the LDAP admin password.";
+          passwordFile = contracts.secret.mkOption {
+            description = "LDAP admin password.";
+            mode = "0440";
+            owner = "jellyfin";
+            group = "jellyfin";
+            restartUnits = [ "jellyfin.service" ];
           };
         };
       };
@@ -118,9 +121,18 @@ in
             default = "one_factor";
           };
 
-          secretFile = lib.mkOption {
-            type = lib.types.path;
-            description = "File containing the OIDC shared secret.";
+          secretFile = contracts.secret.mkOption {
+            description = "OIDC shared secret for Jellyfin.";
+            mode = "0440";
+            owner = "jellyfin";
+            group = "jellyfin";
+            restartUnits = [ "jellyfin.service" ];
+          };
+
+          secretFileAuthelia = contracts.secret.mkOption {
+            description = "OIDC shared secret for Authelia.";
+            mode = "0400";
+            owner = config.shb.authelia.autheliaUser;
           };
         };
       };
@@ -400,30 +412,35 @@ in
         lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript {
           file = ldapConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
-          replacements = {
-            "%LDAP_PASSWORD%" = "$(cat ${cfg.ldap.passwordFile})";
-          };
+          replacements = [
+            {
+              name = [ "%LDAP_PASSWORD%" ];
+              source = cfg.ldap.passwordFile.result.path;
+            }
+          ];
         })
         + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
           file = ssoConfig;
           resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
-          replacements = {
-            "%SSO_SECRET%" = "$(cat ${cfg.sso.secretFile})";
-          };
+          replacements = [
+            {
+              name = [ "%SSO_SECRET%" ];
+              source = cfg.sso.secretFile.result.path;
+            }
+          ];
         })
         + lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
           file = brandingConfig;
           resultPath = "/var/lib/jellyfin/config/branding.xml";
-          replacements = {
-            "%a%" = "%a%";
-          };
+          replacements = [
+          ];
         });
 
     shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [
       {
         client_id = cfg.sso.clientID;
         client_name = "Jellyfin";
-        client_secret.source = cfg.sso.secretFile;
+        client_secret.source = cfg.sso.secretFileAuthelia.result.path;
         public = false;
         authorization_policy = cfg.sso.authorization_policy;
         redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
diff --git a/test/common.nix b/test/common.nix
index 40d512f..8e5f2c2 100644
--- a/test/common.nix
+++ b/test/common.nix
@@ -1,6 +1,4 @@
-{
-  lib,
-}:
+{ lib }:
 let
   baseImports = pkgs: [
     (pkgs.path + "/nixos/modules/profiles/headless.nix")
@@ -109,6 +107,7 @@ in
         ../modules/blocks/postgresql.nix
         ../modules/blocks/authelia.nix
         ../modules/blocks/nginx.nix
+        ../modules/blocks/hardcodedsecret.nix
       ]
       ++ additionalModules;
 
@@ -138,7 +137,7 @@ in
     systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
   };
 
-  ldap = domain: pkgs: {
+  ldap = domain: pkgs: { config, ... }: {
     imports = [
       ../modules/blocks/ldap.nix
     ];
@@ -147,6 +146,13 @@ in
       "127.0.0.1" = [ "ldap.${domain}" ];
     };
 
+    shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // {
+      content = "ldapUserPassword";
+    };
+    shb.hardcodedsecret.jwtSecret = config.shb.ldap.ldapUserPassword.request // {
+      content = "jwtSecrets";
+    };
+
     shb.ldap = {
       enable = true;
       inherit domain;
@@ -154,8 +160,8 @@ in
       ldapPort = 3890;
       webUIListenPort = 17170;
       dcdomain = "dc=example,dc=com";
-      ldapUserPassword.result.path = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
-      jwtSecret.result.path = pkgs.writeText "jwtSecret" "jwtSecret";
+      ldapUserPassword.result.path = config.shb.hardcodedsecret.ldapUserPassword.path;
+      jwtSecret.result.path = config.shb.hardcodedsecret.jwtSecret.path;
     };
   };
 
@@ -179,17 +185,36 @@ in
       dcdomain = config.shb.ldap.dcdomain;
 
       secrets = {
-        jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
-        ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
-        sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret";
-        storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey";
-        identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret";
-        identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} ''
-          mkdir $out
-          ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
-        '') + "/private.pem";
+        jwtSecret.result.path = config.shb.hardcodedsecret.autheliaJwtSecret.path;
+        ldapAdminPassword.result.path = config.shb.hardcodedsecret.ldapAdminPassword.path;
+        sessionSecret.result.path = config.shb.hardcodedsecret.sessionSecret.path;
+        storageEncryptionKey.result.path = config.shb.hardcodedsecret.storageEncryptionKey.path;
+        identityProvidersOIDCHMACSecret.result.path = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.path;
+        identityProvidersOIDCIssuerPrivateKey.result.path = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.path;
       };
     };
+
+    shb.hardcodedsecret.autheliaJwtSecret = config.shb.authelia.secrets.jwtSecret.request // {
+      content = "jwtSecret";
+    };
+    shb.hardcodedsecret.ldapAdminPassword = config.shb.authelia.secrets.ldapAdminPassword.request // {
+      content = "ldapUserPassword";
+    };
+    shb.hardcodedsecret.sessionSecret = config.shb.authelia.secrets.sessionSecret.request // {
+      content = "sessionSecret";
+    };
+    shb.hardcodedsecret.storageEncryptionKey = config.shb.authelia.secrets.storageEncryptionKey.request // {
+      content = "storageEncryptionKey";
+    };
+    shb.hardcodedsecret.identityProvidersOIDCHMACSecret = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request // {
+      content = "identityProvidersOIDCHMACSecret";
+    };
+    shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request // {
+      source = (pkgs.runCommand "gen-private-key" {} ''
+        mkdir $out
+        ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
+      '') + "/private.pem";
+    };
   };
 
 }
diff --git a/test/services/jellyfin.nix b/test/services/jellyfin.nix
index 2561456..2011c38 100644
--- a/test/services/jellyfin.nix
+++ b/test/services/jellyfin.nix
@@ -43,9 +43,13 @@ let
         host = "127.0.0.1";
         port = config.shb.ldap.ldapPort;
         dcdomain = config.shb.ldap.dcdomain;
-        passwordFile = config.shb.ldap.ldapUserPassword.result.path;
+        passwordFile.result.path = config.shb.hardcodedsecret.jellyfinLdapUserPassword.path;
       };
     };
+
+    shb.hardcodedsecret.jellyfinLdapUserPassword = config.shb.jellyfin.ldap.passwordFile.request // {
+      content = "ldapUserPassword";
+    };
   };
 
   sso = { config, ... }: {
@@ -53,9 +57,18 @@ let
       sso = {
         enable = true;
         endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
-        secretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile";
+        secretFile.result.path = config.shb.hardcodedsecret.jellyfinSSOPassword.path;
+        secretFileAuthelia.result.path = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.path;
       };
     };
+
+    shb.hardcodedsecret.jellyfinSSOPassword = config.shb.jellyfin.sso.secretFile.request // {
+      content = "ssoPassword";
+    };
+
+    shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = config.shb.jellyfin.sso.secretFileAuthelia.request // {
+      content = "ssoPassword";
+    };
   };
 in
 {