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