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 .
This commit is contained in:
Erik Brakkee 2024-08-06 22:03:36 +02:00
parent 95926c5896
commit 3c803d6125
8 changed files with 126 additions and 55 deletions

View File

@ -77,8 +77,7 @@ func (user UserActivityDetector) Write(p []byte) (int, error) {
} }
func sshServer(hostKeyFile string, shellCommand string, func sshServer(hostKeyFile string, shellCommand string,
passwordHandler ssh.PasswordHandler, authorizedPublicKeys *AuthorizedPublicKeys) *ssh.Server {
authorizedPublicKeys AuthorizedPublicKeys) *ssh.Server {
ssh.Handle(func(sshSession ssh.Session) { ssh.Handle(func(sshSession ssh.Session) {
workingDirectory, _ := os.Getwd() workingDirectory, _ := os.Getwd()
env := append(os.Environ(), fmt.Sprintf("agentdir=%s", workingDirectory)) 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") log.Println("starting ssh server, waiting for debug sessions")
server := ssh.Server{ server := ssh.Server{
PasswordHandler: passwordHandler,
PublicKeyHandler: authorizedPublicKeys.authorize, PublicKeyHandler: authorizedPublicKeys.authorize,
SubsystemHandlers: map[string]ssh.SubsystemHandler{ SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": SftpHandler, "sftp": SftpHandler,
@ -185,9 +183,7 @@ func printHelp(msg string) {
"Here <ID> is the unique id of the agent that allows rendez-vous with an end-user.\n" + "Here <ID> 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" + "The end-user must specify the same id when connecting using ssh.\n" +
"\n" + "\n" +
"--id: rendez-vous id. When specified an SSH authorized key must be used and password\n" + "--id: rendez-vous id, this is the id used to connect agents and clients. \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" +
"--authorized-keys: SSH authorized keys file in openssh format. By default .authorized_keys in the\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" + " directory where the agent is started is used.\n" +
"--warning-time: advance warning time before sessio ends (default '5m')\n" + "--warning-time: advance warning time before sessio ends (default '5m')\n" +
@ -333,15 +329,20 @@ func main() {
// Authentiocation // Authentiocation
passwordHandler, authorizedKeys := setupAuthentication( authorizedKeys := NewAuthorizedPublicKeys(authorizedKeysFile)
commChannel, // initial check
serverInfo.UserPassword, pubkeys := authorizedKeys.Parse()
authorizedKeysFile) if len(pubkeys) == 0 {
log.Printf("No public keys found in '%s', exiting", authorizedKeysFile)
os.Exit(1)
}
go comms.ListenForServerEvents(commChannel)
var service AgentService var service AgentService
service = ListenerServer(func() *ssh.Server { service = ListenerServer(func() *ssh.Server {
return sshServer("hostkey.pem", shell, passwordHandler, authorizedKeys) return sshServer("hostkey.pem", shell, authorizedKeys)
}) })
//service = ConnectionServer(netCatServer) //service = ConnectionServer(netCatServer)
//service = ConnectionServer(echoServer) //service = ConnectionServer(echoServer)
@ -376,22 +377,6 @@ func main() {
service.Run(listener) 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 { func chooseShell(shells []string) string {
log.Printf("Shell search path is %v", shells) log.Printf("Shell search path is %v", shells)
var err error var err error

View File

@ -2,12 +2,16 @@ package main
import ( import (
"bufio" "bufio"
"converge/pkg/agent/session"
"fmt" "fmt"
"github.com/fsnotify/fsnotify"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
"log" "log"
"os" "os"
"path/filepath"
"strings" "strings"
"sync"
) )
func publicKeyHandler(ctx ssh.Context, key gossh.PublicKey, authorizedKey gossh.PublicKey) bool { 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 { type AuthorizedPublicKeys struct {
keys []ssh.PublicKey authorizedKeysFile string
mutex sync.Mutex
publicKeys []ssh.PublicKey
} }
func ParseOpenSSHAuthorizedKeysFile(authorizedKeysFile string) AuthorizedPublicKeys { func NewAuthorizedPublicKeys(authorizedKeysFile string) *AuthorizedPublicKeys {
if authorizedKeysFile == "" { pubkeys := AuthorizedPublicKeys{
return 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) { if os.IsNotExist(err) {
log.Printf("Authorized keys file '%s' not found.", authorizedKeysFile) log.Printf("Authorized keys file '%s' not found.", pubkeys.authorizedKeysFile)
return AuthorizedPublicKeys{} return nil
} }
if err != nil { if err != nil {
log.Println("Public key authentication will not work since no public keys were found.") 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 { func (key *AuthorizedPublicKeys) authorize(ctx ssh.Context, userProvidedKey ssh.PublicKey) bool {
for _, key := range key.keys { //
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) { if publicKeyHandler(ctx, userProvidedKey, key) {
return true return true
} }

View File

@ -100,7 +100,6 @@ func main() {
userPassword := comms.UserPassword{ userPassword := comms.UserPassword{
Username: strconv.Itoa(rand.Int()), Username: strconv.Itoa(rand.Int()),
Password: strconv.Itoa(rand.Int()),
} }
username, ok := os.LookupEnv("CONVERGE_USERNAME") username, ok := os.LookupEnv("CONVERGE_USERNAME")
@ -110,14 +109,7 @@ func main() {
os.Setenv("CONVERGE_USERNAME", userPassword.Username) os.Setenv("CONVERGE_USERNAME", userPassword.Username)
} }
password, ok := os.LookupEnv("CONVERGE_PASSWORD") log.Printf("Using username '%s'", userPassword.Username)
if ok {
userPassword.Password = password
} else {
os.Setenv("CONVERGE_PASSWORD", userPassword.Password)
}
log.Printf("Using username '%s' and password '%s'", userPassword.Username, userPassword.Password)
notifications := make(chan *models.State, 10) notifications := make(chan *models.State, 10)
admin := converge.NewAdmin(notifications) admin := converge.NewAdmin(notifications)

View File

@ -9,5 +9,4 @@ services:
- 8000:8000 - 8000:8000
environment: environment:
CONVERGE_USERNAME: abc CONVERGE_USERNAME: abc
CONVERGE_PASSWORD: "123"
TZ: "Japan" TZ: "Japan"

2
go.mod
View File

@ -1,6 +1,6 @@
module converge module converge
go 1.21 go 1.22.5
require ( require (
github.com/ActiveState/termtest/conpty v0.5.0 github.com/ActiveState/termtest/conpty v0.5.0

View File

@ -24,8 +24,5 @@ spec:
env: env:
- name: CONVERGE_USERNAME - name: CONVERGE_USERNAME
value: converge value: converge
- name: CONVERGE_PASSWORD
# change this password in your final deployment
value: "abc123"

View File

@ -137,6 +137,12 @@ func UserActivityDetected() {
} }
} }
func MessageUsers(message string) {
events <- func() {
messageUsers(message)
}
}
// Internal interface synchronous // Internal interface synchronous
func userActivityDetected() { func userActivityDetected() {
@ -151,12 +157,12 @@ func userActivityDetected() {
func monitorHoldFile() { func monitorHoldFile() {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { 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() defer watcher.Close()
err = watcher.Add(".") err = watcher.Add(".")
if err != nil { 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 { for {
select { select {

View File

@ -49,7 +49,6 @@ type ProtocolVersion struct {
type UserPassword struct { type UserPassword struct {
Username string Username string
Password string
} }
// initialization mesaage when agent connects to server // initialization mesaage when agent connects to server