Files
Gitea/modules/setting/security.go
T
TheFox0x7 1b1274486c fix(git)!: use internal proxy for all git operations (#39426)
Introduces gitproxy module which spawns a small forward proxy as scanner
for git calls
Replaces hostmatcher with matchlist which supports port rules
Deprecates ALLOWED_DOMAINS/BLOCKED_DOMAINS and ALLOW_LOCALNETWORKS
settings in migration in favor of full names we have in security
configs.
Removes `external` preset in favor of lax/strict modes, strict mode
requiring explicit ports if they aren't standard http/s ones.

Breaking changes:
- `external` preset no longer works as deny rule. To enforce that, use
`strict` mode and allow ranges to connect to
- Wildcards are no longer accepted in IP addresses
- `*` is no longer allowed as entry in lists
- domain rules now use curl like syntax `*.example.com` matching
subdomains but not `example.com`, `example.com` matching itself and all
subdomains. `example.*` is not a valid rule
- In the default `lax` mode, `[security] ALLOWED_HOST_LIST` no longer
restricts public hosts, set `EGRESS_MODE = strict` to keep an exclusive
list. A startup warning flags this
- Invalid list entries are logged at startup, invalid
`BLOCKED_HOST_LIST`/`BLOCKED_DOMAINS` entries stop it

Docs: https://gitea.com/gitea/docs/pulls/557
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-29 14:43:36 +02:00

237 lines
9.0 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/url"
"os"
"strings"
"gitea.dev/modules/auth/password/hash"
"gitea.dev/modules/egress/policy"
"gitea.dev/modules/generate"
"gitea.dev/modules/log"
)
// Security settings
var Security = struct {
// TODO: move more settings to this struct in future
XFrameOptions string
XContentTypeOptions string
ContentSecurityPolicyGeneral string // it only supports empty (default policy) or "unset", maybe it can support more in the future
EgressMode string
AllowedHostList string
}{
XFrameOptions: "SAMEORIGIN",
XContentTypeOptions: "nosniff",
EgressMode: "lax",
}
// normalizePolicyMode validates a lax/strict egress policy EGRESS_MODE value, empty defaults to lax
func normalizePolicyMode(mode string) string {
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "":
return "lax"
case "lax", "strict":
return mode
default:
log.Fatal("Invalid egress policy EGRESS_MODE %q, use lax or strict", mode)
return ""
}
}
// checkHostList reports the entries an egress host list drops, a dropped block entry would allow a blocked host so it stops startup
func checkHostList(key, hostList string, isBlockList bool) {
rejected := policy.NewAllowList(hostList, policy.Lax).Rejected()
for _, reason := range rejected {
LogStartupProblem(1, log.ERROR, "%s ignores an invalid entry: %s", key, reason)
}
if isBlockList && len(rejected) > 0 {
log.Fatal("%s has invalid entries, fix them so no blocked host is allowed", key)
}
}
var (
InstallLock bool
SecretKey string
InternalToken string // internal access token
LogInRememberDays int
CookieRememberName string
ReverseProxyAuthUser string
ReverseProxyAuthEmail string
ReverseProxyAuthFullName string
ReverseProxyLimit int
ReverseProxyLogoutRedirect string
ReverseProxyTrustedProxies []string
MinPasswordLength int
ImportLocalPaths bool
DisableGitHooks = true
DisableWebhooks bool
OnlyAllowPushIfGiteaEnvironmentSet bool
PasswordComplexity []string
PasswordHashAlgo string
PasswordCheckPwn bool
SuccessfulTokensCacheSize int
DisableQueryAuthToken bool
RecordUserSignupMetadata = false
TwoFactorAuthEnforced = false
)
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
// If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear.
func loadSecret(sec ConfigSection, uriKey, verbatimKey string) 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 verbatim
}
tempURI, err := url.Parse(uri)
if err != nil {
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 {
log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err)
}
val := strings.TrimSpace(string(buf))
if val == "" {
// The file shouldn't be empty, otherwise we can not know whether the user has ever set the KEY or KEY_URI
// For example: if INTERNAL_TOKEN_URI=file:///empty-file,
// Then if the token is re-generated during installation and saved to INTERNAL_TOKEN
// Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't)
log.Fatal("Failed to read %s (%s): the file is empty", uriKey, tempURI.RequestURI())
}
return val
// only file URIs are allowed
default:
log.Fatal("Unsupported URI-Scheme %q (%q = %q)", tempURI.Scheme, uriKey, uri)
return ""
}
}
// generateSaveInternalToken generates and saves the internal token to app.ini
func generateSaveInternalToken(rootCfg ConfigProvider) {
token, err := generate.NewInternalToken()
if err != nil {
log.Fatal("Error generate internal token: %v", err)
}
InternalToken = token
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Fatal("Error saving internal token: %v", err)
}
rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
if err = saveCfg.Save(); err != nil {
log.Fatal("Error saving internal token: %v", err)
}
}
func loadSecurityFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("security")
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
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
SecretKey = "!#@FDEWREWR&*("
}
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL")
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String()
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
if len(ReverseProxyTrustedProxies) == 0 {
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
}
MinPasswordLength = sec.Key("MIN_PASSWORD_LENGTH").MustInt(8)
ImportLocalPaths = sec.Key("IMPORT_LOCAL_PATHS").MustBool(false)
DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true)
DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false)
OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true)
// Ensure that the provided default hash algorithm is a valid hash algorithm
var algorithm *hash.PasswordHashAlgorithm
PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(sec.Key("PASSWORD_HASH_ALGO").MustString(""))
if algorithm == nil {
log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString(""))
}
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0")
if !sec.HasKey("X_FRAME_OPTIONS") {
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
}
if err := sec.MapTo(&Security); err != nil {
log.Fatal("Failed to map security settings: %v", err)
}
egressModeSet := sec.HasKey("EGRESS_MODE")
Security.EgressMode = normalizePolicyMode(sec.Key("EGRESS_MODE").String())
checkHostList("[security] ALLOWED_HOST_LIST", Security.AllowedHostList, false)
if Security.AllowedHostList != "" && !egressModeSet {
LogStartupProblem(1, log.WARN, "[security] ALLOWED_HOST_LIST only restricts private hosts in the default lax mode, set EGRESS_MODE = strict to allow only the listed hosts, or lax to keep this")
}
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
switch twoFactorAuth {
case "":
case "enforced":
TwoFactorAuthEnforced = true
default:
log.Fatal("Invalid two-factor auth option: %s", twoFactorAuth)
}
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
if InstallLock && InternalToken == "" {
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate
// some users do cluster deployment, they still depend on this auto-generating behavior.
generateSaveInternalToken(rootCfg)
}
cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
if len(cfgdata) == 0 {
cfgdata = []string{"off"}
}
PasswordComplexity = make([]string, 0, len(cfgdata))
for _, name := range cfgdata {
name := strings.ToLower(strings.Trim(name, `"`))
if name != "" {
PasswordComplexity = append(PasswordComplexity, name)
}
}
sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN")
// TODO: default value should be true in future releases
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
RecordUserSignupMetadata = sec.Key("RECORD_USER_SIGNUP_METADATA").MustBool(false)
// warn if the setting is set to false explicitly
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
}
}