mirror of
https://github.com/fosrl/newt.git
synced 2026-09-07 06:31:29 +02:00
1
go.mod
1
go.mod
@@ -52,6 +52,7 @@ require (
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/msteinert/pam/v2 v2.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -67,6 +67,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY=
|
||||
github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
|
||||
49
main.go
49
main.go
@@ -1715,14 +1715,14 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
|
||||
|
||||
// Define the structure of the incoming message
|
||||
type SSHCertData struct {
|
||||
MessageId int `json:"messageId"`
|
||||
AgentPort int `json:"agentPort"`
|
||||
AgentHost string `json:"agentHost"`
|
||||
ExternalAuthDaemon bool `json:"externalAuthDaemon"`
|
||||
CACert string `json:"caCert"`
|
||||
Username string `json:"username"`
|
||||
NiceID string `json:"niceId"`
|
||||
Metadata struct {
|
||||
MessageId int `json:"messageId"`
|
||||
AgentPort int `json:"agentPort"`
|
||||
AgentHost string `json:"agentHost"`
|
||||
AuthDaemonMode string `json:"authDaemonMode"` // site, remote, native
|
||||
CACert string `json:"caCert"`
|
||||
Username string `json:"username"`
|
||||
NiceID string `json:"niceId"`
|
||||
Metadata struct {
|
||||
SudoMode string `json:"sudoMode"`
|
||||
SudoCommands []string `json:"sudoCommands"`
|
||||
Homedir bool `json:"homedir"`
|
||||
@@ -1745,28 +1745,8 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
|
||||
return
|
||||
}
|
||||
|
||||
var useNativeSSH = true
|
||||
|
||||
if useNativeSSH {
|
||||
// Update in-memory credentials used by the native SSH server.
|
||||
if err := sshCredStore.SetCAKey(certData.CACert); err != nil {
|
||||
logger.Error("nativessh: failed to set CA key: %v", err)
|
||||
}
|
||||
sshCredStore.AddPrincipals(certData.Username, certData.NiceID)
|
||||
logger.Info("nativessh: updated credentials for user %s (niceId=%s)", certData.Username, certData.NiceID)
|
||||
|
||||
// Acknowledge the PAM connection to the cloud.
|
||||
if err := client.SendMessage("ws/round-trip/complete", map[string]interface{}{
|
||||
"messageId": certData.MessageId,
|
||||
"complete": true,
|
||||
}); err != nil {
|
||||
logger.Error("nativessh: failed to send round-trip complete: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're running the auth daemon internally
|
||||
if authDaemonServer != nil && !certData.ExternalAuthDaemon { // if the auth daemon is running internally and the external auth daemon is not enabled
|
||||
if authDaemonServer != nil && certData.AuthDaemonMode == "site" { // if the auth daemon is running internally and the external auth daemon is not enabled
|
||||
// Call ProcessConnection directly when running internally
|
||||
logger.Debug("Calling internal auth daemon ProcessConnection for user %s", certData.Username)
|
||||
|
||||
@@ -1789,7 +1769,7 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
|
||||
})
|
||||
|
||||
logger.Info("Successfully processed connection via internal auth daemon for user %s", certData.Username)
|
||||
} else {
|
||||
} else if certData.AuthDaemonMode == "remote" {
|
||||
// External auth daemon mode - make HTTP request
|
||||
// Check if auth daemon key is configured
|
||||
if authDaemonKey == "" {
|
||||
@@ -1886,6 +1866,15 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
|
||||
}
|
||||
|
||||
logger.Info("Successfully registered SSH certificate with external auth daemon for user %s", certData.Username)
|
||||
} else if certData.AuthDaemonMode == "native" {
|
||||
// Update in-memory credentials used by the native SSH server.
|
||||
if err := sshCredStore.SetCAKey(certData.CACert); err != nil {
|
||||
logger.Error("nativessh: failed to set CA key: %v", err)
|
||||
}
|
||||
sshCredStore.AddPrincipals(certData.Username, certData.NiceID)
|
||||
logger.Info("nativessh: updated credentials for user %s (niceId=%s)", certData.Username, certData.NiceID)
|
||||
} else {
|
||||
logger.Error("Unknown auth daemon mode: %s", certData.AuthDaemonMode)
|
||||
}
|
||||
|
||||
// Send success response back to cloud
|
||||
|
||||
50
nativessh/auth.go
Normal file
50
nativessh/auth.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package nativessh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// checkAuthorizedKeys reports whether key matches any entry in the system
|
||||
// user's ~/.ssh/authorized_keys file. Returns false (not an error) when the
|
||||
// user or file does not exist.
|
||||
func checkAuthorizedKeys(username string, key ssh.PublicKey) bool {
|
||||
u, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
f, err := os.Open(filepath.Join(u.HomeDir, ".ssh", "authorized_keys"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
want := ssh.FingerprintSHA256(key)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ssh.FingerprintSHA256(parsed) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// systemUserExists reports whether a user account with the given name exists
|
||||
// on the host OS.
|
||||
func systemUserExists(username string) bool {
|
||||
_, err := user.Lookup(username)
|
||||
return err == nil
|
||||
}
|
||||
34
nativessh/pam_linux.go
Normal file
34
nativessh/pam_linux.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build linux
|
||||
|
||||
package nativessh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/msteinert/pam/v2"
|
||||
)
|
||||
|
||||
// verifySystemPassword authenticates username/password via PAM using the
|
||||
// "sshd" service stack. It returns nil on success and an error on failure.
|
||||
// The caller must not reveal the error detail to the client.
|
||||
func verifySystemPassword(username, password string) error {
|
||||
tx, err := pam.StartFunc("sshd", username, func(s pam.Style, msg string) (string, error) {
|
||||
switch s {
|
||||
case pam.PromptEchoOff, pam.PromptEchoOn:
|
||||
return password, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("PAM start: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Authenticate(0); err != nil {
|
||||
return fmt.Errorf("PAM authenticate: %w", err)
|
||||
}
|
||||
if err := tx.AcctMgmt(0); err != nil {
|
||||
return fmt.Errorf("PAM acct_mgmt: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
11
nativessh/pam_other.go
Normal file
11
nativessh/pam_other.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !linux
|
||||
|
||||
package nativessh
|
||||
|
||||
import "errors"
|
||||
|
||||
// verifySystemPassword is not supported on non-Linux platforms; it always
|
||||
// returns an error so that password authentication is never accepted.
|
||||
func verifySystemPassword(username, password string) error {
|
||||
return errors.New("password authentication not supported on this platform")
|
||||
}
|
||||
@@ -93,14 +93,17 @@ func NewServer(cfg ServerConfig) *Server {
|
||||
return &Server{cfg: cfg}
|
||||
}
|
||||
|
||||
// buildSSHConfig builds the ssh.ServerConfig backed by the in-memory CredentialStore.
|
||||
// buildSSHConfig builds the ssh.ServerConfig with multi-method authentication:
|
||||
// 1. Public key: host ~/.ssh/authorized_keys, then CA certificate.
|
||||
// 2. Password: system PAM stack (Linux only).
|
||||
func (s *Server) buildSSHConfig() (*ssh.ServerConfig, error) {
|
||||
hostSigner, err := generateHostKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("host key: %w", err)
|
||||
}
|
||||
cfg := &ssh.ServerConfig{
|
||||
PublicKeyCallback: makeCredentialStoreCallback(s.cfg.Credentials),
|
||||
PublicKeyCallback: makePublicKeyCallback(s.cfg.Credentials),
|
||||
PasswordCallback: makePasswordCallback(),
|
||||
}
|
||||
cfg.AddHostKey(hostSigner)
|
||||
return cfg, nil
|
||||
@@ -240,28 +243,57 @@ func (s *Server) handleSession(ch ssh.Channel, requests <-chan *ssh.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// makeCredentialStoreCallback returns an ssh.PublicKeyCallback that reads
|
||||
// the CA key and per-user principals from store on every auth attempt, so
|
||||
// credentials updated via AddPrincipals/SetCAKey are applied immediately.
|
||||
func makeCredentialStoreCallback(store *CredentialStore) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
// makePublicKeyCallback returns a PublicKeyCallback that tries, in order:
|
||||
// 1. Host authorized_keys – matches any key in the OS user's
|
||||
// ~/.ssh/authorized_keys file.
|
||||
// 2. CA certificate – validates an SSH certificate signed by the
|
||||
// configured CA and checks that the user appears in the principals map.
|
||||
//
|
||||
// store may be nil or empty; those paths are simply skipped.
|
||||
func makePublicKeyCallback(store *CredentialStore) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
return func(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
caKey, userPrincipals := store.get(meta.User())
|
||||
if caKey == nil {
|
||||
return nil, fmt.Errorf("no CA key configured")
|
||||
// 1. Host authorized_keys.
|
||||
if checkAuthorizedKeys(meta.User(), key) {
|
||||
log.Printf("nativessh: authorized_keys auth for user %q", meta.User())
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
checker := &ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(caKey)
|
||||
},
|
||||
|
||||
// 2. CA certificate.
|
||||
if store != nil {
|
||||
caKey, userPrincipals := store.get(meta.User())
|
||||
if caKey != nil {
|
||||
checker := &ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
return ssh.FingerprintSHA256(auth) == ssh.FingerprintSHA256(caKey)
|
||||
},
|
||||
}
|
||||
perms, err := checker.Authenticate(meta, key)
|
||||
if err == nil {
|
||||
if len(userPrincipals) == 0 {
|
||||
return nil, fmt.Errorf("user %q not in allowed principals list", meta.User())
|
||||
}
|
||||
log.Printf("nativessh: CA cert auth for user %q", meta.User())
|
||||
return perms, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
perms, err := checker.Authenticate(meta, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
return nil, fmt.Errorf("public key not authorized for user %q", meta.User())
|
||||
}
|
||||
}
|
||||
|
||||
// makePasswordCallback returns a PasswordCallback that validates the supplied
|
||||
// password via the host OS PAM stack. On non-Linux platforms this always
|
||||
// fails (see pam_other.go).
|
||||
func makePasswordCallback() func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) {
|
||||
return func(meta ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
if err := verifySystemPassword(meta.User(), string(password)); err != nil {
|
||||
// Return a generic message to the client; log the real reason.
|
||||
log.Printf("nativessh: password auth failed for user %q: %v", meta.User(), err)
|
||||
return nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
if len(userPrincipals) == 0 {
|
||||
return nil, fmt.Errorf("user %q not in allowed principals list", meta.User())
|
||||
}
|
||||
return perms, nil
|
||||
log.Printf("nativessh: password auth for user %q", meta.User())
|
||||
return &ssh.Permissions{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user