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 committed by Erik Brakkee
parent 8d554a5ce1
commit d109c72f66
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,
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 <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" +
"\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

View File

@ -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
}

View File

@ -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)

View File

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

2
go.mod
View File

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

View File

@ -24,8 +24,5 @@ spec:
env:
- name: CONVERGE_USERNAME
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
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 {

View File

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