Initial import
This commit is contained in:
96
auth/auth.go
Normal file
96
auth/auth.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.maze.io/maze/conduit/logger"
|
||||
)
|
||||
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
// Provider implements zero or more of the [Password], [PublicKey] interfaces.
|
||||
type Provider any
|
||||
|
||||
// Password authenticator.
|
||||
type Password interface {
|
||||
VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error)
|
||||
}
|
||||
|
||||
// Token authenticator.
|
||||
type Token interface {
|
||||
Instruction() string
|
||||
|
||||
Prompt() string
|
||||
|
||||
// Multiline accepts multiline input and wait for the user to supply
|
||||
// an empty line.
|
||||
Multiline() bool
|
||||
|
||||
VerifyToken(meta ssh.ConnMetadata, token string) (Principal, error)
|
||||
}
|
||||
|
||||
// PublicKey authenticator.
|
||||
type PublicKey interface {
|
||||
VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error)
|
||||
}
|
||||
|
||||
type CertificateAuthority struct {
|
||||
checker *ssh.CertChecker
|
||||
keys []ssh.PublicKey
|
||||
blob [][]byte
|
||||
user bool
|
||||
host bool
|
||||
}
|
||||
|
||||
func NewUserCertificateAuthority(keys ...ssh.PublicKey) *CertificateAuthority {
|
||||
var blob [][]byte
|
||||
for _, key := range keys {
|
||||
blob = append(blob, key.Marshal())
|
||||
}
|
||||
auth := &CertificateAuthority{
|
||||
checker: &ssh.CertChecker{
|
||||
Clock: time.Now,
|
||||
},
|
||||
keys: keys,
|
||||
blob: blob,
|
||||
user: true,
|
||||
}
|
||||
auth.checker.IsUserAuthority = auth.isUserAuthority
|
||||
return auth
|
||||
}
|
||||
|
||||
func (auth *CertificateAuthority) isUserAuthority(key ssh.PublicKey) bool {
|
||||
if !auth.user {
|
||||
return false
|
||||
}
|
||||
blob := key.Marshal()
|
||||
for _, other := range auth.blob {
|
||||
if bytes.Equal(blob, other) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *CertificateAuthority) VerifyPublicKey(meta ssh.ConnMetadata, publicKey ssh.PublicKey) (Principal, error) {
|
||||
log := logger.StandardLog.Values(logger.Values{
|
||||
"client": meta.RemoteAddr().String(),
|
||||
"key": strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))),
|
||||
"user": meta.User(),
|
||||
"version": string(meta.ClientVersion()),
|
||||
})
|
||||
log.Debug("Verifying user certificate")
|
||||
|
||||
_, err := auth.checker.Authenticate(meta, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert := publicKey.(*ssh.Certificate)
|
||||
return UserCertificatePrincipal{cert}, nil
|
||||
}
|
104
auth/file.go
Normal file
104
auth/file.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-crypt/crypt"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.maze.io/maze/conduit/logger"
|
||||
"git.maze.io/maze/conduit/ssh/sshutil"
|
||||
)
|
||||
|
||||
type passwordFile map[string]string
|
||||
|
||||
func PasswordFile(name string) (Password, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := bufio.NewScanner(f)
|
||||
p := make(passwordFile)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
if i := strings.IndexByte(line, ':'); i > 0 {
|
||||
p[line[:i]] = line[i+1:]
|
||||
}
|
||||
}
|
||||
return p, s.Err()
|
||||
}
|
||||
|
||||
func (auth passwordFile) VerifyPassword(meta ssh.ConnMetadata, password string) (Principal, error) {
|
||||
encodedDigest := auth[meta.User()]
|
||||
if ok, err := crypt.CheckPasswordWithPlainText(password, encodedDigest); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
return MakePasswordPrincipal(meta), nil
|
||||
}
|
||||
|
||||
type publibKeyFile map[string][]ssh.PublicKey
|
||||
|
||||
func PublicKeyFile(name string) (PublicKey, error) {
|
||||
log := logger.StandardLog.Value("path", name)
|
||||
log.Trace("Parsing public keys file")
|
||||
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
s = bufio.NewScanner(f)
|
||||
p = make(publibKeyFile)
|
||||
lineno int
|
||||
)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
lineno++
|
||||
if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
if i := strings.IndexByte(line, ':'); i > 0 {
|
||||
//k, err := ssh.ParsePublicKey([]byte(line[i+1:]))
|
||||
k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line[i+1:]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth: invalid public key in %s:%d: %w", name, lineno, err)
|
||||
}
|
||||
log.Values(logger.Values{
|
||||
"user": line[:i],
|
||||
"key": k.Type(),
|
||||
}).Trace("Parsed authorized public key")
|
||||
p[line[:i]] = append(p[line[:i]], k)
|
||||
}
|
||||
}
|
||||
return p, s.Err()
|
||||
}
|
||||
|
||||
func (auth publibKeyFile) VerifyPublicKey(meta ssh.ConnMetadata, key ssh.PublicKey) (Principal, error) {
|
||||
log := logger.StandardLog.Values(logger.Values{
|
||||
"client": meta.RemoteAddr().String(),
|
||||
"key_type": sshutil.KeyType(key),
|
||||
"key_bits": sshutil.KeyBits(key),
|
||||
"user": meta.User(),
|
||||
"version": string(meta.ClientVersion()),
|
||||
})
|
||||
log.Debug("Verifying user public key")
|
||||
|
||||
blob := key.Marshal()
|
||||
keys := auth[meta.User()]
|
||||
for _, other := range keys {
|
||||
if bytes.Equal(other.Marshal(), blob) {
|
||||
return MakePublicKeyPrincipal(meta, key), nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUnauthorized
|
||||
}
|
134
auth/file_test.go
Normal file
134
auth/file_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var testAddr = &net.TCPAddr{
|
||||
IP: net.ParseIP("127.1.2.3"),
|
||||
Port: 22,
|
||||
}
|
||||
|
||||
type testConnMetadata struct {
|
||||
user string
|
||||
sessionID []byte
|
||||
clientVersion string
|
||||
serverVersion string
|
||||
laddr, raddr net.Addr
|
||||
}
|
||||
|
||||
func (t testConnMetadata) User() string { return t.user }
|
||||
func (t testConnMetadata) SessionID() []byte { return t.sessionID }
|
||||
func (t testConnMetadata) ClientVersion() []byte { return []byte(t.clientVersion) }
|
||||
func (t testConnMetadata) ServerVersion() []byte { return []byte(t.serverVersion) }
|
||||
func (t testConnMetadata) RemoteAddr() net.Addr { return t.raddr }
|
||||
func (t testConnMetadata) LocalAddr() net.Addr { return t.laddr }
|
||||
|
||||
var _ ssh.ConnMetadata = (*testConnMetadata)(nil)
|
||||
|
||||
func TestPasswordFile(t *testing.T) {
|
||||
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Username string
|
||||
Password string
|
||||
}{
|
||||
{"example", "example"},
|
||||
{"bcrypt", "example"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.Username, func(it *testing.T) {
|
||||
p, err := a.VerifyPassword(testConnMetadata{user: test.Username}, test.Password)
|
||||
if err != nil {
|
||||
it.Error(err)
|
||||
} else {
|
||||
it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyFile(t *testing.T) {
|
||||
a, err := PublicKeyFile(filepath.Join("testdata", "pubkey"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
Username string
|
||||
PublicKey string
|
||||
}{
|
||||
{"single/ed25519", "test_a", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb"},
|
||||
{"dual/ed25519", "test_b", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir"},
|
||||
{"dual/rsa", "test_b", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.Username, func(it *testing.T) {
|
||||
k, _, _, _, err := ssh.ParseAuthorizedKey([]byte(test.PublicKey))
|
||||
if err != nil {
|
||||
it.Fatal(err)
|
||||
}
|
||||
|
||||
p, err := a.VerifyPublicKey(testConnMetadata{
|
||||
user: test.Username,
|
||||
laddr: testAddr,
|
||||
raddr: testAddr,
|
||||
}, k)
|
||||
if err != nil {
|
||||
it.Error(err)
|
||||
} else {
|
||||
it.Logf("%s: %s (%T)", p.Type(), p.Identity(), p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPasswordFileHits(b *testing.B) {
|
||||
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
c := testConnMetadata{user: "example"}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
a.VerifyPassword(c, "example")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPasswordFileMissPassword(b *testing.B) {
|
||||
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
c := testConnMetadata{user: "example"}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
a.VerifyPassword(c, "invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPasswordFileMissPrincipal(b *testing.B) {
|
||||
a, err := PasswordFile(filepath.Join("testdata", "passwd"))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
c := testConnMetadata{user: "invalid"}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
a.VerifyPassword(c, "example")
|
||||
}
|
||||
}
|
329
auth/handler.go
Normal file
329
auth/handler.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.maze.io/maze/conduit/logger"
|
||||
"git.maze.io/maze/conduit/policy"
|
||||
"git.maze.io/maze/conduit/policy/input"
|
||||
)
|
||||
|
||||
// None accepts no authentication, this is only useful for debugging/testing.
|
||||
type None struct{}
|
||||
|
||||
func (None) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
|
||||
config := new(ssh.ServerConfig)
|
||||
if serverConfig != nil {
|
||||
*config = *serverConfig
|
||||
}
|
||||
config.NoClientAuth = true
|
||||
return ssh.NewServerConn(conn, config)
|
||||
}
|
||||
|
||||
// UserCertificate offers user certificate based authentication.
|
||||
type UserCertificate struct {
|
||||
// Loader is the policy loader.
|
||||
Loader policy.Loader
|
||||
|
||||
// CA is our trusted user certificate authority public keys.
|
||||
CA []ssh.PublicKey
|
||||
|
||||
// VerifyCallback can optionally be used to perform additional checks on the certificate.
|
||||
VerifyCallback func(*ssh.Certificate) bool
|
||||
}
|
||||
|
||||
func (auth UserCertificate) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
|
||||
log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
|
||||
|
||||
policy, err := auth.Loader.LoadPolicy("conduit/auth/user_certificate")
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
_ = policy
|
||||
|
||||
checker := &ssh.CertChecker{
|
||||
Clock: time.Now,
|
||||
}
|
||||
checker.IsUserAuthority = func(key ssh.PublicKey) bool {
|
||||
blob := key.Marshal()
|
||||
for _, ca := range auth.CA {
|
||||
if bytes.Equal(ca.Marshal(), blob) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
config := new(ssh.ServerConfig)
|
||||
if serverConfig != nil {
|
||||
*config = *serverConfig
|
||||
}
|
||||
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if _, err := checker.Authenticate(conn, key); err != nil {
|
||||
log.Err(err).Debug("Certificate check failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert := key.(*ssh.Certificate)
|
||||
principal := UserCertificatePrincipal{Certificate: cert}
|
||||
var (
|
||||
query = "data." + policy.Package()
|
||||
input = struct {
|
||||
Conn *input.ConnMetadata `json:"conn"`
|
||||
Principal Principal `json:"principal"`
|
||||
}{input.NewConnMetadata(conn), principal}
|
||||
result struct {
|
||||
Permit bool `mapstructure:"permit"`
|
||||
}
|
||||
)
|
||||
if err = policy.Query(query, input, &result); err != nil {
|
||||
log.Err(err).Warn("Policy query returned error")
|
||||
return nil, err
|
||||
}
|
||||
if !result.Permit {
|
||||
log.Debug("Policy rejected certificate")
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
return &cert.Permissions, nil
|
||||
}
|
||||
|
||||
return ssh.NewServerConn(conn, config)
|
||||
}
|
||||
|
||||
// MultiFactor offers policy-based MFA (multi factor authentication).
|
||||
type MultiFactor struct {
|
||||
// Loader for the policy rules.
|
||||
Loader policy.Loader
|
||||
|
||||
// PublicKey enables public key authentication.
|
||||
PublicKey PublicKey
|
||||
|
||||
// UserCA enables user certificate based authentication.
|
||||
UserCA []ssh.PublicKey
|
||||
|
||||
// Password enables password authentication.
|
||||
Password Password
|
||||
|
||||
// Token enabled token authentication.
|
||||
Token Token
|
||||
}
|
||||
|
||||
func (mfa *MultiFactor) HandleAccept(conn net.Conn, serverConfig *ssh.ServerConfig) (*ssh.ServerConn, <-chan ssh.NewChannel, <-chan *ssh.Request, error) {
|
||||
log := logger.StandardLog.Value("client", conn.RemoteAddr().String())
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if err, ok := r.(error); ok {
|
||||
log.Err(err).Error("Recovered from panic in accept handler! This is a bug!")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
rules, err := mfa.Loader.LoadPolicy("conduit/auth/mfa")
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
config := new(ssh.ServerConfig)
|
||||
if serverConfig != nil {
|
||||
*config = *serverConfig
|
||||
}
|
||||
|
||||
var (
|
||||
meta ssh.ConnMetadata
|
||||
principals []Principal
|
||||
tokenAuth = mfa.Token
|
||||
passwordAuth = mfa.Password
|
||||
publicKeyAuth = mfa.PublicKey
|
||||
certificateAuth []ssh.PublicKey
|
||||
)
|
||||
|
||||
checkPolicy := func() (perms *ssh.Permissions, err error) {
|
||||
var (
|
||||
query = "data." + rules.Package()
|
||||
input = struct {
|
||||
Conn *input.ConnMetadata `json:"conn"`
|
||||
Principals []Principal `json:"principals"`
|
||||
}{input.NewConnMetadata(meta), principals}
|
||||
result struct {
|
||||
Permit bool `mapstructure:"permit"`
|
||||
PermitPassword bool `mapstructure:"permit_password"`
|
||||
PermitToken bool `mapstructure:"permit_token"`
|
||||
PermitCertificate bool `mapstructure:"permit_certificate"`
|
||||
PermitPublicKey bool `mapstructure:"permit_publickey"`
|
||||
}
|
||||
)
|
||||
if err = rules.Query(query, input, &result); err != nil {
|
||||
log.Err(err).Warn("Policy query returned error")
|
||||
return
|
||||
}
|
||||
|
||||
if !result.PermitPassword && passwordAuth != nil {
|
||||
log.Debug("Policy disabled password authentication")
|
||||
passwordAuth = nil
|
||||
}
|
||||
if passwordAuth == nil && config.PasswordCallback != nil {
|
||||
// Disable password authentication.
|
||||
log.Debug("Policy disabled password callback")
|
||||
config.PasswordCallback = nil
|
||||
}
|
||||
|
||||
if !result.PermitToken && tokenAuth != nil {
|
||||
log.Debug("Policy disabled token authentication")
|
||||
tokenAuth = nil
|
||||
}
|
||||
if tokenAuth == nil && config.KeyboardInteractiveCallback != nil {
|
||||
// Disable token authentication.
|
||||
log.Debug("Policy disabled token authentication")
|
||||
config.KeyboardInteractiveCallback = nil
|
||||
}
|
||||
|
||||
if !result.PermitCertificate && len(certificateAuth) != 0 {
|
||||
log.Debug("Policy disabled certificate authentication")
|
||||
certificateAuth = nil
|
||||
}
|
||||
if !result.PermitPublicKey && publicKeyAuth != nil {
|
||||
log.Debug("Policy disabled public key authentication")
|
||||
publicKeyAuth = nil
|
||||
}
|
||||
if publicKeyAuth == nil && len(certificateAuth) == 0 && config.PublicKeyCallback != nil {
|
||||
// Disable pubkey authentication.
|
||||
log.Debug("Policy disabled public key callback")
|
||||
config.PublicKeyCallback = nil
|
||||
}
|
||||
|
||||
if !result.Permit {
|
||||
log.Debug("Policy rejected principals; not authorized")
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
if len(principals) == 0 {
|
||||
log.Debug("No valid principals; not authorized")
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
attr := principals[0].Attributes()
|
||||
return &ssh.Permissions{
|
||||
CriticalOptions: attr.Options,
|
||||
Extensions: attr.Extensions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if mfa.PublicKey != nil || len(mfa.UserCA) > 0 {
|
||||
log = log.Values(logger.Values{
|
||||
"permit_pubkey": mfa.PublicKey != nil,
|
||||
"permit_certificate": len(mfa.UserCA) > 0,
|
||||
})
|
||||
log.Trace("MFA enabling public key authentication")
|
||||
|
||||
var checker *ssh.CertChecker
|
||||
if len(mfa.UserCA) > 0 {
|
||||
checker = &ssh.CertChecker{
|
||||
Clock: time.Now,
|
||||
}
|
||||
checker.IsUserAuthority = func(key ssh.PublicKey) bool {
|
||||
blob := key.Marshal()
|
||||
for _, ca := range mfa.UserCA {
|
||||
if bytes.Equal(ca.Marshal(), blob) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
config.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
var (
|
||||
principal Principal
|
||||
errs []error
|
||||
)
|
||||
|
||||
if checker != nil {
|
||||
if _, err := checker.Authenticate(conn, key); err != nil {
|
||||
log.Err(err).Debug("Certificate check failed")
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
principal = UserCertificatePrincipal{Certificate: key.(*ssh.Certificate)}
|
||||
}
|
||||
}
|
||||
if principal == nil && mfa.PublicKey != nil {
|
||||
if _, err := mfa.PublicKey.VerifyPublicKey(conn, key); err != nil {
|
||||
log.Err(err).Debug("Public key check failed")
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
principal = PublicKeyPrincipal{User: conn.User(), PublicKey: key}
|
||||
}
|
||||
}
|
||||
|
||||
if principal == nil {
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
|
||||
meta = conn
|
||||
principals = append(principals, principal)
|
||||
return checkPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
if mfa.Password != nil {
|
||||
log.Trace("MFA enabling password authentication")
|
||||
|
||||
config.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
if _, err := mfa.Password.VerifyPassword(conn, string(password)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta = conn
|
||||
principals = append(principals, PasswordPrincipal{User: conn.User()})
|
||||
return checkPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
if mfa.Token != nil {
|
||||
log.Trace("MFA enabling token authentication")
|
||||
|
||||
config.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||
var (
|
||||
multiline = mfa.Token.Multiline()
|
||||
token string
|
||||
)
|
||||
|
||||
for {
|
||||
answers, err := challenge("", mfa.Token.Instruction(), []string{"Token: "}, []bool{false})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(answers) != 1 {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
if multiline {
|
||||
if answers[0] == "" {
|
||||
break
|
||||
}
|
||||
token += strings.TrimSpace(answers[0])
|
||||
} else {
|
||||
token = answers[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := mfa.Token.VerifyToken(conn, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
meta = conn
|
||||
principals = append(principals, TokenPrincipal{User: conn.User()})
|
||||
return checkPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
return ssh.NewServerConn(conn, config)
|
||||
}
|
148
auth/principal.go
Normal file
148
auth/principal.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.maze.io/maze/conduit/policy/input"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeCertificate = "certificate"
|
||||
TypeToken = "token"
|
||||
TypePassword = "password"
|
||||
TypePublicKey = "publickey"
|
||||
)
|
||||
|
||||
type Attributes struct {
|
||||
Groups []string `json:"groups"`
|
||||
Principals []string `json:"principals"`
|
||||
Options map[string]string `json:"options"`
|
||||
Extensions map[string]string `json:"extensions"`
|
||||
Source any `json:"source"`
|
||||
}
|
||||
|
||||
func MakeAttributes() Attributes {
|
||||
return Attributes{
|
||||
Groups: make([]string, 0),
|
||||
Principals: make([]string, 0),
|
||||
Options: make(map[string]string),
|
||||
Extensions: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
type Principal interface {
|
||||
Type() string // "user", "host", "key", etc.
|
||||
|
||||
// Identity is the user identity.
|
||||
Identity() string
|
||||
|
||||
// Attributes for the principal.
|
||||
Attributes() Attributes
|
||||
}
|
||||
|
||||
type PasswordPrincipal struct {
|
||||
User string
|
||||
Attr Attributes
|
||||
}
|
||||
|
||||
func MakePasswordPrincipal(meta ssh.ConnMetadata) PasswordPrincipal {
|
||||
attr := MakeAttributes()
|
||||
attr.Source = meta
|
||||
return PasswordPrincipal{
|
||||
User: meta.User(),
|
||||
Attr: attr,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PasswordPrincipal) Type() string { return TypePassword }
|
||||
func (p PasswordPrincipal) Identity() string { return p.User }
|
||||
func (p PasswordPrincipal) Attributes() Attributes { return p.Attr }
|
||||
func (p PasswordPrincipal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"type"`
|
||||
User string `json:"identity"`
|
||||
Attr Attributes `json:"attr"`
|
||||
}{
|
||||
p.Type(),
|
||||
p.Identity(),
|
||||
p.Attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
type TokenPrincipal struct {
|
||||
User string `json:"identity"`
|
||||
Attr Attributes `json:"attr"`
|
||||
}
|
||||
|
||||
func (p TokenPrincipal) Type() string { return TypeToken }
|
||||
func (p TokenPrincipal) Identity() string { return p.User }
|
||||
func (p TokenPrincipal) Attributes() Attributes { return p.Attr }
|
||||
func (p TokenPrincipal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"type"`
|
||||
User string `json:"identity"`
|
||||
Attr Attributes `json:"attr"`
|
||||
}{
|
||||
p.Type(),
|
||||
p.Identity(),
|
||||
p.Attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
type PublicKeyPrincipal struct {
|
||||
User string
|
||||
ssh.PublicKey
|
||||
}
|
||||
|
||||
func MakePublicKeyPrincipal(meta ssh.ConnMetadata, key ssh.PublicKey) PublicKeyPrincipal {
|
||||
attr := MakeAttributes()
|
||||
attr.Source = meta
|
||||
return PublicKeyPrincipal{
|
||||
User: meta.User(),
|
||||
PublicKey: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PublicKeyPrincipal) Type() string { return TypePublicKey }
|
||||
func (p PublicKeyPrincipal) Identity() string { return p.User }
|
||||
func (p PublicKeyPrincipal) Attributes() Attributes { return MakeAttributes() }
|
||||
func (p PublicKeyPrincipal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"type"`
|
||||
User string `json:"identity"`
|
||||
Attr Attributes `json:"attr"`
|
||||
}{
|
||||
p.Type(),
|
||||
p.Identity(),
|
||||
p.Attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
type UserCertificatePrincipal struct {
|
||||
*ssh.Certificate
|
||||
}
|
||||
|
||||
func (cert UserCertificatePrincipal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"type"`
|
||||
User string `json:"identity"`
|
||||
Attr Attributes `json:"attr"`
|
||||
}{
|
||||
cert.Type(),
|
||||
cert.Identity(),
|
||||
cert.Attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
func (cert UserCertificatePrincipal) Type() string { return TypeCertificate }
|
||||
func (cert UserCertificatePrincipal) Identity() string { return cert.KeyId }
|
||||
func (cert UserCertificatePrincipal) Attributes() Attributes {
|
||||
return Attributes{
|
||||
Principals: cert.ValidPrincipals,
|
||||
Options: cert.CriticalOptions,
|
||||
Extensions: cert.Extensions,
|
||||
Source: input.NewCertificate(cert.Certificate),
|
||||
}
|
||||
}
|
105
auth/system_linux.go
Normal file
105
auth/system_linux.go
Normal file
@@ -0,0 +1,105 @@
|
||||
//go:build linux
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/msteinert/pam"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type system struct{}
|
||||
|
||||
type systemPrincipal struct {
|
||||
*user.User
|
||||
attributes Attributes
|
||||
}
|
||||
|
||||
func newSystemPrincipal(name string) (*systemPrincipal, error) {
|
||||
u, err := user.Lookup(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := Attributes{
|
||||
Custom: make(map[string]any),
|
||||
}
|
||||
if gids, err := u.GroupIds(); err == nil {
|
||||
a.Custom["groups"] = gids
|
||||
for _, gid := range gids {
|
||||
if g, err := user.LookupGroupId(gid); err == nil {
|
||||
a.Groups = append(a.Groups, g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &systemPrincipal{
|
||||
User: u,
|
||||
attributes: a,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u systemPrincipal) Type() string {
|
||||
return "user"
|
||||
}
|
||||
|
||||
func (u systemPrincipal) Identity() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
func (u systemPrincipal) Attributes() Attributes {
|
||||
return u.attributes
|
||||
}
|
||||
|
||||
func SystemPassword() Password {
|
||||
return system{}
|
||||
}
|
||||
|
||||
func (system) VerifyPassword(username, password string) (Principal, error) {
|
||||
t, err := pam.StartFunc("sshd", username, func(s pam.Style, msg string) (string, error) {
|
||||
switch s {
|
||||
case pam.PromptEchoOff:
|
||||
return password, nil
|
||||
case pam.PromptEchoOn:
|
||||
return username, nil
|
||||
default:
|
||||
return "", errors.New("unrecognized message style")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = t.Authenticate(0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newSystemPrincipal(username)
|
||||
}
|
||||
|
||||
func (system) VerifyPublicKey(username string, key ssh.PublicKey) (Principal, error) {
|
||||
p, err := newSystemPrincipal(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rest, err := os.ReadFile(filepath.Join(p.HomeDir, ".ssh", "authorized_keys"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for len(rest) > 0 {
|
||||
var out ssh.PublicKey
|
||||
if out, _, _, rest, err = ssh.ParseAuthorizedKey(rest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bytes.Equal(out.Marshal(), key.Marshal()) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrAuthorized
|
||||
}
|
3
auth/testdata/passwd
vendored
Normal file
3
auth/testdata/passwd
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# This line is ignored
|
||||
example:$argon2id$v=19$m=2097152,t=1,p=4$BjVeoTI4ntTQc0WkFQdLWg$OAUnkkyx5STI0Ixl+OSpv4JnI6J1TYWKuCuvIbUGHTY
|
||||
bcrypt:$2y$10$jeTxJGC9SZ1KZgbBZoZeseq0H8Gi1yqxvGX43YnwodTYeUxvl6TPK
|
5
auth/testdata/pubkey
vendored
Normal file
5
auth/testdata/pubkey
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
maze:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIjJv5YZX22u40Wr+DRHH6jnCjxqk1u7rvNc6ALsCncK
|
||||
maze:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCce9LTIeQ4FvyHUMzOK0NhZsVtuWIdUDOdl3j+5rtvrdsVzlLp2eqaSQEwZwPl7qek0M7+5i8DoSzZjg9MrsUsCiSXqRY9/G3M/KwL+MaL0116R7uwkQX+ndKvUvnqpjKKOJ/PsBFmZlXKYPDy6SFJURpmRATiafenEkg5D64fxBFw1k66ZXcQ81aYAGjpV8nqE18DcfVfj6czadpQ5Ycf/D4InUTPKTuFa2lMdVqrZ1+S6DDGIQdG9HEI7IpzFfYvGEFQc2x4BaeHroNym/k9PtIH+4debEDkrZ7Aaq/ofXTWoWLR4KoZoHcyUSGlPT+M9/aICgqaza/VZfPgobbiXRQTwNfNe4lUcbAhracX4RQDJbyPlHMtGAuDrIi6WLyBKZp4ehZPIT00YbPMP5BRTcPPwUneYVL2D1vbgWLO+0GsYfdr7fsm5TPd6fkajNj08ZSOWYRdJuoZJkplqM1GETlXkv5ictZ9k2Nm6K0qxBKIxaaBMm46jvcTM9K2YRK7QdvLTrXQzUYvR6HqcHUN3rAMT0LuyKY69FzJXuKaO4HYHQB0MaXJW/CVIo1xR92sqHUmcKn5nwyccT4hUscPAx/lNE7AZ8qcTqUqYRACF9dJhKOZD2dyO8dBHgnCqkg5JfifmbdCSEc2nGrGPx3rHRj/JXNqI404T27yB26mQw==
|
||||
test_a:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFo1lt6lEk+1VUrMbhlaVpkI0p1TFUGujHaKKn7+VoGb
|
||||
test_b:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICA9dQjNeX3eBvkOXJN+nJm1C2W9UtRiLbK9O87Mjkir
|
||||
test_b:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFq82Pfsg7KjTU5LN4jikxITDQhCWB3TFxQdXTgYtKt40+gv88hZkemM1MYTzR30bUX/zcRsioUSwr3u7/2La7ti+BoilsHjrEx4w+nxNGCCe8D3M6K5Xi8MPL2AqbXFqkPSEpX+psrs+qILfNhs1lWAsN7GLP0cTIxPynFNECwJnUlleN0hsn8N8bQCoUInZQGmHwIHq62H+3IPbv7Vko3J0Zrqqo4OqfeV5BA0By7ZP+2Jd9ZsLJ2efaiALcs6oTk0v95wVQ36wp605x9ePYg6zHzIZDfpA400RqeuiZF5jpiG7q3eb0+CysfMbU0BpfeHmCq15PFYqre8HKAJZ3
|
Reference in New Issue
Block a user