Skip to content

WIP: Introduce general web secret #30929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var (
Usage: "Generate a secret token",
Subcommands: []*cli.Command{
microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret,
microcmdGenerateGeneralWebSecret,
microcmdGenerateSecretKey,
},
}
Expand All @@ -40,18 +40,17 @@ var (
Action: runGenerateInternalToken,
}

microcmdGenerateLfsJwtSecret = &cli.Command{
Name: "JWT_SECRET",
Aliases: []string{"LFS_JWT_SECRET"},
Usage: "Generate a new JWT_SECRET",
Action: runGenerateLfsJwtSecret,
}

microcmdGenerateSecretKey = &cli.Command{
Name: "SECRET_KEY",
Usage: "Generate a new SECRET_KEY",
Action: runGenerateSecretKey,
}

microcmdGenerateGeneralWebSecret = &cli.Command{
Name: "GENERAL_WEB_SECRET",
Usage: "Generate a new GENERAL_WEB_SECRET",
Action: runGenerateGeneralWebSecret,
}
)

func runGenerateInternalToken(c *cli.Context) error {
Expand All @@ -69,18 +68,15 @@ func runGenerateInternalToken(c *cli.Context) error {
return nil
}

func runGenerateLfsJwtSecret(c *cli.Context) error {
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
func runGenerateGeneralWebSecret(c *cli.Context) error {
_, webSecretBase64, err := generate.NewGeneralWebSecretWithBase64()
if err != nil {
return err
}

fmt.Printf("%s", jwtSecretBase64)

fmt.Printf("%s", webSecretBase64)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Printf("\n")
}

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ func runServ(c *cli.Context) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
}
Expand Down
24 changes: 8 additions & 16 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,6 @@ RUN_USER = ; git
;; Enables git-lfs support. true or false, default is false.
;LFS_START_SERVER = false
;;
;;
;; LFS authentication secret, change this yourself
;LFS_JWT_SECRET =
;;
;; Alternative location to specify LFS authentication secret. You cannot specify both this and LFS_JWT_SECRET, and must pick one
;LFS_JWT_SECRET_URI = file:/etc/gitea/lfs_jwt_secret
;;
;; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail.
;LFS_HTTP_AUTH_EXPIRY = 24h
;;
Expand Down Expand Up @@ -428,18 +421,24 @@ INSTALL_LOCK = false
;;
;; 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_KEY =
;;
;; 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 =
;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
;;
;; A general secret used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc)
;GENERAL_WEB_SECRET =
;;
;; Alternative location to specify general web secret (eg: file:/etc/gitea/general_web_secret), you cannot specify both this and GENERAL_WEB_SECRET, and must pick one
;GENERAL_WEB_SECRET_URI =
;;
;; How long to remember that a user is logged in before requiring relogin (in days)
;LOGIN_REMEMBER_DAYS = 31
;;
Expand Down Expand Up @@ -538,13 +537,6 @@ ENABLED = true
;; The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you.
;JWT_SIGNING_PRIVATE_KEY_FILE = jwt/private.pem
;;
;; OAuth2 authentication secret for access and refresh tokens, change this yourself to a unique string. CLI generate option is helpful in this case. https://docs.gitea.io/en-us/command-line/#generate
;; This setting is only needed if JWT_SIGNING_ALGORITHM is set to HS256, HS384 or HS512.
;JWT_SECRET =
;;
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
;;
;; Lifetime of an OAuth2 access token in seconds
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
;;
Expand Down
4 changes: 2 additions & 2 deletions docs/content/administration/command-line.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,11 @@ for automatic deployments.
- `secret`:
- Options:
- `INTERNAL_TOKEN`: Token used for an internal API call authentication.
- `JWT_SECRET`: LFS & OAUTH2 JWT authentication secret (LFS_JWT_SECRET is aliased to this option for backwards compatibility).
- `GENERAL_WEB_SECRET`: A general secret used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc).
- `SECRET_KEY`: Global secret key.
- Examples:
- `gitea generate secret INTERNAL_TOKEN`
- `gitea generate secret JWT_SECRET`
- `gitea generate secret GENERAL_WEB_SECRET`
- `gitea generate secret SECRET_KEY`

### keys
Expand Down
4 changes: 2 additions & 2 deletions docs/content/administration/command-line.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,11 @@ menu:
- `secret`:
- 选项:
- `INTERNAL_TOKEN`: 用于内部 API 调用身份验证的令牌。
- `JWT_SECRET`: 用于 LFS 和 OAUTH2 JWT 身份验证的密钥(LFS_JWT_SECRET 是此选项的别名,用于向后兼容)
- `GENERAL_WEB_SECRET`: 用于签名或者加密 web 内容(例如 JWT、CSRF、验证等)的通用密钥
- `SECRET_KEY`: 全局密钥。
- 示例:
- `gitea generate secret INTERNAL_TOKEN`
- `gitea generate secret JWT_SECRET`
- `gitea generate secret GENERAL_WEB_SECRET`
- `gitea generate secret SECRET_KEY`

