Merge pull request #212 from GreyOBox/dev/GreyOBox/acls-cmd-hook

This commit is contained in:
Manfred Touron 2021-04-25 11:50:28 +02:00 committed by GitHub
commit 0415f116ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 10 deletions

View file

@ -83,6 +83,11 @@ func main() {
Value: 0,
Usage: "Duration before an inactive connection is timed out (0 to disable)",
},
cli.StringFlag{
Name: "acl-check-cmd",
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
Usage: "Execute external command to check ACL",
},
},
}, {
Name: "healthcheck",

View file

@ -1,19 +1,28 @@
package bastion
import (
"context"
"encoding/json"
"fmt"
"log"
"os/exec"
"sort"
"strings"
"time"
"moul.io/sshportal/pkg/dbmodels"
)
// ACLHookTimeout is timeout for external ACL hook execution
const ACLHookTimeout = 2 * time.Second
type byWeight []*dbmodels.ACL
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func checkACLs(user dbmodels.User, host dbmodels.Host) string {
func checkACLs(user dbmodels.User, host dbmodels.Host, aclCheckCmd string) string {
currentTime := time.Now()
// shared ACLs between user and host
@ -34,9 +43,13 @@ func checkACLs(user dbmodels.User, host dbmodels.Host) string {
}
// FIXME: add ACLs that match host pattern
// deny by default if no shared ACL
// if no shared ACL then execute ACLs hook if it exists and return its result
if len(aclMap) == 0 {
return string(dbmodels.ACLActionDeny) // default action
action, err := checkACLsHook(aclCheckCmd, string(dbmodels.ACLActionDeny), user, host)
if err != nil {
log.Println(err)
}
return action
}
// transform map to slice and sort it
@ -46,5 +59,62 @@ func checkACLs(user dbmodels.User, host dbmodels.Host) string {
}
sort.Sort(byWeight(acls))
return acls[0].Action
action, err := checkACLsHook(aclCheckCmd, acls[0].Action, user, host)
if err != nil {
log.Println(err)
}
return action
}
// checkACLsHook executes external command to check ACL and passes following parameters:
// $1 - SSH Portal `action` (`allow` or `deny`)
// $2 - User as JSON string
// $3 - Host as JSON string
// External program has to return `allow` or `deny` in stdout.
// In case of any error function returns `action`.
func checkACLsHook(aclCheckCmd string, action string, user dbmodels.User, host dbmodels.Host) (string, error) {
if aclCheckCmd == "" {
return action, nil
}
ctx, cancel := context.WithTimeout(context.Background(), ACLHookTimeout)
defer cancel()
jsonUser, err := json.Marshal(user)
if err != nil {
return action, err
}
jsonHost, err := json.Marshal(host)
if err != nil {
return action, err
}
args := []string{
action,
string(jsonUser),
string(jsonHost),
}
cmd := exec.CommandContext(ctx, aclCheckCmd, args...)
out, err := cmd.Output()
if err != nil {
return action, err
}
if ctx.Err() == context.DeadlineExceeded {
return action, fmt.Errorf("external ACL hook command timed out")
}
outStr := strings.TrimSuffix(string(out), "\n")
switch outStr {
case string(dbmodels.ACLActionAllow):
return string(dbmodels.ACLActionAllow), nil
case string(dbmodels.ACLActionDeny):
return string(dbmodels.ACLActionDeny), nil
default:
return action, fmt.Errorf("acl-check-cmd wrong output '%s'", outStr)
}
}

View file

@ -43,7 +43,7 @@ func TestCheckACLs(t *testing.T) {
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action := checkACLs(users[0], hosts[0])
action := checkACLs(users[0], hosts[0], "")
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
})
}

View file

@ -28,6 +28,7 @@ type authContext struct {
db *gorm.DB
userKey dbmodels.UserKey
logsLocation string
aclCheckCmd string
aesKey string
dbDriver, dbURL string
bindAddr string
@ -206,7 +207,7 @@ func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientCon
return nil, err
}
action := checkACLs(tmpUser, tmpHost)
action := checkACLs(tmpUser, tmpHost, actx.aclCheckCmd)
switch action {
case string(dbmodels.ACLActionAllow):
// do nothing
@ -251,12 +252,13 @@ func ShellHandler(s ssh.Session, version, gitSha, gitTag string) {
panic("should not happen")
}
func PasswordAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
func PasswordAuthHandler(db *gorm.DB, logsLocation, aclCheckCmd, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
return func(ctx ssh.Context, pass string) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aclCheckCmd: aclCheckCmd,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
@ -287,12 +289,13 @@ func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
}
}
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aclCheckCmd, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aclCheckCmd: aclCheckCmd,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,

View file

@ -22,6 +22,7 @@ type serverConfig struct {
bindAddr string
debug, demo bool
idleTimeout time.Duration
aclCheckCmd string
}
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
@ -34,6 +35,7 @@ func parseServerConfig(c *cli.Context) (*serverConfig, error) {
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
aclCheckCmd: c.String("acl-check-cmd"),
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
@ -119,8 +121,8 @@ func server(c *serverConfig) (err error) {
for _, opt := range []ssh.Option{
// custom PublicKeyAuth handler
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aclCheckCmd, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aclCheckCmd, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
// retrieve sshportal SSH private key from database
bastion.PrivateKeyFromDB(db, c.aesKey),
} {