converge/cmd/agent/sshauthorizedkeys.go
Erik Brakkee 3c803d6125 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 .
2024-08-06 22:03:36 +02:00

176 lines
4.8 KiB
Go

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 {
providedKey := gossh.MarshalAuthorizedKey(key)
if ssh.KeysEqual(key, authorizedKey) {
log.Printf("Successful login from %s", ctx.RemoteAddr())
return true
}
log.Printf("Failed login attempt from %s with key: %s", ctx.RemoteAddr(), strings.TrimSpace(string(providedKey)))
return false
}
func readSshPublicKeys(fileName string) ([]ssh.PublicKey, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, fmt.Errorf("Failed to open file: '%s': %s", fileName, err)
}
defer file.Close()
res := make([]ssh.PublicKey, 0)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineText := scanner.Text()
ind := strings.Index(lineText, "#")
if ind >= 0 {
lineText = lineText[:ind]
}
log.Println("Reading public key " + lineText)
lineText = strings.Trim(lineText, "")
if lineText == "" {
continue
}
line := []byte(lineText)
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(line)
if err != nil {
log.Printf("Failed to parse authorized key: %v", lineText)
} else {
res = append(res, parsedKey)
}
}
return res, nil
}
type AuthorizedPublicKeys struct {
authorizedKeysFile string
mutex sync.Mutex
publicKeys []ssh.PublicKey
}
func NewAuthorizedPublicKeys(authorizedKeysFile string) *AuthorizedPublicKeys {
pubkeys := AuthorizedPublicKeys{
authorizedKeysFile: authorizedKeysFile,
mutex: sync.Mutex{},
publicKeys: nil,
}
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.", pubkeys.authorizedKeysFile)
return nil
}
if err != nil {
log.Println("Public key authentication will not work since no public keys were found.")
}
return 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
}
}
return false
}