converge/pkg/agent/service/sshauthorizedkeys.go

179 lines
5.0 KiB
Go

package service
import (
"bufio"
"fmt"
"git.wamblee.org/converge/pkg/agent/session"
"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, []string, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, nil, fmt.Errorf("Failed to open file: '%s': %s", fileName, err)
}
defer file.Close()
res := make([]ssh.PublicKey, 0)
scanner := bufio.NewScanner(file)
var errorKeys []string
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 {
errorKeys = append(errorKeys, lineText)
log.Printf("Failed to parse authorized key: %v", lineText)
} else {
res = append(res, parsedKey)
}
}
return res, errorKeys, nil
}
type AuthorizedPublicKeys struct {
authorizedKeysFile string
mutex sync.Mutex
publicKeys []ssh.PublicKey
}
func NewAuthorizedPublicKeys(authorizedKeysFile string) (*AuthorizedPublicKeys, error) {
pubkeys := AuthorizedPublicKeys{
authorizedKeysFile: authorizedKeysFile,
mutex: sync.Mutex{},
publicKeys: nil,
}
pubkeys.publicKeys, _ = pubkeys.Parse()
if len(pubkeys.publicKeys) == 0 {
return nil, fmt.Errorf("No valid public keys found, login is impossible, exiting")
}
go pubkeys.monitorAuthorizedKeysFile(authorizedKeysFile)
return &pubkeys, nil
}
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()
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 {
session.MessageUsers(
fmt.Sprintf("Watching authorized keys file '%s' stopped", authorizedPublicKeysFile))
return
}
base := filepath.Base(event.Name)
if base == filepath.Base(authorizedPublicKeysFile) {
keys, errorKeys := pubkeys.Parse()
for _, errorKey := range errorKeys {
session.MessageUsers(fmt.Sprintf("Public key '%s' is invalid", errorKey))
}
if len(keys) == 0 {
session.MessageUsers(
fmt.Sprintf("Authorized keys file '%s' does not exist or does not contain any valid keys, using last known configuration", authorizedPublicKeysFile))
} else {
session.MessageUsers(fmt.Sprintf("Updated authorized keys, now %d valid key(s)", len(keys)))
pubkeys.setPubKeys(keys)
}
}
case err, ok := <-watcher.Errors:
if ok {
session.MessageUsers(fmt.Sprintf(
"Watching authorized keys file '%s' stopped",
pubkeys.authorizedKeysFile))
}
session.MessageUsers(fmt.Sprintf(
"Watching authorized keys file '%s' stopped: %v",
pubkeys.authorizedKeysFile, err))
return
}
}
}
func (pubkeys *AuthorizedPublicKeys) Parse() ([]ssh.PublicKey, []string) {
if pubkeys.authorizedKeysFile == "" {
return nil, nil
}
keys, errorKeys, err := readSshPublicKeys(pubkeys.authorizedKeysFile)
if os.IsNotExist(err) {
log.Printf("Authorized keys file '%s' not found.", pubkeys.authorizedKeysFile)
return nil, nil
}
if err != nil {
log.Println("Public key authentication will not work since no public keys were found.")
}
return keys, errorKeys
}
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
}
for _, key := range keys {
if publicKeyHandler(ctx, userProvidedKey, key) {
return true
}
}
return false
}