From d109c72f669f614d0e9fcbda1d85b2b02420fa93 Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Tue, 6 Aug 2024 22:03:36 +0200 Subject: [PATCH] removed password based access authorized keys can now be modified within the session. keep last set of keys when no valid keys were found and keys are changed during the session . --- cmd/agent/agent.go | 39 ++++-------- cmd/agent/sshauthorizedkeys.go | 113 ++++++++++++++++++++++++++++++--- cmd/converge/converge.go | 10 +-- compose.yaml | 1 - go.mod | 2 +- kubernetes/deployment.yaml | 5 +- pkg/agent/session/session.go | 10 ++- pkg/comms/events.go | 1 - 8 files changed, 126 insertions(+), 55 deletions(-) diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 897a5b1..6fc2070 100755 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -77,8 +77,7 @@ func (user UserActivityDetector) Write(p []byte) (int, error) { } func sshServer(hostKeyFile string, shellCommand string, - passwordHandler ssh.PasswordHandler, - authorizedPublicKeys AuthorizedPublicKeys) *ssh.Server { + authorizedPublicKeys *AuthorizedPublicKeys) *ssh.Server { ssh.Handle(func(sshSession ssh.Session) { workingDirectory, _ := os.Getwd() env := append(os.Environ(), fmt.Sprintf("agentdir=%s", workingDirectory)) @@ -106,7 +105,6 @@ func sshServer(hostKeyFile string, shellCommand string, log.Println("starting ssh server, waiting for debug sessions") server := ssh.Server{ - PasswordHandler: passwordHandler, PublicKeyHandler: authorizedPublicKeys.authorize, SubsystemHandlers: map[string]ssh.SubsystemHandler{ "sftp": SftpHandler, @@ -185,9 +183,7 @@ func printHelp(msg string) { "Here is the unique id of the agent that allows rendez-vous with an end-user.\n" + "The end-user must specify the same id when connecting using ssh.\n" + "\n" + - "--id: rendez-vous id. When specified an SSH authorized key must be used and password\n" + - " based access is disabled. When not specified a random id is chosen by the agent and\n" + - " password based access is possible. The password is configured on the converge server\n" + + "--id: rendez-vous id, this is the id used to connect agents and clients. \n" + "--authorized-keys: SSH authorized keys file in openssh format. By default .authorized_keys in the\n" + " directory where the agent is started is used.\n" + "--warning-time: advance warning time before sessio ends (default '5m')\n" + @@ -333,15 +329,20 @@ func main() { // Authentiocation - passwordHandler, authorizedKeys := setupAuthentication( - commChannel, - serverInfo.UserPassword, - authorizedKeysFile) + authorizedKeys := NewAuthorizedPublicKeys(authorizedKeysFile) + // initial check + pubkeys := authorizedKeys.Parse() + if len(pubkeys) == 0 { + log.Printf("No public keys found in '%s', exiting", authorizedKeysFile) + os.Exit(1) + } + + go comms.ListenForServerEvents(commChannel) var service AgentService service = ListenerServer(func() *ssh.Server { - return sshServer("hostkey.pem", shell, passwordHandler, authorizedKeys) + return sshServer("hostkey.pem", shell, authorizedKeys) }) //service = ConnectionServer(netCatServer) //service = ConnectionServer(echoServer) @@ -376,22 +377,6 @@ func main() { service.Run(listener) } -func setupAuthentication(commChannel comms.CommChannel, - userPassword comms.UserPassword, - authorizedKeysFile string) (func(ctx ssh.Context, password string) bool, AuthorizedPublicKeys) { - - passwordHandler := func(ctx ssh.Context, password string) bool { - // Replace with your own logic to validate username and password - return ctx.User() == userPassword.Username && password == userPassword.Password - } - go comms.ListenForServerEvents(commChannel) - authorizedKeys := ParseOpenSSHAuthorizedKeysFile(authorizedKeysFile) - if len(authorizedKeys.keys) > 0 { - log.Printf("A total of %d authorized ssh keys were found", len(authorizedKeys.keys)) - } - return passwordHandler, authorizedKeys -} - func chooseShell(shells []string) string { log.Printf("Shell search path is %v", shells) var err error diff --git a/cmd/agent/sshauthorizedkeys.go b/cmd/agent/sshauthorizedkeys.go index 5f6ba57..7f49362 100644 --- a/cmd/agent/sshauthorizedkeys.go +++ b/cmd/agent/sshauthorizedkeys.go @@ -2,12 +2,16 @@ package main import ( "bufio" + "converge/pkg/agent/session" "fmt" + "github.com/fsnotify/fsnotify" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" "log" "os" + "path/filepath" "strings" + "sync" ) func publicKeyHandler(ctx ssh.Context, key gossh.PublicKey, authorizedKey gossh.PublicKey) bool { @@ -54,26 +58,115 @@ func readSshPublicKeys(fileName string) ([]ssh.PublicKey, error) { } type AuthorizedPublicKeys struct { - keys []ssh.PublicKey + authorizedKeysFile string + mutex sync.Mutex + publicKeys []ssh.PublicKey } -func ParseOpenSSHAuthorizedKeysFile(authorizedKeysFile string) AuthorizedPublicKeys { - if authorizedKeysFile == "" { - return AuthorizedPublicKeys{} +func NewAuthorizedPublicKeys(authorizedKeysFile string) *AuthorizedPublicKeys { + pubkeys := AuthorizedPublicKeys{ + authorizedKeysFile: authorizedKeysFile, + mutex: sync.Mutex{}, + publicKeys: nil, } - keys, err := readSshPublicKeys(authorizedKeysFile) + go pubkeys.monitorAuthorizedKeysFile(authorizedKeysFile) + return &pubkeys +} + +func (pubkeys *AuthorizedPublicKeys) notifyUsers(message string) { + session.MessageUsers(message) +} + +func (pubkeys *AuthorizedPublicKeys) setPubKeys(keys []ssh.PublicKey) { + pubkeys.mutex.Lock() + defer pubkeys.mutex.Unlock() + pubkeys.publicKeys = keys +} + +func (pubkeys *AuthorizedPublicKeys) getPubKeys() []ssh.PublicKey { + pubkeys.mutex.Lock() + defer pubkeys.mutex.Unlock() + return pubkeys.publicKeys +} + +func (pubkeys *AuthorizedPublicKeys) monitorAuthorizedKeysFile(authorizedPublicKeysFile string) { + dir := filepath.Dir(authorizedPublicKeysFile) + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Cannot watch authorized keys file '%s', user notifications about changes will not work: %v", + authorizedPublicKeysFile, err) + } + defer watcher.Close() + log.Println("XXX: monitor " + dir) + err = watcher.Add(dir) + if err != nil { + log.Printf("Cannot watch hold file %s, user notifications for change in expiry time will be unavailable: %v", authorizedPublicKeysFile, err) + } + for { + select { + case event, ok := <-watcher.Events: + if !ok { + pubkeys.notifyUsers( + fmt.Sprintf("Watching authorized keys file '%s' stopped", authorizedPublicKeysFile)) + return + } + base := filepath.Base(event.Name) + log.Println("CHANGE " + base + " " + authorizedPublicKeysFile + " " + filepath.Base(authorizedPublicKeysFile)) + if base == filepath.Base(authorizedPublicKeysFile) { + keys := pubkeys.Parse() + if len(keys) == 0 { + pubkeys.notifyUsers( + fmt.Sprintf("Authorized keys file '%s' does not contain any valid keys, using last known configuration", authorizedPublicKeysFile)) + } else { + pubkeys.notifyUsers(fmt.Sprintf("Updated authorized keys, now %d valid keys", len(keys))) + pubkeys.setPubKeys(keys) + } + } + + case err, ok := <-watcher.Errors: + if ok { + pubkeys.notifyUsers(fmt.Sprintf( + "Watching authorized keys file '%s' stopped", + pubkeys.authorizedKeysFile)) + } + pubkeys.notifyUsers(fmt.Sprintf( + "Watching authorized keys file '%s' stopped: %v", + pubkeys.authorizedKeysFile, err)) + return + } + } +} + +func (pubkeys *AuthorizedPublicKeys) Parse() []ssh.PublicKey { + if pubkeys.authorizedKeysFile == "" { + return nil + } + keys, err := readSshPublicKeys(pubkeys.authorizedKeysFile) if os.IsNotExist(err) { - log.Printf("Authorized keys file '%s' not found.", authorizedKeysFile) - return AuthorizedPublicKeys{} + log.Printf("Authorized keys file '%s' not found.", pubkeys.authorizedKeysFile) + return nil } if err != nil { log.Println("Public key authentication will not work since no public keys were found.") } - return AuthorizedPublicKeys{keys: keys} + return keys } -func (key AuthorizedPublicKeys) authorize(ctx ssh.Context, userProvidedKey ssh.PublicKey) bool { - for _, key := range key.keys { +func (key *AuthorizedPublicKeys) authorize(ctx ssh.Context, userProvidedKey ssh.PublicKey) bool { + // + keys := key.Parse() + if len(keys) == 0 { + keys = key.getPubKeys() + } + if len(keys) > 0 { + log.Printf("A total of %d authorized ssh keys were found", len(keys)) + } else { + log.Printf("No valid public keys were found, login is impossible") + // keep agent running, a user may still be logged in and could change the + // authorizedk eys file + // TODO: if no users are logged in, the agent should exit. + } + for _, key := range keys { if publicKeyHandler(ctx, userProvidedKey, key) { return true } diff --git a/cmd/converge/converge.go b/cmd/converge/converge.go index fb5f0c6..ee3aff9 100644 --- a/cmd/converge/converge.go +++ b/cmd/converge/converge.go @@ -100,7 +100,6 @@ func main() { userPassword := comms.UserPassword{ Username: strconv.Itoa(rand.Int()), - Password: strconv.Itoa(rand.Int()), } username, ok := os.LookupEnv("CONVERGE_USERNAME") @@ -110,14 +109,7 @@ func main() { os.Setenv("CONVERGE_USERNAME", userPassword.Username) } - password, ok := os.LookupEnv("CONVERGE_PASSWORD") - if ok { - userPassword.Password = password - } else { - os.Setenv("CONVERGE_PASSWORD", userPassword.Password) - } - - log.Printf("Using username '%s' and password '%s'", userPassword.Username, userPassword.Password) + log.Printf("Using username '%s'", userPassword.Username) notifications := make(chan *models.State, 10) admin := converge.NewAdmin(notifications) diff --git a/compose.yaml b/compose.yaml index 2dbb4b8..8009cce 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,5 +9,4 @@ services: - 8000:8000 environment: CONVERGE_USERNAME: abc - CONVERGE_PASSWORD: "123" TZ: "Japan" diff --git a/go.mod b/go.mod index bcdb396..4406438 100755 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module converge -go 1.21 +go 1.22.5 require ( github.com/ActiveState/termtest/conpty v0.5.0 diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml index 120a39c..52e2039 100644 --- a/kubernetes/deployment.yaml +++ b/kubernetes/deployment.yaml @@ -24,8 +24,5 @@ spec: env: - name: CONVERGE_USERNAME value: converge - - name: CONVERGE_PASSWORD - # change this password in your final deployment - value: "abc123" - + diff --git a/pkg/agent/session/session.go b/pkg/agent/session/session.go index dbed5f6..c07410c 100644 --- a/pkg/agent/session/session.go +++ b/pkg/agent/session/session.go @@ -137,6 +137,12 @@ func UserActivityDetected() { } } +func MessageUsers(message string) { + events <- func() { + messageUsers(message) + } +} + // Internal interface synchronous func userActivityDetected() { @@ -151,12 +157,12 @@ func userActivityDetected() { func monitorHoldFile() { watcher, err := fsnotify.NewWatcher() if err != nil { - log.Printf("Cannot watch old file %s, user notifications for change in expiry time will be unavailable: %v", holdFilename, err) + log.Printf("Cannot watch hold file %s, user notifications for change in expiry time will be unavailable: %v", holdFilename, err) } defer watcher.Close() err = watcher.Add(".") if err != nil { - log.Printf("Cannot watch old file %s, user notifications for change in expiry time will be unavailable: %v", holdFilename, err) + log.Printf("Cannot watch hold file %s, user notifications for change in expiry time will be unavailable: %v", holdFilename, err) } for { select { diff --git a/pkg/comms/events.go b/pkg/comms/events.go index 9f028b7..f8c6653 100644 --- a/pkg/comms/events.go +++ b/pkg/comms/events.go @@ -49,7 +49,6 @@ type ProtocolVersion struct { type UserPassword struct { Username string - Password string } // initialization mesaage when agent connects to server