Initial import
This commit is contained in:
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)
|
||||
}
|
Reference in New Issue
Block a user