From 620410c48023909753448be6c770a5c52af8d934 Mon Sep 17 00:00:00 2001
From: ltdk <usr@ltdk.xyz>
Date: Mon, 9 May 2022 13:10:16 -0400
Subject: [PATCH 1/6] Allow specifying SECRET_KEY_URI, similar to
 INTERNAL_TOKEN_URI

This modifies the existing internal token loading and the new secret key loading such that:

* Specifying both INTERNAL_TOKEN and INTERNAL_TOKEN_URI is an error, instead of just preferring the URI
* The install lock isn't required to regenerate the internal token or the secret key; it will always be regenerated if it's absent
---
 custom/conf/app.example.ini |  7 ++-
 modules/setting/setting.go  | 89 ++++++++++++++++++++++++-------------
 2 files changed, 64 insertions(+), 32 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 1561a53da0f07..98a9eeaa165d7 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -385,8 +385,11 @@ SECRET_KEY =
 ;; Secret used to validate communication within Gitea binary.
 INTERNAL_TOKEN=
 ;;
-;; Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: file:/etc/gitea/internal_token)
-;INTERNAL_TOKEN_URI = ;e.g. /etc/gitea/internal_token
+;; Alternative location to specify secret key, instead of this file; you cannot specify both this and SECRET_KEY, and must pick one
+;SECRET_KEY_URI = file:/etc/gitea/secret_key
+;;
+;; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
+;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
 ;;
 ;; How long to remember that a user is logged in before requiring relogin (in days)
 ;LOGIN_REMEMBER_DAYS = 7
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 09e510ffa01b4..79dc8ef5370e0 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -921,9 +921,16 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 
 	sec = Cfg.Section("security")
 	InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
-	SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
 	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
+	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", func() (string, error) {
+		// FIXME: https://github.com/go-gitea/gitea/issues/16832
+		//
+		// Until we properly support rotating an existing secret key,
+		// we shouldn't move users off of the default value
+		return "!#@FDEWREWR&*(", nil
+	})
+
 	CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
 
 	ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
@@ -946,11 +953,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
 	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
 
-	InternalToken = loadInternalToken(sec)
-	if InstallLock && InternalToken == "" {
-		// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
-		generateSaveInternalToken()
-	}
+	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN", generate.NewInternalToken)
 
 	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
 	if len(cfgdata) == 0 {
@@ -1139,51 +1142,73 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
 	return authorizedPrincipalsAllow, true
 }
 
