Host key checking shared across users

This commit is contained in:
Manfred Touron 2017-12-01 22:17:44 +01:00
parent 017ee2ab39
commit 511470087b
8 changed files with 80 additions and 21 deletions

View file

@ -5,6 +5,7 @@
* Create Session objects on each connections (history)
* Connection history
* Audit log
* Add dynamic strict host key checking (learning on the first time, strict on the next ones)
## v1.4.0 (2017-11-24)

View file

@ -37,6 +37,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
* Sensitive data encryption
* Session management
* Audit log
* Host Keys verifications shared across users
## Usage
@ -153,11 +154,11 @@ event inspect [-h] EVENT...
# host management
host help
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <username>[:<password>]@<host>[:<port>]
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <username>[:<password>]@<host>[:<port>]
host inspect [-h] [--decrypt] HOST...
host ls [-h]
host rm [-h] HOST...
host update [-h] [--name=<value>] [--comment=<value>] [--fingerprint=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] HOST...
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] HOST...
# hostgroup management
hostgroup help

20
db.go
View file

@ -34,6 +34,7 @@ type Setting struct {
Value string `valid:"required"`
}
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
type SSHKey struct {
// FIXME: use uuid for ID
gorm.Model
@ -50,17 +51,18 @@ type SSHKey struct {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string `valid:"optional"`
}
// UserKey defines a user public key used by sshportal to identify the user
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`

View file

@ -370,6 +370,28 @@ func dbInit(db *gorm.DB) error {
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "25",
Migrate: func(tx *gorm.DB) error {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Host{}).Error
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
},
})
if err := m.Migrate(); err != nil {

View file

@ -167,7 +167,7 @@ func server(c *cli.Context) error {
fmt.Fprintf(s, "error: %v\n", err)
return
}
err := proxy(s, host)
err := proxy(s, host, DynamicHostKey(db, host))
sessUpdate := Session{}
if err != nil {
fmt.Fprintf(s, "error: %v\n", err)

View file

@ -10,8 +10,8 @@ import (
gossh "golang.org/x/crypto/ssh"
)
func proxy(s ssh.Session, host *Host) error {
config, err := host.ClientConfig(s)
func proxy(s ssh.Session, host *Host, hk gossh.HostKeyCallback) error {
config, err := host.ClientConfig(s, hk)
if err != nil {
return err
}
@ -27,7 +27,7 @@ func proxy(s ssh.Session, host *Host) error {
return err
}
log.Println("SSH Connectin established")
log.Println("SSH Connection established")
return pipe(s.MaskedReqs(), rreqs, s, rch)
}
@ -76,10 +76,10 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
return nil
}
func (host *Host) ClientConfig(_ ssh.Session) (*gossh.ClientConfig, error) {
func (host *Host) ClientConfig(_ ssh.Session, hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.User,
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
HostKeyCallback: hk,
Auth: []gossh.AuthMethod{},
}
if host.SSHKey != nil {

View file

@ -539,7 +539,6 @@ GLOBAL OPTIONS:
Flags: []cli.Flag{
cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"},
cli.StringFlag{Name: "password, p", Usage: "If present, sshportal will use password-based authentication"},
cli.StringFlag{Name: "fingerprint, f", Usage: "SSH host key fingerprint"},
cli.StringFlag{Name: "comment, c"},
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
@ -560,7 +559,6 @@ GLOBAL OPTIONS:
if c.String("password") != "" {
host.Password = c.String("password")
}
host.Fingerprint = c.String("fingerprint")
host.Name = strings.Split(host.Hostname(), ".")[0]
if c.String("name") != "" {
@ -708,7 +706,6 @@ GLOBAL OPTIONS:
Flags: []cli.Flag{
cli.StringFlag{Name: "name, n", Usage: "Rename the host"},
cli.StringFlag{Name: "password, p", Usage: "Update/set a password, use \"none\" to unset"},
cli.StringFlag{Name: "fingerprint, f", Usage: "Update/set a host fingerprint, use \"none\" to unset"},
cli.StringFlag{Name: "comment, c", Usage: "Update/set a host comment"},
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
@ -736,7 +733,7 @@ GLOBAL OPTIONS:
for _, host := range hosts {
model := tx.Model(&host)
// simple fields
for _, fieldname := range []string{"name", "comment", "password", "fingerprint"} {
for _, fieldname := range []string{"name", "comment", "password"} {
if c.String(fieldname) != "" {
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
tx.Rollback()

36
ssh.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"bytes"
"fmt"
"log"
"net"
"github.com/jinzhu/gorm"
gossh "golang.org/x/crypto/ssh"
)
type dynamicHostKey struct {
db *gorm.DB
host *Host
}
func (d *dynamicHostKey) check(hostname string, remote net.Addr, key gossh.PublicKey) error {
if len(d.host.HostKey) == 0 {
log.Println("Discovering host fingerprint...")
return d.db.Model(d.host).Update("HostKey", key.Marshal()).Error
}
if !bytes.Equal(d.host.HostKey, key.Marshal()) {
return fmt.Errorf("ssh: host key mismatch")
}
return nil
}
// DynamicHostKey returns a function for use in
// ClientConfig.HostKeyCallback to dynamically learn or accept host key.
func DynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
// FIXME: forward interactively the host key checking
hk := &dynamicHostKey{db, host}
return hk.check
}