2019-02-07 02:13:12 -05:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2014-07-26 00:24:27 -04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package ssh
import (
2020-10-10 20:38:09 -04:00
"bytes"
2019-02-07 02:13:12 -05:00
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
2019-07-06 21:28:09 -04:00
"fmt"
2015-11-08 16:59:56 -05:00
"io"
2021-06-28 13:05:27 -04:00
"net"
2014-07-26 00:24:27 -04:00
"os"
"os/exec"
2015-11-08 16:59:56 -05:00
"path/filepath"
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
2016-11-10 11:24:48 -05:00
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2020-11-27 21:42:08 -05: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
)
2019-07-06 21:28:09 -04:00
type contextKey string
const giteaKeyID = contextKey ( "gitea-key-id" )
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
}
2019-07-06 21:28:09 -04:00
func sessionHandler ( session ssh . Session ) {
2020-12-25 04:59:32 -05:00
keyID := fmt . Sprintf ( "%d" , session . Context ( ) . Value ( giteaKeyID ) . ( int64 ) )
2019-07-06 21:28:09 -04:00
command := session . RawCommand ( )
log . Trace ( "SSH: Payload: %v" , command )
2020-12-25 04:59:32 -05:00
args := [ ] string { "serv" , "key-" + keyID , "--config=" + setting . CustomConf }
2019-07-06 21:28:09 -04:00
log . Trace ( "SSH: Arguments: %v" , args )
2021-06-30 16:07:23 -04:00
cmd := exec . CommandContext ( session . Context ( ) , setting . AppPath , args ... )
2019-07-06 21:28:09 -04:00
cmd . Env = append (
os . Environ ( ) ,
"SSH_ORIGINAL_COMMAND=" + command ,
"SKIP_MINWINSVC=1" ,
)
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
}
2019-07-06 21:28:09 -04:00
stderr , err := cmd . StderrPipe ( )
if err != nil {
log . Error ( "SSH: StderrPipe: %v" , err )
return
}
stdin , err := cmd . StdinPipe ( )
if err != nil {
log . Error ( "SSH: StdinPipe: %v" , err )
return
}
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 ( )
if _ , err := io . Copy ( session , stdout ) ; err != nil {
log . Error ( "Failed to write stdout to session. %s" , err )
}
} ( )
go func ( ) {
defer wg . Done ( )
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 {
log . Error ( "SSH: Wait: %v" , err )
}
if err := session . Exit ( getExitStatusFromError ( err ) ) ; err != nil {
log . Error ( "Session failed to exit. %s" , err )
}
}
func publicKeyHandler ( ctx ssh . Context , key ssh . PublicKey ) bool {
2020-12-15 03:45:13 -05: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 03:45:13 -05: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-10 20:38:09 -04:00
// check if we have a certificate
if cert , ok := key . ( * gossh . Certificate ) ; ok {
2020-12-15 03:45:13 -05: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-10 20:38:09 -04:00
if len ( setting . SSH . TrustedUserCAKeys ) == 0 {
2020-12-15 03:45:13 -05:00
log . Warn ( "Certificate Rejected: No trusted certificate authorities for this server" )
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-10-10 20:38:09 -04:00
return false
}
// look for the exact principal
2020-12-11 17:52:38 -05:00
principalLoop :
2020-10-10 20:38:09 -04:00
for _ , principal := range cert . ValidPrincipals {
pkey , err := models . SearchPublicKeyByContentExact ( principal )
if err != nil {
2020-12-11 17:52:38 -05:00
if models . IsErrKeyNotExist ( err ) {
2020-12-15 03:45:13 -05:00
log . Debug ( "Principal Rejected: %s Unknown Principal: %s" , ctx . RemoteAddr ( ) , principal )
2020-12-11 17:52:38 -05:00
continue principalLoop
}
2020-10-10 20:38:09 -04:00
log . Error ( "SearchPublicKeyByContentExact: %v" , err )
return false
}
c := & gossh . CertChecker {
IsUserAuthority : func ( auth gossh . PublicKey ) bool {
for _ , k := range setting . SSH . TrustedUserCAKeysParsed {
if bytes . Equal ( auth . Marshal ( ) , k . Marshal ( ) ) {
return true
}
}
return false
} ,
}
// check the CA of the cert
if ! c . IsUserAuthority ( cert . SignatureKey ) {
2020-12-15 03:45:13 -05: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 17:52:38 -05:00
continue principalLoop
2020-10-10 20:38:09 -04:00
}
// validate the cert for this principal
if err := c . CheckCert ( principal , cert ) ; err != nil {
2020-12-15 03:45:13 -05:00
// User is presenting an invalid certificate - STOP any further processing
if log . IsError ( ) {
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 ( ) )
}
log . Warn ( "Failed authentication attempt from %s" , ctx . RemoteAddr ( ) )
2020-10-10 20:38:09 -04:00
return false
}
2020-12-15 03:45:13 -05: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 )
}
2020-10-10 20:38:09 -04:00
ctx . SetValue ( giteaKeyID , pkey . ID )
return true
}
2020-12-15 03:45:13 -05:00
if log . IsWarn ( ) {
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 ( ) )
}
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-10 20:38:09 -04:00
}
2019-07-06 21:28:09 -04:00
pkey , err := models . SearchPublicKeyByContent ( strings . TrimSpace ( string ( gossh . MarshalAuthorizedKey ( key ) ) ) )
if err != nil {
2020-12-11 17:52:38 -05:00
if models . IsErrKeyNotExist ( err ) {
2020-12-15 03:45:13 -05:00
if log . IsWarn ( ) {
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 17:52:38 -05:00
return false
}
2020-12-15 03:45:13 -05: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 03:45:13 -05: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 ) )
}
2019-07-06 21:28:09 -04:00
ctx . SetValue ( giteaKeyID , pkey . ID )
return true
2014-07-26 00:24:27 -04:00
}
2021-06-28 13:05:27 -04: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 ( ) )
}
2014-07-26 00:24:27 -04:00
// Listen starts a SSH server listens on given port.
2017-11-02 11:26:41 -04:00
func Listen ( host string , port int , ciphers [ ] string , keyExchanges [ ] string , macs [ ] string ) {
2019-07-06 21:28:09 -04:00
srv := ssh . Server {
Addr : fmt . Sprintf ( "%s:%d" , host , port ) ,
PublicKeyHandler : publicKeyHandler ,
Handler : sessionHandler ,
2021-01-30 08:20:32 -05: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 13:05:27 -04: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-07 21:43:59 -05: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-27 21:42:08 -05:00
}
2021-03-07 21:43:59 -05:00
if len ( keys ) == 0 {
filePath := filepath . Dir ( setting . SSH . ServerHostKeys [ 0 ] )
2016-11-30 18:56:15 -05:00
if err := os . MkdirAll ( filePath , os . ModePerm ) ; err != nil {
2019-04-02 03:48:31 -04:00
log . Error ( "Failed to create dir %s: %v" , filePath , err )
2016-11-30 18:56:15 -05:00
}
2021-03-07 21:43:59 -05:00
err := GenKeyPair ( setting . SSH . ServerHostKeys [ 0 ] )
2015-11-14 13:21:31 -05:00
if err != nil {
2019-04-02 03:48:31 -04:00
log . Fatal ( "Failed to generate private key: %v" , err )
2015-11-14 13:21:31 -05:00
}
2021-03-07 21:43:59 -05: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-07 21:43:59 -05: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
}
2019-10-15 09:39:51 -04:00
go listen ( & srv )
2019-07-06 21:28:09 -04:00
2014-07-26 00:24:27 -04:00
}
2019-02-07 02:13:12 -05: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-07 21:43:59 -05:00
privateKey , err := rsa . GenerateKey ( rand . Reader , 4096 )
2019-02-07 02:13:12 -05:00
if err != nil {
return err
}
privateKeyPEM := & pem . Block { Type : "RSA PRIVATE KEY" , Bytes : x509 . MarshalPKCS1PrivateKey ( privateKey ) }
f , err := os . OpenFile ( keyPath , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0600 )
if err != nil {
return err
}
2019-06-12 15:41:28 -04:00
defer func ( ) {
if err = f . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 02:13:12 -05: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 02:13:12 -05:00
if err != nil {
return err
}
2019-07-06 21:28:09 -04:00
public := gossh . MarshalAuthorizedKey ( pub )
2019-02-07 02:13:12 -05:00
p , err := os . OpenFile ( keyPath + ".pub" , os . O_RDWR | os . O_CREATE | os . O_TRUNC , 0600 )
if err != nil {
return err
}
2019-06-12 15:41:28 -04:00
defer func ( ) {
if err = p . Close ( ) ; err != nil {
log . Error ( "Close: %v" , err )
}
} ( )
2019-02-07 02:13:12 -05:00
_ , err = p . Write ( public )
return err
}