-func loadInternalToken(sec *ini.Section) string {
-	uri := sec.Key("INTERNAL_TOKEN_URI").String()
+func loadSecret(
+	sec *ini.Section,
+	uriKey string,
+	verbatimKey string,
+	generator func() (string, error),
+) string {
+	// don't allow setting both URI and verbatim string
+	uri := sec.Key(uriKey).String()
+	verbatim := sec.Key(verbatimKey).String()
+	if uri != "" && verbatim != "" {
+		log.Fatal("Cannot specify both %s and %s", uriKey, verbatimKey)
+	}
+
+	// if we have no URI, use verbatim
 	if uri == "" {
-		return sec.Key("INTERNAL_TOKEN").String()
+		// if verbatim isn't provided, generate one
+		if verbatim == "" {
+			secret, err := generator()
+			if err != nil {
+				log.Fatal("Error trying to generate %s: %v", verbatimKey, err)
+			}
+			CreateOrAppendToCustomConf(func(cfg *ini.File) {
+				cfg.Section(sec.Name()).Key(verbatimKey).SetValue(secret)
+			})
+			return secret
+		}
+
+		return verbatim
 	}
+
 	tempURI, err := url.Parse(uri)
 	if err != nil {
-		log.Fatal("Failed to parse INTERNAL_TOKEN_URI (%s): %v", uri, err)
+		log.Fatal("Failed to parse %s (%s): %v", uriKey, uri, err)
 	}
 	switch tempURI.Scheme {
 	case "file":
 		buf, err := os.ReadFile(tempURI.RequestURI())
 		if err != nil && !os.IsNotExist(err) {
-			log.Fatal("Failed to open InternalTokenURI (%s): %v", uri, err)
+			log.Fatal("Failed to open %s (%s): %v", uriKey, uri, err)
 		}
-		// No token in the file, generate one and store it.
+
+		// empty file; generate secret and store it
 		if len(buf) == 0 {
-			token, err := generate.NewInternalToken()
+			token, err := generator()
 			if err != nil {
-				log.Fatal("Error generate internal token: %v", err)
+				log.Fatal("Error generating %s: %v", verbatimKey, err)
 			}
+
 			err = os.WriteFile(tempURI.RequestURI(), []byte(token), 0o600)
 			if err != nil {
-				log.Fatal("Error writing to InternalTokenURI (%s): %v", uri, err)
+				log.Fatal("Error writing to %s (%s): %v", uriKey, uri, err)
 			}
+
+			// we assume generator gives pre-parsed token
 			return token
 		}
+
 		return strings.TrimSpace(string(buf))
+
+	// only file URIs are allowed
 	default:
 		log.Fatal("Unsupported URI-Scheme %q (INTERNAL_TOKEN_URI = %q)", tempURI.Scheme, uri)
 	}
-	return ""
-}
 
-// generateSaveInternalToken generates and saves the internal token to app.ini
-func generateSaveInternalToken() {
-	token, err := generate.NewInternalToken()
-	if err != nil {
-		log.Fatal("Error generate internal token: %v", err)
-	}
-
-	InternalToken = token
-	CreateOrAppendToCustomConf(func(cfg *ini.File) {
-		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
-	})
+	// we should never get here
+	log.Fatal("Unknown error when loading %s", verbatimKey)
+	return ""
 }
 
 // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
@@ -1248,6 +1273,11 @@ func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte {
 // CreateOrAppendToCustomConf creates or updates the custom config.
 // Use the callback to set individual values.
 func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) {
+	if CustomConf == "" {
+		log.Error("Custom config path must not be empty")
+		return
+	}
+
 	cfg := ini.Empty()
 	isFile, err := util.IsFile(CustomConf)
 	if err != nil {
@@ -1262,8 +1292,6 @@ func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) {
 
 	callback(cfg)
 
-	log.Info("Settings saved to: %q", CustomConf)
-
 	if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
 		log.Fatal("failed to create '%s': %v", CustomConf, err)
 		return
@@ -1271,6 +1299,7 @@ func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) {
 	if err := cfg.SaveTo(CustomConf); err != nil {
 		log.Fatal("error saving to custom config: %v", err)
 	}
+	log.Info("Settings saved to: %q", CustomConf)
 
 	// Change permissions to be more restrictive
 	fi, err := os.Stat(CustomConf)

From 5cbdab7b28789ecd117172eb215c3a445116e2cb Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 28 Sep 2022 20:48:32 +0800
Subject: [PATCH 2/6] make log clear when setting.CreateOrAppendToCustomConf

---
 cmd/web.go                                   | 2 +-
 modules/setting/lfs.go                       | 2 +-
 modules/setting/setting.go                   | 6 +++---
 services/auth/source/oauth2/jwtsigningkey.go | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/cmd/web.go b/cmd/web.go
index e09560bb86cfe..1b9f7e420a916 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -203,7 +203,7 @@ func setPort(port string) error {
 		defaultLocalURL += ":" + setting.HTTPPort + "/"
 
 		// Save LOCAL_ROOT_URL if port changed
-		setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
+		setting.CreateOrAppendToCustomConf("server.LOCAL_ROOT_URL", func(cfg *ini.File) {
 			cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
 		})
 	}
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index 3179a67ce745a..686b043657422 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -62,7 +62,7 @@ func newLFSService() {
 			}
 
 			// Save secret
-			CreateOrAppendToCustomConf(func(cfg *ini.File) {
+			CreateOrAppendToCustomConf("server.LFS_JWT_SECRET", func(cfg *ini.File) {
 				cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
 			})
 		}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 79dc8ef5370e0..83dd76d777239 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -1163,7 +1163,7 @@ func loadSecret(
 			if err != nil {
 				log.Fatal("Error trying to generate %s: %v", verbatimKey, err)
 			}
-			CreateOrAppendToCustomConf(func(cfg *ini.File) {
+			CreateOrAppendToCustomConf(sec.Name()+"."+verbatimKey, func(cfg *ini.File) {
 				cfg.Section(sec.Name()).Key(verbatimKey).SetValue(secret)
 			})
 			return secret
@@ -1272,7 +1272,7 @@ func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte {
 
 // CreateOrAppendToCustomConf creates or updates the custom config.
 // Use the callback to set individual values.
-func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) {
+func CreateOrAppendToCustomConf(purpose string, callback func(cfg *ini.File)) {
 	if CustomConf == "" {
 		log.Error("Custom config path must not be empty")
 		return
@@ -1299,7 +1299,7 @@ func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) {
 	if err := cfg.SaveTo(CustomConf); err != nil {
 		log.Fatal("error saving to custom config: %v", err)
 	}
-	log.Info("Settings saved to: %q", CustomConf)
+	log.Info("Settings for %s saved to: %q", purpose, CustomConf)
 
 	// Change permissions to be more restrictive
 	fi, err := os.Stat(CustomConf)
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index d6b3c05a4fcbf..d9312ee820bd8 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -364,7 +364,7 @@ func loadOrCreateSymmetricKey() (interface{}, error) {
 			return nil, err
 		}
 
-		setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
+		setting.CreateOrAppendToCustomConf("oauth2.JWT_SECRET", func(cfg *ini.File) {
 			secretBase64 := base64.RawURLEncoding.EncodeToString(key)
 			cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64)
 		})

From 1119fee762841d2e562087fcd922d2961769a5d6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 28 Sep 2022 20:55:10 +0800
Subject: [PATCH 3/6] rename loadSecret to loadOrGenerateSecret and add more
 comments

---
 modules/setting/setting.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 023f1dbcb1753..4e28beb8daa39 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -925,11 +925,9 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
 	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
-	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", func() (string, error) {
+	SecretKey = loadOrGenerateSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", func() (string, error) {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832
-		//
-		// Until we properly support rotating an existing secret key,
-		// we shouldn't move users off of the default value
+		// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
 		return "!#@FDEWREWR&*(", nil
 	})
 
@@ -955,7 +953,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
 	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
 
-	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN", generate.NewInternalToken)
+	InternalToken = loadOrGenerateSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN", generate.NewInternalToken)
 
 	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
 	if len(cfgdata) == 0 {
@@ -1144,7 +1142,9 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
 	return authorizedPrincipalsAllow, true
 }
 
-func loadSecret(
+// loadOrGenerateSecret loads the secret if it exists in the config file,
+// or generates a new one and saves it into the config file
+func loadOrGenerateSecret(
 	sec *ini.Section,
 	uriKey string,
 	verbatimKey string,

From 7b5140c24797182f0e8732e56538acdd937b1255 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 28 Sep 2022 21:00:30 +0800
Subject: [PATCH 4/6] do not write the default secret to config (keep the old
 behavior)

---
 modules/setting/setting.go | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 4e28beb8daa39..3ba8ec99f8475 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -925,11 +925,12 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
 	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
-	SecretKey = loadOrGenerateSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", func() (string, error) {
+	SecretKey = loadOrGenerateSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", nil)
+	if SecretKey == "" {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832
 		// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
-		return "!#@FDEWREWR&*(", nil
-	})
+		SecretKey = "!#@FDEWREWR&*(" // nolint:gosec
+	}
 
 	CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
 
@@ -1160,7 +1161,7 @@ func loadOrGenerateSecret(
 	// if we have no URI, use verbatim
 	if uri == "" {
 		// if verbatim isn't provided, generate one
-		if verbatim == "" {
+		if verbatim == "" && generator != nil {
 			secret, err := generator()
 			if err != nil {
 				log.Fatal("Error trying to generate %s: %v", verbatimKey, err)
@@ -1186,7 +1187,7 @@ func loadOrGenerateSecret(
 		}
 
 		// empty file; generate secret and store it
-		if len(buf) == 0 {
+		if len(buf) == 0 && generator != nil {
 			token, err := generator()
 			if err != nil {
 				log.Fatal("Error generating %s: %v", verbatimKey, err)

From c2f0a01877c4a7e6ec024d84e1d21097eb90099c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 28 Sep 2022 21:53:31 +0800
Subject: [PATCH 5/6] fix document

---
 custom/conf/app.example.ini                           | 10 ++++++----
 docs/content/doc/advanced/config-cheat-sheet.en-us.md |  5 +++--
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 90841694e1ae1..3759428ed5cef 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -379,15 +379,17 @@ LOG_SQL = false ; if unset defaults to true
 ;; Whether the installer is disabled (set to true to disable the installer)
 INSTALL_LOCK = false
 ;;
-;; Global secret key that will be used - if blank will be regenerated.
+;; Global secret key that will be used
+;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
 SECRET_KEY =
 ;;
-;; Secret used to validate communication within Gitea binary.
-INTERNAL_TOKEN=
-;;
 ;; Alternative location to specify secret key, instead of this file; you cannot specify both this and SECRET_KEY, and must pick one
+;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
 ;SECRET_KEY_URI = file:/etc/gitea/secret_key
 ;;
+;; Secret used to validate communication within Gitea binary.
+INTERNAL_TOKEN=
+;;
 ;; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
 ;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
 ;;
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index ef64c57246155..a93234e28acd5 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -494,7 +494,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 ## Security (`security`)
 
 - `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible.
-- `SECRET_KEY`: **\<random at every install\>**: Global secret key. This should be changed.
+- `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
+- `SECRET_KEY_URI`: **<empty>**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_token`). It shouldn't be lost like SECRET_KEY.
 - `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
 - `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username.
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
@@ -520,7 +521,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 - `ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET`: **true**: Set to `false` to allow local users to push to gitea-repositories without setting up the Gitea environment. This is not recommended and if you want local users to push to Gitea repositories you should set the environment appropriately.
 - `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
 - `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
-- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
+- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
 - `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others.
 - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
 - `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users.

From 0c237716e59dc161c738fffdef82dea8fd152d2f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 29 Sep 2022 10:32:31 +0800
Subject: [PATCH 6/6] avoid writing incorrect config files

---
 .../doc/advanced/config-cheat-sheet.en-us.md  |  2 +-
 modules/setting/setting.go                    | 52 +++----------------
 routers/private/internal.go                   |  5 ++
 3 files changed, 12 insertions(+), 47 deletions(-)

diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index a93234e28acd5..50571bd4433fb 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -495,7 +495,7 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
 
 - `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible.
 - `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
-- `SECRET_KEY_URI`: **<empty>**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_token`). It shouldn't be lost like SECRET_KEY.
+- `SECRET_KEY_URI`: **<empty>**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY.
 - `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
 - `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username.
 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 3ba8ec99f8475..6233437bf5aac 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -21,7 +21,6 @@ import (
 	"text/template"
 	"time"
 
-	"code.gitea.io/gitea/modules/generate"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/user"
@@ -925,7 +924,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
 	LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
 	CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
-	SecretKey = loadOrGenerateSecret(sec, "SECRET_KEY_URI", "SECRET_KEY", nil)
+	SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
 	if SecretKey == "" {
 		// FIXME: https://github.com/go-gitea/gitea/issues/16832
 		// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
@@ -954,7 +953,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 	PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
 	SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
 
-	InternalToken = loadOrGenerateSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN", generate.NewInternalToken)
+	InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
 
 	cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
 	if len(cfgdata) == 0 {
@@ -1143,14 +1142,7 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
 	return authorizedPrincipalsAllow, true
 }
 
-// loadOrGenerateSecret loads the secret if it exists in the config file,
-// or generates a new one and saves it into the config file
-func loadOrGenerateSecret(
-	sec *ini.Section,
-	uriKey string,
-	verbatimKey string,
-	generator func() (string, error),
-) string {
+func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string {
 	// don't allow setting both URI and verbatim string
 	uri := sec.Key(uriKey).String()
 	verbatim := sec.Key(verbatimKey).String()
@@ -1160,18 +1152,6 @@ func loadOrGenerateSecret(
 
 	// if we have no URI, use verbatim
 	if uri == "" {
-		// if verbatim isn't provided, generate one
-		if verbatim == "" && generator != nil {
-			secret, err := generator()
-			if err != nil {
-				log.Fatal("Error trying to generate %s: %v", verbatimKey, err)
-			}
-			CreateOrAppendToCustomConf(sec.Name()+"."+verbatimKey, func(cfg *ini.File) {
-				cfg.Section(sec.Name()).Key(verbatimKey).SetValue(secret)
-			})
-			return secret
-		}
-
 		return verbatim
 	}
 
@@ -1182,36 +1162,16 @@ func loadOrGenerateSecret(
 	switch tempURI.Scheme {
 	case "file":
 		buf, err := os.ReadFile(tempURI.RequestURI())
-		if err != nil && !os.IsNotExist(err) {
-			log.Fatal("Failed to open %s (%s): %v", uriKey, uri, err)
-		}
-
-		// empty file; generate secret and store it
-		if len(buf) == 0 && generator != nil {
-			token, err := generator()
-			if err != nil {
-				log.Fatal("Error generating %s: %v", verbatimKey, err)
-			}
-
-			err = os.WriteFile(tempURI.RequestURI(), []byte(token), 0o600)
-			if err != nil {
-				log.Fatal("Error writing to %s (%s): %v", uriKey, uri, err)
-			}
-
-			// we assume generator gives pre-parsed token
-			return token
+		if err != nil {
+			log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err)
 		}
-
 		return strings.TrimSpace(string(buf))
 
 	// only file URIs are allowed
 	default:
 		log.Fatal("Unsupported URI-Scheme %q (INTERNAL_TOKEN_URI = %q)", tempURI.Scheme, uri)
+		return ""
 	}
-
-	// we should never get here
-	log.Fatal("Unknown error when loading %s", verbatimKey)
-	return ""
 }
 
 // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 061c7f3c822af..e9cc20a77dc6d 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -24,6 +24,11 @@ func CheckInternalToken(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 		tokens := req.Header.Get("Authorization")
 		fields := strings.SplitN(tokens, " ", 2)
+		if setting.InternalToken == "" {
+			log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
+			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+			return
+		}
 		if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
 			log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
 			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)