1// Copyright 2023 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package setting
5
6import (
7 "os"
8 "path"
9 "path/filepath"
10 "strings"
11 "text/template"
12 "time"
13
14 "forgejo.org/modules/log"
15 "forgejo.org/modules/util"
16
17 gossh "golang.org/x/crypto/ssh"
18)
19
20var SSH = struct {
21 Disabled bool `ini:"DISABLE_SSH"`
22 StartBuiltinServer bool `ini:"START_SSH_SERVER"`
23 BuiltinServerUser string `ini:"BUILTIN_SSH_SERVER_USER"`
24 UseProxyProtocol bool `ini:"SSH_SERVER_USE_PROXY_PROTOCOL"`
25 Domain string `ini:"SSH_DOMAIN"`
26 Port int `ini:"SSH_PORT"`
27 User string `ini:"SSH_USER"`
28 ListenHost string `ini:"SSH_LISTEN_HOST"`
29 ListenPort int `ini:"SSH_LISTEN_PORT"`
30 RootPath string `ini:"SSH_ROOT_PATH"`
31 ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"`
32 ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"`
33 ServerMACs []string `ini:"SSH_SERVER_MACS"`
34 ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"`
35 KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
36 KeygenPath string `ini:"SSH_KEYGEN_PATH"`
37 AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"`
38 AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"`
39 AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"`
40 AuthorizedKeysCommandTemplateTemplate *template.Template `ini:"-"`
41 MinimumKeySizeCheck bool `ini:"-"`
42 MinimumKeySizes map[string]int `ini:"-"`
43 CreateAuthorizedKeysFile bool `ini:"SSH_CREATE_AUTHORIZED_KEYS_FILE"`
44 CreateAuthorizedPrincipalsFile bool `ini:"SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE"`
45 ExposeAnonymous bool `ini:"SSH_EXPOSE_ANONYMOUS"`
46 AuthorizedPrincipalsAllow []string `ini:"SSH_AUTHORIZED_PRINCIPALS_ALLOW"`
47 AuthorizedPrincipalsEnabled bool `ini:"-"`
48 TrustedUserCAKeys []string `ini:"SSH_TRUSTED_USER_CA_KEYS"`
49 TrustedUserCAKeysFile string `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
50 TrustedUserCAKeysParsed []gossh.PublicKey `ini:"-"`
51 PerWriteTimeout time.Duration `ini:"SSH_PER_WRITE_TIMEOUT"`
52 PerWritePerKbTimeout time.Duration `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"`
53}{
54 Disabled: false,
55 StartBuiltinServer: false,
56 Domain: "",
57 Port: 22,
58 ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
59 ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
60 ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
61 KeygenPath: "",
62 MinimumKeySizeCheck: true,
63 MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
64 ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
65 AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}",
66 PerWriteTimeout: PerWriteTimeout,
67 PerWritePerKbTimeout: PerWritePerKbTimeout,
68}
69
70func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) {
71 anything := false
72 email := false
73 username := false
74 for _, value := range values {
75 v := strings.ToLower(strings.TrimSpace(value))
76 switch v {
77 case "off":
78 return []string{"off"}, false
79 case "email":
80 email = true
81 case "username":
82 username = true
83 case "anything":
84 anything = true
85 }
86 }
87 if anything {
88 return []string{"anything"}, true
89 }
90
91 authorizedPrincipalsAllow := []string{}
92 if username {
93 authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "username")
94 }
95 if email {
96 authorizedPrincipalsAllow = append(authorizedPrincipalsAllow, "email")
97 }
98
99 return authorizedPrincipalsAllow, true
100}
101
102func loadSSHFrom(rootCfg ConfigProvider) {
103 sec := rootCfg.Section("server")
104 if len(SSH.Domain) == 0 {
105 SSH.Domain = Domain
106 }
107
108 homeDir, err := util.HomeDir()
109 if err != nil {
110 log.Fatal("Failed to get home directory: %v", err)
111 }
112 homeDir = strings.ReplaceAll(homeDir, "\\", "/")
113
114 SSH.RootPath = path.Join(homeDir, ".ssh")
115 serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",")
116 if len(serverCiphers) > 0 {
117 SSH.ServerCiphers = serverCiphers
118 }
119 serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",")
120 if len(serverKeyExchanges) > 0 {
121 SSH.ServerKeyExchanges = serverKeyExchanges
122 }
123 serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",")
124 if len(serverMACs) > 0 {
125 SSH.ServerMACs = serverMACs
126 }
127 SSH.KeyTestPath = os.TempDir()
128 if err = sec.MapTo(&SSH); err != nil {
129 log.Fatal("Failed to map SSH settings: %v", err)
130 }
131 for i, key := range SSH.ServerHostKeys {
132 if !filepath.IsAbs(key) {
133 SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key)
134 }
135 }
136
137 SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").String()
138 SSH.Port = sec.Key("SSH_PORT").MustInt(22)
139 SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port)
140 SSH.UseProxyProtocol = sec.Key("SSH_SERVER_USE_PROXY_PROTOCOL").MustBool(false)
141
142 // When disable SSH, start builtin server value is ignored.
143 if SSH.Disabled {
144 SSH.StartBuiltinServer = false
145 }
146
147 SSH.TrustedUserCAKeysFile = sec.Key("SSH_TRUSTED_USER_CA_KEYS_FILENAME").MustString(filepath.Join(SSH.RootPath, "gitea-trusted-user-ca-keys.pem"))
148
149 for _, caKey := range SSH.TrustedUserCAKeys {
150 pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(caKey))
151 if err != nil {
152 log.Fatal("Failed to parse TrustedUserCaKeys: %s %v", caKey, err)
153 }
154
155 SSH.TrustedUserCAKeysParsed = append(SSH.TrustedUserCAKeysParsed, pubKey)
156 }
157 if len(SSH.TrustedUserCAKeys) > 0 {
158 // Set the default as email,username otherwise we can leave it empty
159 sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("username,email")
160 } else {
161 sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").MustString("off")
162 }
163
164 SSH.AuthorizedPrincipalsAllow, SSH.AuthorizedPrincipalsEnabled = parseAuthorizedPrincipalsAllow(sec.Key("SSH_AUTHORIZED_PRINCIPALS_ALLOW").Strings(","))
165
166 SSH.MinimumKeySizeCheck = sec.Key("MINIMUM_KEY_SIZE_CHECK").MustBool(SSH.MinimumKeySizeCheck)
167 minimumKeySizes := rootCfg.Section("ssh.minimum_key_sizes").Keys()
168 for _, key := range minimumKeySizes {
169 if key.MustInt() != -1 {
170 SSH.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt()
171 } else {
172 delete(SSH.MinimumKeySizes, strings.ToLower(key.Name()))
173 }
174 }
175
176 SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(false)
177 SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true)
178
179 SSH.AuthorizedPrincipalsBackup = false
180 SSH.CreateAuthorizedPrincipalsFile = false
181 if SSH.AuthorizedPrincipalsEnabled {
182 SSH.AuthorizedPrincipalsBackup = sec.Key("SSH_AUTHORIZED_PRINCIPALS_BACKUP").MustBool(true)
183 SSH.CreateAuthorizedPrincipalsFile = sec.Key("SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE").MustBool(true)
184 }
185
186 SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
187 SSH.AuthorizedKeysCommandTemplate = sec.Key("SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE").MustString(SSH.AuthorizedKeysCommandTemplate)
188
189 SSH.AuthorizedKeysCommandTemplateTemplate = template.Must(template.New("").Parse(SSH.AuthorizedKeysCommandTemplate))
190
191 SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
192 SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
193
194 // ensure parseRunModeSetting has been executed before this
195 SSH.BuiltinServerUser = rootCfg.Section("server").Key("BUILTIN_SSH_SERVER_USER").MustString(RunUser)
196 SSH.User = rootCfg.Section("server").Key("SSH_USER").MustString(SSH.BuiltinServerUser)
197}