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