gitea/modules/setting/ssh.go

198 lines
8.5 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"os"
"path"
"path/filepath"
"strings"
"text/template"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
gossh "golang.org/x/crypto/ssh"
)
var SSH = struct {
Disabled bool `ini:"DISABLE_SSH"`
StartBuiltinServer bool `ini:"START_SSH_SERVER"`
BuiltinServerUser string `ini:"BUILTIN_SSH_SERVER_USER"`
UseProxyProtocol bool `ini:"SSH_SERVER_USE_PROXY_PROTOCOL"`
Domain string `ini:"SSH_DOMAIN"`
Port int `ini:"SSH_PORT"`
User string `ini:"SSH_USER"`
ListenHost string `ini:"SSH_LISTEN_HOST"`
ListenPort int `ini:"SSH_LISTEN_PORT"`
RootPath string `ini:"SSH_ROOT_PATH"`
ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"`
ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"`
ServerMACs []string `ini:"SSH_SERVER_MACS"`
ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"`
KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
KeygenPath string `ini:"SSH_KEYGEN_PATH"`
AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"`
AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"`
AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"`
AuthorizedKeysCommandTemplateTemplate *template.Template `ini:"-"`
MinimumKeySizeCheck bool `ini:"-"`
MinimumKeySizes map[string]int `ini:"-"`
CreateAuthorizedKeysFile bool `ini:"SSH_CREATE_AUTHORIZED_KEYS_FILE"`
CreateAuthorizedPrincipalsFile bool `ini:"SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE"`
ExposeAnonymous bool `ini:"SSH_EXPOSE_ANONYMOUS"`
AuthorizedPrincipalsAllow []string `ini:"SSH_AUTHORIZED_PRINCIPALS_ALLOW"`
AuthorizedPrincipalsEnabled bool `ini:"-"`
TrustedUserCAKeys []string `ini:"SSH_TRUSTED_USER_CA_KEYS"`
TrustedUserCAKeysFile string `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
TrustedUserCAKeysParsed []gossh.PublicKey `ini:"-"`
PerWriteTimeout time.Duration `ini:"SSH_PER_WRITE_TIMEOUT"`
PerWritePerKbTimeout time.Duration `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"`
}{
Disabled: false,
StartBuiltinServer: false,
Domain: "",
Port: 22,
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
KeygenPath: "ssh-keygen",
MinimumKeySizeCheck: true,
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2047},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}",
PerWriteTimeout: PerWriteTimeout,
PerWritePerKbTimeout: PerWritePerKbTimeout,
}
func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
anything := false
email := false
username := false
for _, value := range values {
v := strings.ToLower(strings.TrimSpace(value))
switch v {
case "off":
return []string{"off"}, false
case "email":
email = true
case "username":
username = true
case "anything":
anything = true
}
}
if anything {
return []string{"anything"}, true
}
authorizedPrincipalsAllow := []string{}
if username {
authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "username")
}
if email {
authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "email")
}
return authorizedPrincipalsAllow, true
}
func loadSSHFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server")
if len(SSH.Domain) == 0 {
SSH.Domain = Domain
}
homeDir, err := util.HomeDir()
if err != nil {
log.Fatal("Failed to get home directory: %v", err)
}
homeDir = strings.ReplaceAll(homeDir, "\\", "/")
SSH.RootPath = path.Join(homeDir, ".ssh")
serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",")
if len(serverCiphers) > 0 {
SSH.ServerCiphers = serverCiphers
}
serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",")
if len(serverKeyExchanges) > 0 {
SSH.ServerKeyExchanges = serverKeyExchanges
}
serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",")
if len(serverMACs) > 0 {
SSH.ServerMACs = serverMACs
}
SSH.KeyTestPath = os.TempDir()
if err = sec.MapTo(&SSH); err != nil {
log.Fatal("Failed to map SSH settings: %v", err)
}
for i, key := range SSH.ServerHostKeys {
if !filepath.IsAbs(key) {
SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key)
}
}
SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").MustString("ssh-keygen")
SSH.Port = sec.Key("SSH_PORT").MustInt(22)
SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port)
SSH.UseProxyProtocol = sec.Key("SSH_SERVER_USE_PROXY_PROTOCOL").MustBool(false)
// When disable SSH, start builtin server value is ignored.
if SSH.Disabled {
SSH.StartBuiltinServer = false
}
SSH.TrustedUserCAKeysFile = sec.Key("SSH_TRUSTED_USER_CA_KEYS_FILENAME").MustString(filepath.Join(SSH.RootPath, "gitea-trusted-user-ca-keys.pem"))
for _, caKey := range SSH.TrustedUserCAKeys {
pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(caKey))
if err != nil {
log.Fatal("Failed to parse TrustedUserCaKeys: %s %v", caKey, err)
}
SSH.TrustedUserCAKeysParsed = append(SSH.TrustedUserCAKeysParsed, pubKey)
}
if len(SSH.TrustedUserCAKeys) > 0 {
// Set the default as email,username otherwise we can leave it empty
sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("username,email")
} else {
sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("off")
}
SSH.AuthorizedPrincipalsAllow, SSH.AuthorizedPrincipalsEnabled = parseAuthorizedPrincipalsAllow(sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").Strings(","))
SSH.MinimumKeySizeCheck = sec.Key("MINIMUM_KEY_SIZE_CHECK").MustBool(SSH.MinimumKeySizeCheck)
minimumKeySizes := rootCfg.Section("ssh.minimum_key_sizes").Keys()
for _, key := range minimumKeySizes {
if key.MustInt() != -1 {
SSH.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt()
} else {
delete(SSH.MinimumKeySizes, strings.ToLower(key.Name()))
}
}
SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(true)
SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true)
SSH.AuthorizedPrincipalsBackup = false
SSH.CreateAuthorizedPrincipalsFile = false
if SSH.AuthorizedPrincipalsEnabled {
SSH.AuthorizedPrincipalsBackup = sec.Key("SSH_AUTHORIZED_PRINCIPALS_BACKUP").MustBool(true)
SSH.CreateAuthorizedPrincipalsFile = sec.Key("SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE").MustBool(true)
}
SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
SSH.AuthorizedKeysCommandTemplate = sec.Key("SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE").MustString(SSH.AuthorizedKeysCommandTemplate)
SSH.AuthorizedKeysCommandTemplateTemplate = template.Must(template.New("").Parse(SSH.AuthorizedKeysCommandTemplate))
SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
// ensure parseRunModeSetting has been executed before this
SSH.BuiltinServerUser = rootCfg.Section("server").Key("BUILTIN_SSH_SERVER_USER").MustString(RunUser)
SSH.User = rootCfg.Section("server").Key("SSH_USER").MustString(SSH.BuiltinServerUser)
}