loading up the forgejo repo on tangled to test page performance
at forgejo 11 kB view raw
1// Copyright 2017 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package ssh 5 6import ( 7 "bytes" 8 "context" 9 "crypto/rand" 10 "crypto/rsa" 11 "crypto/x509" 12 "encoding/pem" 13 "errors" 14 "io" 15 "net" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "strconv" 20 "strings" 21 "sync" 22 "syscall" 23 24 asymkey_model "forgejo.org/models/asymkey" 25 "forgejo.org/modules/graceful" 26 "forgejo.org/modules/log" 27 "forgejo.org/modules/process" 28 "forgejo.org/modules/setting" 29 "forgejo.org/modules/util" 30 31 "github.com/gliderlabs/ssh" 32 gossh "golang.org/x/crypto/ssh" 33) 34 35func getExitStatusFromError(err error) int { 36 if err == nil { 37 return 0 38 } 39 40 exitErr, ok := err.(*exec.ExitError) 41 if !ok { 42 return 1 43 } 44 45 waitStatus, ok := exitErr.Sys().(syscall.WaitStatus) 46 if !ok { 47 // This is a fallback and should at least let us return something useful 48 // when running on Windows, even if it isn't completely accurate. 49 if exitErr.Success() { 50 return 0 51 } 52 53 return 1 54 } 55 56 return waitStatus.ExitStatus() 57} 58 59func sessionHandler(session ssh.Session) { 60 keyID := session.ConnPermissions().Extensions["forgejo-key-id"] 61 62 command := session.RawCommand() 63 64 log.Trace("SSH: Payload: %v", command) 65 66 args := []string{"--config=" + setting.CustomConf, "serv", "key-" + keyID} 67 log.Trace("SSH: Arguments: %v", args) 68 69 ctx, cancel := context.WithCancel(session.Context()) 70 defer cancel() 71 72 gitProtocol := "" 73 for _, env := range session.Environ() { 74 if strings.HasPrefix(env, "GIT_PROTOCOL=") { 75 _, gitProtocol, _ = strings.Cut(env, "=") 76 break 77 } 78 } 79 80 cmd := exec.CommandContext(ctx, setting.AppPath, args...) 81 cmd.Env = append( 82 os.Environ(), 83 "SSH_ORIGINAL_COMMAND="+command, 84 "SKIP_MINWINSVC=1", 85 "GIT_PROTOCOL="+gitProtocol, 86 ) 87 88 stdout, err := cmd.StdoutPipe() 89 if err != nil { 90 log.Error("SSH: StdoutPipe: %v", err) 91 return 92 } 93 defer stdout.Close() 94 95 stderr, err := cmd.StderrPipe() 96 if err != nil { 97 log.Error("SSH: StderrPipe: %v", err) 98 return 99 } 100 defer stderr.Close() 101 102 stdin, err := cmd.StdinPipe() 103 if err != nil { 104 log.Error("SSH: StdinPipe: %v", err) 105 return 106 } 107 defer stdin.Close() 108 109 process.SetSysProcAttribute(cmd) 110 111 wg := &sync.WaitGroup{} 112 wg.Add(2) 113 114 if err = cmd.Start(); err != nil { 115 log.Error("SSH: Start: %v", err) 116 return 117 } 118 119 go func() { 120 defer stdin.Close() 121 if _, err := io.Copy(stdin, session); err != nil { 122 log.Error("Failed to write session to stdin. %s", err) 123 } 124 }() 125 126 go func() { 127 defer wg.Done() 128 defer stdout.Close() 129 if _, err := io.Copy(session, stdout); err != nil { 130 log.Error("Failed to write stdout to session. %s", err) 131 } 132 }() 133 134 go func() { 135 defer wg.Done() 136 defer stderr.Close() 137 if _, err := io.Copy(session.Stderr(), stderr); err != nil { 138 log.Error("Failed to write stderr to session. %s", err) 139 } 140 }() 141 142 // Ensure all the output has been written before we wait on the command 143 // to exit. 144 wg.Wait() 145 146 // Wait for the command to exit and log any errors we get 147 err = cmd.Wait() 148 if err != nil { 149 // Cannot use errors.Is here because ExitError doesn't implement Is 150 // Thus errors.Is will do equality test NOT type comparison 151 if _, ok := err.(*exec.ExitError); !ok { 152 log.Error("SSH: Wait: %v", err) 153 } 154 } 155 156 if err := session.Exit(getExitStatusFromError(err)); err != nil && !errors.Is(err, io.EOF) { 157 log.Error("Session failed to exit. %s", err) 158 } 159} 160 161func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { 162 if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary 163 log.Debug("Handle Public Key: Fingerprint: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr()) 164 } 165 166 if ctx.User() != setting.SSH.BuiltinServerUser { 167 log.Warn("Invalid SSH username %s - must use %s for all git operations via ssh", ctx.User(), setting.SSH.BuiltinServerUser) 168 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 169 return false 170 } 171 172 // check if we have a certificate 173 if cert, ok := key.(*gossh.Certificate); ok { 174 if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary 175 log.Debug("Handle Certificate: %s Fingerprint: %s is a certificate", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) 176 } 177 178 if len(setting.SSH.TrustedUserCAKeys) == 0 { 179 log.Warn("Certificate Rejected: No trusted certificate authorities for this server") 180 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 181 return false 182 } 183 184 if cert.CertType != gossh.UserCert { 185 log.Warn("Certificate Rejected: Not a user certificate") 186 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 187 return false 188 } 189 190 // look for the exact principal 191 principalLoop: 192 for _, principal := range cert.ValidPrincipals { 193 pkey, err := asymkey_model.SearchPublicKeyByContentExact(ctx, principal) 194 if err != nil { 195 if asymkey_model.IsErrKeyNotExist(err) { 196 log.Debug("Principal Rejected: %s Unknown Principal: %s", ctx.RemoteAddr(), principal) 197 continue principalLoop 198 } 199 log.Error("SearchPublicKeyByContentExact: %v", err) 200 return false 201 } 202 203 c := &gossh.CertChecker{ 204 IsUserAuthority: func(auth gossh.PublicKey) bool { 205 marshaled := auth.Marshal() 206 for _, k := range setting.SSH.TrustedUserCAKeysParsed { 207 if bytes.Equal(marshaled, k.Marshal()) { 208 return true 209 } 210 } 211 212 return false 213 }, 214 } 215 216 // check the CA of the cert 217 if !c.IsUserAuthority(cert.SignatureKey) { 218 if log.IsDebug() { 219 log.Debug("Principal Rejected: %s Untrusted Authority Signature Fingerprint %s for Principal: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(cert.SignatureKey), principal) 220 } 221 continue principalLoop 222 } 223 224 // validate the cert for this principal 225 if err := c.CheckCert(principal, cert); err != nil { 226 // User is presenting an invalid certificate - STOP any further processing 227 log.Error("Invalid Certificate KeyID %s with Signature Fingerprint %s presented for Principal: %s from %s", cert.KeyId, gossh.FingerprintSHA256(cert.SignatureKey), principal, ctx.RemoteAddr()) 228 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 229 230 return false 231 } 232 233 if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary 234 log.Debug("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key), principal) 235 } 236 if ctx.Permissions().Extensions == nil { 237 ctx.Permissions().Extensions = map[string]string{} 238 } 239 ctx.Permissions().Extensions["forgejo-key-id"] = strconv.FormatInt(pkey.ID, 10) 240 241 return true 242 } 243 244 log.Warn("From %s Fingerprint: %s is a certificate, but no valid principals found", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) 245 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 246 return false 247 } 248 249 if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary 250 log.Debug("Handle Public Key: %s Fingerprint: %s is not a certificate", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) 251 } 252 253 pkey, err := asymkey_model.SearchPublicKeyByContent(ctx, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key)))) 254 if err != nil { 255 if asymkey_model.IsErrKeyNotExist(err) { 256 log.Warn("Unknown public key: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr()) 257 log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr()) 258 return false 259 } 260 log.Error("SearchPublicKeyByContent: %v", err) 261 return false 262 } 263 264 if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary 265 log.Debug("Successfully authenticated: %s Public Key Fingerprint: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) 266 } 267 if ctx.Permissions().Extensions == nil { 268 ctx.Permissions().Extensions = map[string]string{} 269 } 270 ctx.Permissions().Extensions["forgejo-key-id"] = strconv.FormatInt(pkey.ID, 10) 271 272 return true 273} 274 275// sshConnectionFailed logs a failed connection 276// - this mainly exists to give a nice function name in logging 277func sshConnectionFailed(conn net.Conn, err error) { 278 // Log the underlying error with a specific message 279 log.Warn("Failed connection from %s with error: %v", conn.RemoteAddr(), err) 280 // Log with the standard failed authentication from message for simpler fail2ban configuration 281 log.Warn("Failed authentication attempt from %s", conn.RemoteAddr()) 282} 283 284// Listen starts a SSH server listens on given port. 285func Listen(host string, port int, ciphers, keyExchanges, macs []string) { 286 srv := ssh.Server{ 287 Addr: net.JoinHostPort(host, strconv.Itoa(port)), 288 PublicKeyHandler: publicKeyHandler, 289 Handler: sessionHandler, 290 ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig { 291 config := &gossh.ServerConfig{} 292 config.KeyExchanges = keyExchanges 293 config.MACs = macs 294 config.Ciphers = ciphers 295 return config 296 }, 297 ConnectionFailedCallback: sshConnectionFailed, 298 // We need to explicitly disable the PtyCallback so text displays 299 // properly. 300 PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool { 301 return false 302 }, 303 } 304 305 keys := make([]string, 0, len(setting.SSH.ServerHostKeys)) 306 for _, key := range setting.SSH.ServerHostKeys { 307 isExist, err := util.IsExist(key) 308 if err != nil { 309 log.Fatal("Unable to check if %s exists. Error: %v", setting.SSH.ServerHostKeys, err) 310 } 311 if isExist { 312 keys = append(keys, key) 313 } 314 } 315 316 if len(keys) == 0 { 317 filePath := filepath.Dir(setting.SSH.ServerHostKeys[0]) 318 319 if err := os.MkdirAll(filePath, os.ModePerm); err != nil { 320 log.Error("Failed to create dir %s: %v", filePath, err) 321 } 322 323 err := GenKeyPair(setting.SSH.ServerHostKeys[0]) 324 if err != nil { 325 log.Fatal("Failed to generate private key: %v", err) 326 } 327 log.Trace("New private key is generated: %s", setting.SSH.ServerHostKeys[0]) 328 keys = append(keys, setting.SSH.ServerHostKeys[0]) 329 } 330 331 for _, key := range keys { 332 log.Info("Adding SSH host key: %s", key) 333 err := srv.SetOption(ssh.HostKeyFile(key)) 334 if err != nil { 335 log.Error("Failed to set Host Key. %s", err) 336 } 337 } 338 339 go func() { 340 _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Service: Built-in SSH server", process.SystemProcessType, true) 341 defer finished() 342 listen(&srv) 343 }() 344} 345 346// GenKeyPair make a pair of public and private keys for SSH access. 347// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. 348// Private Key generated is PEM encoded 349func GenKeyPair(keyPath string) error { 350 privateKey, err := rsa.GenerateKey(rand.Reader, 4096) 351 if err != nil { 352 return err 353 } 354 355 privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} 356 f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) 357 if err != nil { 358 return err 359 } 360 defer func() { 361 if err = f.Close(); err != nil { 362 log.Error("Close: %v", err) 363 } 364 }() 365 366 if err := pem.Encode(f, privateKeyPEM); err != nil { 367 return err 368 } 369 370 // generate public key 371 pub, err := gossh.NewPublicKey(&privateKey.PublicKey) 372 if err != nil { 373 return err 374 } 375 376 public := gossh.MarshalAuthorizedKey(pub) 377 p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) 378 if err != nil { 379 return err 380 } 381 defer func() { 382 if err = p.Close(); err != nil { 383 log.Error("Close: %v", err) 384 } 385 }() 386 _, err = p.Write(public) 387 return err 388}