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 .
176 lines
4.8 KiB
Go
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
|
|
}
|