2019-02-07 15:13:12 +08:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-07-26 00:24:27 -04:00
package ssh
import (
2020-10-11 02:38:09 +02:00
"bytes"
2021-07-14 15:43:13 +01:00
"context"
2019-02-07 15:13:12 +08:00
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
2022-07-28 20:56:55 +01:00
"errors"
2015-11-08 16:59:56 -05:00
"io"
2021-06-28 18:05:27 +01:00
"net"
2014-07-26 00:24:27 -04:00
"os"
"os/exec"
2015-11-08 16:59:56 -05:00
"path/filepath"
2024-12-13 11:57:37 +08:00
"reflect"
2021-11-06 07:23:32 +01:00
"strconv"
2014-07-26 00:24:27 -04:00
"strings"
2019-07-06 21:28:09 -04:00
"sync"
"syscall"
2014-07-26 00:24:27 -04:00
2021-12-10 16:14:24 +08:00
asymkey_model "code.gitea.io/gitea/models/asymkey"
2022-03-31 18:01:43 +01:00
"code.gitea.io/gitea/modules/graceful"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/log"
2022-03-31 18:01:43 +01:00
"code.gitea.io/gitea/modules/process"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2020-11-28 02:42:08 +00:00
"code.gitea.io/gitea/modules/util"
2019-07-06 21:28:09 -04:00
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
2014-07-26 00:24:27 -04:00
)
2024-12-13 11:57:37 +08:00
// The ssh auth overall works like this:
// NewServerConn:
// serverHandshake+serverAuthenticate:
// PublicKeyCallback:
// PublicKeyHandler (our code):
// reset(ctx.Permissions) and set ctx.Permissions.giteaKeyID = keyID
// pubKey.Verify
// return ctx.Permissions // only reaches here, the pub key is really authenticated
// set conn.Permissions from serverAuthenticate
// sessionHandler(conn)
//
// Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one.
// Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key),
// then only A succeeds to authenticate, sessionHandler will see B's keyID
2024-12-15 14:07:50 +08:00
//
// After x/crypto >= 0.31.0 (fix CVE-2024-45337), the PublicKeyCallback will be called again for the verified key,
// it mitigates the misuse for most cases, it's still good for us to make sure we don't rely on that mitigation
// and do not misuse the PublicKeyCallback: we should only use the verified keyID from the verified ssh conn.
2024-12-13 11:57:37 +08:00
const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id"
2019-07-06 21:28:09 -04:00
func getExitStatusFromError ( err error ) int {
if err == nil {
return 0
2015-11-08 16:59:56 -05:00
}
2019-07-06 21:28:09 -04:00
exitErr , ok := err . ( * exec . ExitError )
if ! ok {
return 1
}
2015-11-08 16:59:56 -05:00
2019-07-06 21:28:09 -04:00
waitStatus , ok := exitErr . Sys ( ) . ( syscall . WaitStatus )
if ! ok {
// This is a fallback and should at least let us return something useful
// when running on Windows, even if it isn't completely accurate.
if exitErr . Success ( ) {
return 0
2014-07-26 00:24:27 -04:00
}
2019-07-06 21:28:09 -04:00
return 1
2014-07-26 00:24:27 -04:00
}
2019-07-06 21:28:09 -04:00
return waitStatus . ExitStatus ( )
2014-07-26 00:24:27 -04:00
}
2024-12-13 11:57:37 +08:00
// sessionPartial is the private struct from "gliderlabs/ssh/session.go"
// We need to read the original "conn" field from "ssh.Session interface" which contains the "*session pointer"
// https://github.com/gliderlabs/ssh/blob/d137aad99cd6f2d9495bfd98c755bec4e5dffb8c/session.go#L109-L113
// If upstream fixes the problem and/or changes the struct, we need to follow.
// If the struct mismatches, the builtin ssh server will fail during integration tests.
type sessionPartial struct {
sync . Mutex
gossh . Channel
conn * gossh . ServerConn
}
func ptr [ T any ] ( intf any ) * T {
// https://pkg.go.dev/unsafe#Pointer
// (1) Conversion of a *T1 to Pointer to *T2.
// Provided that T2 is no larger than T1 and that the two share an equivalent memory layout,
// this conversion allows reinterpreting data of one type as data of another type.
v := reflect . ValueOf ( intf )
p := v . UnsafePointer ( )
return ( * T ) ( p )
}
2019-07-06 21:28:09 -04:00
func sessionHandler ( session ssh . Session ) {
2024-12-13 11:57:37 +08:00
// here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one.
// so we must use the original ssh conn, which always contains the correct (verified) keyID.
2024-12-15 14:07:50 +08:00
sshSession := ptr [ sessionPartial ] ( session )
keyID := sshSession . conn . Permissions . Extensions [ giteaPermissionExtensionKeyID ]
2019-07-06 21:28:09 -04:00
command := session . RawCommand ( )
log . Trace ( "SSH: Payload: %v" , command )
2023-06-21 13:50:26 +08:00
args := [ ] string { "--config=" + setting . CustomConf , "serv" , "key-" + keyID }
2019-07-06 21:28:09 -04:00
log . Trace ( "SSH: Arguments: %v" , args )
2021-07-14 15:43:13 +01:00
ctx , cancel := context . WithCancel ( session . Context ( ) )
defer cancel ( )
2022-08-02 09:56:38 +02:00
gitProtocol := ""
for _ , env := range session . Environ ( ) {
if strings . HasPrefix ( env , "GIT_PROTOCOL=" ) {
2022-08-03 00:34:50 +02:00
_ , gitProtocol , _ = strings . Cut ( env , "=" )
2022-08-02 09:56:38 +02:00
break
}
}
2021-07-14 15:43:13 +01:00
cmd := exec . CommandContext ( ctx , setting . AppPath , args ... )
2019-07-06 21:28:09 -04:00
cmd . Env = append (
os . Environ ( ) ,
"SSH_ORIGINAL_COMMAND=" + command ,
"SKIP_MINWINSVC=1" ,
2022-08-02 09:56:38 +02:00
"GIT_PROTOCOL=" + gitProtocol ,
2019-07-06 21:28:09 -04:00
)
stdout , err := cmd . StdoutPipe ( )
2014-07-26 00:24:27 -04:00
if err != nil {
2019-07-06 21:28:09 -04:00
log . Error ( "SSH: StdoutPipe: %v" , err )
return
2014-07-26 00:24:27 -04:00
}
2021-07-14 15:43:13 +01:00
defer stdout . Close ( )
2019-07-06 21:28:09 -04:00
stderr , err := cmd . StderrPipe ( )
if err != nil {
log . Error ( "SSH: StderrPipe: %v" , err )
return
}
2021-07-14 15:43:13 +01:00
defer stderr . Close ( )
2019-07-06 21:28:09 -04:00
stdin , err := cmd . StdinPipe ( )
if err != nil {
log . Error ( "SSH: StdinPipe: %v" , err )
return
}
2021-07-14 15:43:13 +01:00
defer stdin . Close ( )
2019-07-06 21:28:09 -04:00
2022-06-03 15:36:18 +01:00
process . SetSysProcAttribute ( cmd )
2019-07-06 21:28:09 -04:00
wg := & sync . WaitGroup { }
wg . Add ( 2 )
if err = cmd . Start ( ) ; err != nil {
log . Error ( "SSH: Start: %v" , err )
return
}
go func ( ) {
defer stdin . Close ( )
if _ , err := io . Copy ( stdin , session ) ; err != nil {
log . Error ( "Failed to write session to stdin. %s" , err )
}
} ( )
go func ( ) {
defer wg . Done ( )
2021-07-14 15:43:13 +01:00
defer stdout . Close ( )
2019-07-06 21:28:09 -04:00
if _ , err := io . Copy ( session , stdout ) ; err != nil {
log . Error ( "Failed to write stdout to session. %s" , err )
}
} ( )
go func ( ) {
defer wg . Done ( )
2021-07-14 15:43:13 +01:00
defer stderr . Close ( )
2019-07-06 21:28:09 -04:00
if _ , err := io . Copy ( session . Stderr ( ) , stderr ) ; err != nil {
log . Error ( "Failed to write stderr to session. %s" , err )
2014-07-26 00:24:27 -04:00
}
2019-07-06 21:28:09 -04:00
} ( )
// Ensure all the output has been written before we wait on the command
// to exit.
wg . Wait ( )
// Wait for the command to exit and log any errors we get
err = cmd . Wait ( )
if err != nil {
2022-07-28 20:56:55 +01:00
// Cannot use errors.Is here because ExitError doesn't implement Is
// Thus errors.Is will do equality test NOT type comparison
if _ , ok := err . ( * exec . ExitError ) ; ! ok {
log . Error ( "SSH: Wait: %v" , err )
}
2019-07-06 21:28:09 -04:00
}
2022-07-28 20:56:55 +01:00
if err := session . Exit ( getExitStatusFromError ( err ) ) ; err != nil && ! errors . Is ( err , io . EOF ) {
2019-07-06 21:28:09 -04:00
log . Error ( "Session failed to exit. %s" , err )
}
}
func publicKeyHandler ( ctx ssh . Context , key ssh . PublicKey ) bool {
2024-12-13 11:57:37 +08:00
// The publicKeyHandler (PublicKeyCallback) only helps to provide the candidate keys to authenticate,
// It does NOT really verify here, so we could only record the related information here.
// After authentication (Verify), the "Permissions" will be assigned to the ssh conn,
// then we can use it in the "session handler"
// first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does)
// it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions"
ctx . Permissions ( ) . Permissions = & gossh . Permissions { }
setPermExt := func ( keyID int64 ) {
ctx . Permissions ( ) . Permissions . Extensions = map [ string ] string {
2025-04-01 12:14:01 +02:00
giteaPermissionExtensionKeyID : strconv . FormatInt ( keyID , 10 ) ,
2024-12-13 11:57:37 +08:00
}
}
2020-12-15 08:45:13 +00:00
if log . IsDebug ( ) { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
log . Debug ( "Handle Public Key: Fingerprint: %s from %s" , gossh . FingerprintSHA256 ( key ) , ctx . RemoteAddr ( ) )
}
2019-07-06 21:28:09 -04:00
if ctx . User ( ) != setting . SSH . BuiltinServerUser {
2020-12-15 08:45:13 +00:00
log . Warn ( "Invalid SSH username %s - must use %s for all git operations via ssh" , ctx . User ( ) , setting . SSH . BuiltinServerUser )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2019-07-06 21:28:09 -04:00
return false
}
2016-02-01 12:10:49 -05:00
2020-10-11 02:38:09 +02:00
// check if we have a certificate
if cert , ok := key . ( * gossh . Certificate ) ; ok {
2020-12-15 08:45:13 +00:00
if log . IsDebug ( ) { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
log . Debug ( "Handle Certificate: %s Fingerprint: %s is a certificate" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( key ) )
}
2020-10-11 02:38:09 +02:00
if len ( setting . SSH . TrustedUserCAKeys ) == 0 {
2020-12-15 08:45:13 +00:00
log . Warn ( "Certificate Rejected: No trusted certificate authorities for this server" )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-10-11 02:38:09 +02:00
return false
}
2023-09-01 15:45:22 +02:00
if cert . CertType != gossh . UserCert {
log . Warn ( "Certificate Rejected: Not a user certificate" )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
return false
}
2020-10-11 02:38:09 +02:00
// look for the exact principal
2020-12-11 22:52:38 +00:00
principalLoop :
2020-10-11 02:38:09 +02:00
for _ , principal := range cert . ValidPrincipals {
2023-11-03 16:21:05 +01:00
pkey , err := asymkey_model . SearchPublicKeyByContentExact ( ctx , principal )
2020-10-11 02:38:09 +02:00
if err != nil {
2021-12-10 16:14:24 +08:00
if asymkey_model . IsErrKeyNotExist ( err ) {
2020-12-15 08:45:13 +00:00
log . Debug ( "Principal Rejected: %s Unknown Principal: %s" , ctx . RemoteAddr ( ) , principal )
2020-12-11 22:52:38 +00:00
continue principalLoop
}
2020-10-11 02:38:09 +02:00
log . Error ( "SearchPublicKeyByContentExact: %v" , err )
return false
}
c := & gossh . CertChecker {
IsUserAuthority : func ( auth gossh . PublicKey ) bool {
2022-06-05 09:16:14 +02:00
marshaled := auth . Marshal ( )
2020-10-11 02:38:09 +02:00
for _ , k := range setting . SSH . TrustedUserCAKeysParsed {
2022-06-05 09:16:14 +02:00
if bytes . Equal ( marshaled , k . Marshal ( ) ) {
2020-10-11 02:38:09 +02:00
return true
}
}
return false
} ,
}
// check the CA of the cert
if ! c . IsUserAuthority ( cert . SignatureKey ) {
2020-12-15 08:45:13 +00:00
if log . IsDebug ( ) {
log . Debug ( "Principal Rejected: %s Untrusted Authority Signature Fingerprint %s for Principal: %s" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( cert . SignatureKey ) , principal )
}
2020-12-11 22:52:38 +00:00
continue principalLoop
2020-10-11 02:38:09 +02:00
}
// validate the cert for this principal
if err := c . CheckCert ( principal , cert ) ; err != nil {
2020-12-15 08:45:13 +00:00
// User is presenting an invalid certificate - STOP any further processing
2023-05-22 06:35:11 +08:00
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 ( ) )
2020-12-15 08:45:13 +00:00
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-10-11 02:38:09 +02:00
return false
}
2020-12-15 08:45:13 +00:00
if log . IsDebug ( ) { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
log . Debug ( "Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( key ) , principal )
}
2024-12-13 11:57:37 +08:00
setPermExt ( pkey . ID )
2020-10-11 02:38:09 +02:00
return true
}
2020-12-15 08:45:13 +00:00
2023-05-22 06:35:11 +08:00
log . Warn ( "From %s Fingerprint: %s is a certificate, but no valid principals found" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( key ) )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-12-15 08:45:13 +00:00
return false
}
if log . IsDebug ( ) { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
log . Debug ( "Handle Public Key: %s Fingerprint: %s is not a certificate" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( key ) )
2020-10-11 02:38:09 +02:00
}
2023-11-03 16:21:05 +01:00
pkey , err := asymkey_model . SearchPublicKeyByContent ( ctx , strings . TrimSpace ( string ( gossh . MarshalAuthorizedKey ( key ) ) ) )
2019-07-06 21:28:09 -04:00
if err != nil {
2021-12-10 16:14:24 +08:00
if asymkey_model . IsErrKeyNotExist ( err ) {
2023-05-22 06:35:11 +08:00
log . Warn ( "Unknown public key: %s from %s" , gossh . FingerprintSHA256 ( key ) , ctx . RemoteAddr ( ) )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-12-11 22:52:38 +00:00
return false
}
2020-12-15 08:45:13 +00:00
log . Error ( "SearchPublicKeyByContent: %v" , err )
2019-07-06 21:28:09 -04:00
return false
2014-07-26 00:24:27 -04:00
}
2019-07-06 21:28:09 -04:00
2020-12-15 08:45:13 +00:00
if log . IsDebug ( ) { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
log . Debug ( "Successfully authenticated: %s Public Key Fingerprint: %s" , ctx . RemoteAddr ( ) , gossh . FingerprintSHA256 ( key ) )
}
2024-12-13 11:57:37 +08:00
setPermExt ( pkey . ID )
2019-07-06 21:28:09 -04:00
return true
2014-07-26 00:24:27 -04:00
}
2021-06-28 18:05:27 +01:00
// sshConnectionFailed logs a failed connection
// - this mainly exists to give a nice function name in logging
func sshConnectionFailed ( conn net . Conn , err error ) {
// Log the underlying error with a specific message
log . Warn ( "Failed connection from %s with error: %v" , conn . RemoteAddr ( ) , err )
// Log with the standard failed authentication from message for simpler fail2ban configuration
log . Warn ( "Failed authentication attempt from %s" , conn . RemoteAddr ( ) )
}
2025-06-10 03:51:02 +08:00
// Listen starts an SSH server listening on given port.
2021-12-20 05:41:31 +01:00
func Listen ( host string , port int , ciphers , keyExchanges , macs [ ] string ) {
2019-07-06 21:28:09 -04:00
srv := ssh . Server {
2021-11-06 07:23:32 +01:00
Addr : net . JoinHostPort ( host , strconv . Itoa ( port ) ) ,
2019-07-06 21:28:09 -04:00
PublicKeyHandler : publicKeyHandler ,
Handler : sessionHandler ,
2021-01-30 14:20:32 +01:00
ServerConfigCallback : func ( ctx ssh . Context ) * gossh . ServerConfig {
config := & gossh . ServerConfig { }
config . KeyExchanges = keyExchanges
config . MACs = macs
config . Ciphers = ciphers
return config
} ,
2021-06-28 18:05:27 +01:00
ConnectionFailedCallback : sshConnectionFailed ,
2019-07-06 21:28:09 -04:00
// We need to explicitly disable the PtyCallback so text displays
// properly.
PtyCallback : func ( ctx ssh . Context , pty ssh . Pty ) bool {
return false
2014-07-26 00:24:27 -04:00
} ,
}
2021-03-08 02:43:59 +00:00
keys := make ( [ ] string , 0 , len ( setting . SSH . ServerHostKeys ) )
for _ , key := range setting . SSH . ServerHostKeys {
isExist , err := util . IsExist ( key )
if err != nil {
log . Fatal ( "Unable to check if %s exists. Error: %v" , setting . SSH . ServerHostKeys , err )
}
if isExist {
keys = append ( keys , key )
}
2020-11-28 02:42:08 +00:00
}
2021-03-08 02:43:59 +00:00
if len ( keys ) == 0 {
filePath := filepath . Dir ( setting . SSH . ServerHostKeys [ 0 ] )
2016-12-01 00:56:15 +01:00
if err := os . MkdirAll ( filePath , os . ModePerm ) ; err != nil {
2019-04-02 08:48:31 +01:00
log . Error ( "Failed to create dir %s: %v" , filePath , err )
2016-12-01 00:56:15 +01:00
}
2021-03-08 02:43:59 +00:00
err := GenKeyPair ( setting . SSH . ServerHostKeys [ 0 ] )
2015-11-14 13:21:31 -05:00
if err != nil {
2019-04-02 08:48:31 +01:00
log . Fatal ( "Failed to generate private key: %v" , err )
2015-11-14 13:21:31 -05:00
}
2021-03-08 02:43:59 +00:00
log . Trace ( "New private key is generated: %s" , setting . SSH . ServerHostKeys [ 0 ] )
keys = append ( keys , setting . SSH . ServerHostKeys [ 0 ] )
2015-11-14 13:21:31 -05:00
}
2021-03-08 02:43:59 +00:00
for _ , key := range keys {
log . Info ( "Adding SSH host key: %s" , key )
err := srv . SetOption ( ssh . HostKeyFile ( key ) )
if err != nil {
log . Error ( "Failed to set Host Key. %s" , err )
}
2014-07-26 00:24:27 -04:00
}
2022-03-31 18:01:43 +01:00
go func ( ) {
_ , _ , finished := process . GetManager ( ) . AddTypedContext ( graceful . GetManager ( ) . HammerContext ( ) , "Service: Built-in SSH server" , process . SystemProcessType , true )
defer finished ( )
listen ( & srv )
} ( )
2014-07-26 00:24:27 -04:00
}
2019-02-07 15:13:12 +08:00
// GenKeyPair make a pair of public and private keys for SSH access.
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
// Private Key generated is PEM encoded
func GenKeyPair ( keyPath string ) error {
2021-03-08 02:43:59 +00:00
privateKey , err := rsa . GenerateKey ( rand . Reader , 4096 )
2019-02-07 15:13:12 +08:00
if err != nil {
return err
}
privateKeyPEM := & pem . Block { Type : "RSA PRIVATE KEY" , Bytes : x509 . MarshalPKCS1PrivateKey ( privateKey ) }
2022-01-20 18:46:10 +01:00
f , err := os . OpenFile ( keyPath , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0 o600 )
2019-02-07 15:13:12 +08:00
if err != nil {
return err
}
2019-06-12 21:41:28 +02:00
defer func ( ) {
if err = f . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 15:13:12 +08:00
if err := pem . Encode ( f , privateKeyPEM ) ; err != nil {
return err
}
// generate public key
2019-07-06 21:28:09 -04:00
pub , err := gossh . NewPublicKey ( & privateKey . PublicKey )
2019-02-07 15:13:12 +08:00
if err != nil {
return err
}
2019-07-06 21:28:09 -04:00
public := gossh . MarshalAuthorizedKey ( pub )
2022-01-20 18:46:10 +01:00
p , err := os . OpenFile ( keyPath + ".pub" , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0 o600 )
2019-02-07 15:13:12 +08:00
if err != nil {
return err
}
2019-06-12 21:41:28 +02:00
defer func ( ) {
if err = p . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 15:13:12 +08:00
_ , err = p . Write ( public )
return err
}