### keys
Expand Down
6 changes: 2 additions & 4 deletions docs/content/administration/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login, **custom**\]. Where custom would instead be any URL such as "/org/repo" or even `https://anotherwebsite.com`
- `LFS_START_SERVER`: **false**: Enables Git LFS support.
- `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)s/lfs**: Default LFS content path. (if it is on local storage.) **DEPRECATED** use settings in `[lfs]`.
- `LFS_JWT_SECRET`: **_empty_**: LFS authentication secret, change this a unique string.
- `LFS_JWT_SECRET_URI`: **_empty_**: Instead of defining LFS_JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/lfs_jwt_secret`)
- `LFS_HTTP_AUTH_EXPIRY`: **24h**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail.
- `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit).
- `LFS_LOCKS_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page.
Expand Down Expand Up @@ -556,6 +554,8 @@ And the following unique queues:
- `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`)
- `GENERAL_WEB_SECRET`: **\<random at every install if no uri set\>**: A general secret used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc).
- `GENERAL_WEB_SECRET_URI`: **_empty_**: Instead of defining GENERAL_WEB_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the general web secret (example value: `file:/etc/gitea/genearl_web_secret`)
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
- Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
- The hash functions may be tuned by using `$` after the algorithm:
Expand Down Expand Up @@ -1121,8 +1121,6 @@ This section only does "set" config, a removed config key from this section won'
- `REFRESH_TOKEN_EXPIRATION_TIME`: **730**: Lifetime of an OAuth2 refresh token in hours
- `INVALIDATE_REFRESH_TOKENS`: **false**: Check if refresh token has already been used
- `JWT_SIGNING_ALGORITHM`: **RS256**: Algorithm used to sign OAuth2 tokens. Valid values: \[`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`\]
- `JWT_SECRET`: **_empty_**: OAuth2 authentication secret for access and refresh tokens, change this to a unique string. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `HS256`, `HS384` or `HS512`.
- `JWT_SECRET_URI`: **_empty_**: Instead of defining JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/oauth2_jwt_secret`)
- `JWT_SIGNING_PRIVATE_KEY_FILE`: **jwt/private.pem**: Private key file path used to sign OAuth2 tokens. The path is relative to `APP_DATA_PATH`. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `RS256`, `RS384`, `RS512`, `ES256`, `ES384` or `ES512`. The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you.
- `MAX_TOKEN_LENGTH`: **32767**: Maximum length of token/cookie to accept from OAuth2 provider
- `DEFAULT_APPLICATIONS`: **git-credential-oauth, git-credential-manager, tea**: Pre-register OAuth applications for some services on startup. See the [OAuth2 documentation](/development/oauth2-provider.md) for the list of available options.
Expand Down
4 changes: 0 additions & 4 deletions docs/content/administration/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,6 @@ menu:
- `LANDING_PAGE`: **home**:未经身份验证用户的登录页面 \[home, explore, organizations, login, **custom**]。其中 custom 可以是任何 URL,例如 "/org/repo" 或甚至是 `https://anotherwebsite.com`。
- `LFS_START_SERVER`: **false**:启用 Git LFS 支持。
- `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)s/lfs**:默认的 LFS 内容路径(如果它在本地存储中)。**已弃用**,请使用 `[lfs]` 中的设置。
- `LFS_JWT_SECRET`: **_empty_**:LFS 身份验证密钥,将其更改为唯一的字符串。
- `LFS_JWT_SECRET_URI`: **_empty_**:代替在配置中定义 LFS_JWT_SECRET,可以使用此配置选项为 Gitea 提供包含密钥的文件的路径(示例值:`file:/etc/gitea/lfs_jwt_secret`)。
- `LFS_HTTP_AUTH_EXPIRY`: **24h**:LFS 身份验证的有效期,以 time.Duration 表示,超过此期限的推送可能会失败。
- `LFS_MAX_FILE_SIZE`: **0**:允许的最大 LFS 文件大小(以字节为单位,设置为 0 为无限制)。
- `LFS_LOCKS_PAGING_NUM`: **50**:每页返回的最大 LFS 锁定数。
Expand Down Expand Up @@ -1051,8 +1049,6 @@ Gitea 创建以下非唯一队列:
- `REFRESH_TOKEN_EXPIRATION_TIME`:**730**:OAuth2刷新令牌的生命周期,以小时为单位。
- `INVALIDATE_REFRESH_TOKENS`:**false**:检查刷新令牌是否已被使用。
- `JWT_SIGNING_ALGORITHM`:**RS256**:用于签署OAuth2令牌的算法。有效值:[`HS256`,`HS384`,`HS512`,`RS256`,`RS384`,`RS512`,`ES256`,`ES384`,`ES512`]。
- `JWT_SECRET`:**_empty_**:OAuth2访问和刷新令牌的身份验证密钥,请将其更改为唯一的字符串。仅当`JWT_SIGNING_ALGORITHM`设置为`HS256`,`HS384`或`HS512`时才需要此设置。
- `JWT_SECRET_URI`:**_empty_**:可以使用此配置选项,而不是在配置中定义`JWT_SECRET`,以向Gitea提供包含密钥的文件的路径(示例值:`file:/etc/gitea/oauth2_jwt_secret`)。
- `JWT_SIGNING_PRIVATE_KEY_FILE`:**jwt/private.pem**:用于签署OAuth2令牌的私钥文件路径。路径相对于`APP_DATA_PATH`。仅当`JWT_SIGNING_ALGORITHM`设置为`RS256`,`RS384`,`RS512`,`ES256`,`ES384`或`ES512`时才需要此设置。文件必须包含PKCS8格式的RSA或ECDSA私钥。如果不存在密钥,则将为您创建一个4096位密钥。
- `MAX_TOKEN_LENGTH`:**32767**:从OAuth2提供者接受的令牌/cookie的最大长度。
- `DEFAULT_APPLICATIONS`:**git-credential-oauth,git-credential-manager, tea**:在启动时预注册用于某些服务的OAuth应用程序。有关可用选项列表,请参阅[OAuth2文档](/development/oauth2-provider.md)。
Expand Down
24 changes: 15 additions & 9 deletions modules/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,29 @@ func NewInternalToken() (string, error) {
return internalToken, nil
}

const defaultJwtSecretLen = 32
func DecodeGeneralWebSecretBase64(src string) ([]byte, error) {
return decodeGeneralSecretBase64(src, 32)
}

func NewGeneralWebSecretWithBase64() ([]byte, string, error) {
return newGeneralSecretWithBase64(32)
}

// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
func DecodeJwtSecretBase64(src string) ([]byte, error) {
// decodeGeneralSecretBase64 decodes a base64 encoded secret into bytes, and check its length
func decodeGeneralSecretBase64(src string, length int) ([]byte, error) {
encoding := base64.RawURLEncoding
decoded := make([]byte, encoding.DecodedLen(len(src))+3)
if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
return nil, err
} else if n != defaultJwtSecretLen {
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
} else if n != length {
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
}
return decoded[:defaultJwtSecretLen], nil
return decoded[:length], nil
}

// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
func NewJwtSecretWithBase64() ([]byte, string, error) {
bytes := make([]byte, defaultJwtSecretLen)
// newGeneralSecretWithBase64 generates a secret with its base64 encoded value intended to be used for saving into config file
func newGeneralSecretWithBase64(length int) ([]byte, string, error) {
bytes := make([]byte, length)
_, err := io.ReadFull(rand.Reader, bytes)
if err != nil {
return nil, "", err
Expand Down
10 changes: 5 additions & 5 deletions modules/generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ import (
)

func TestDecodeJwtSecretBase64(t *testing.T) {
_, err := DecodeJwtSecretBase64("abcd")
_, err := DecodeGeneralWebSecretBase64("abcd")
assert.ErrorContains(t, err, "invalid base64 decoded length")
_, err = DecodeJwtSecretBase64(strings.Repeat("a", 64))
_, err = DecodeGeneralWebSecretBase64(strings.Repeat("a", 64))
assert.ErrorContains(t, err, "invalid base64 decoded length")

str32 := strings.Repeat("x", 32)
encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
decoded32, err := DecodeJwtSecretBase64(encoded32)
decoded32, err := DecodeGeneralWebSecretBase64(encoded32)
assert.NoError(t, err)
assert.Equal(t, str32, string(decoded32))
}

func TestNewJwtSecretWithBase64(t *testing.T) {
secret, encoded, err := NewJwtSecretWithBase64()
secret, encoded, err := NewGeneralWebSecretWithBase64()
assert.NoError(t, err)
assert.Len(t, secret, 32)
decoded, err := DecodeJwtSecretBase64(encoded)
decoded, err := DecodeGeneralWebSecretBase64(encoded)
assert.NoError(t, err)
assert.Equal(t, secret, decoded)
}
27 changes: 0 additions & 27 deletions modules/setting/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ package setting
import (
"fmt"
"time"

"code.gitea.io/gitea/modules/generate"
)

// LFS represents the configuration for Git LFS
var LFS = struct {
StartServer bool `ini:"LFS_START_SERVER"`
JWTSecretBytes []byte `ini:"-"`
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
Expand Down Expand Up @@ -54,29 +51,5 @@ func loadLFSFrom(rootCfg ConfigProvider) error {

LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(24 * time.Hour)

if !LFS.StartServer || !InstallLock {
return nil
}

jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
if err != nil {
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
}

// Save secret
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
}

return nil
}
45 changes: 0 additions & 45 deletions modules/setting/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ package setting
import (
"math"
"path/filepath"
"sync/atomic"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
)

Expand Down Expand Up @@ -130,50 +128,7 @@ func loadOAuth2From(rootCfg ConfigProvider) {
return
}

jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")

if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
}

if InstallLock {
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
if err != nil {
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
}
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
}
generalSigningSecret.Store(&jwtSecretBytes)
}
}

// generalSigningSecret is used as container for a []byte value
// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
var generalSigningSecret atomic.Pointer[[]byte]

func GetGeneralTokenSigningSecret() []byte {
old := generalSigningSecret.Load()
if old == nil || len(*old) == 0 {
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("Unable to generate general JWT secret: %s", err.Error())
}
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
LogStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
return jwtSecret
}
return *generalSigningSecret.Load()
}
return *old
}
Loading