mirror of
https://github.com/moul/sshportal.git
synced 2025-09-14 00:24:37 +08:00
Host key checking shared across users
This commit is contained in:
parent
017ee2ab39
commit
511470087b
8 changed files with 80 additions and 21 deletions
|
@ -5,6 +5,7 @@
|
||||||
* Create Session objects on each connections (history)
|
* Create Session objects on each connections (history)
|
||||||
* Connection history
|
* Connection history
|
||||||
* Audit log
|
* Audit log
|
||||||
|
* Add dynamic strict host key checking (learning on the first time, strict on the next ones)
|
||||||
|
|
||||||
## v1.4.0 (2017-11-24)
|
## v1.4.0 (2017-11-24)
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
|
||||||
* Sensitive data encryption
|
* Sensitive data encryption
|
||||||
* Session management
|
* Session management
|
||||||
* Audit log
|
* Audit log
|
||||||
|
* Host Keys verifications shared across users
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -153,11 +154,11 @@ event inspect [-h] EVENT...
|
||||||
|
|
||||||
# host management
|
# host management
|
||||||
host help
|
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 inspect [-h] [--decrypt] HOST...
|
||||||
host ls [-h]
|
host ls [-h]
|
||||||
host rm [-h] HOST...
|
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 management
|
||||||
hostgroup help
|
hostgroup help
|
||||||
|
|
20
db.go
20
db.go
|
@ -34,6 +34,7 @@ type Setting struct {
|
||||||
Value string `valid:"required"`
|
Value string `valid:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
|
||||||
type SSHKey struct {
|
type SSHKey struct {
|
||||||
// FIXME: use uuid for ID
|
// FIXME: use uuid for ID
|
||||||
gorm.Model
|
gorm.Model
|
||||||
|
@ -50,17 +51,18 @@ type SSHKey struct {
|
||||||
type Host struct {
|
type Host struct {
|
||||||
// FIXME: use uuid for ID
|
// FIXME: use uuid for ID
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
|
||||||
Addr string `valid:"required"`
|
Addr string `valid:"required"`
|
||||||
User string `valid:"optional"`
|
User string `valid:"optional"`
|
||||||
Password string `valid:"optional"`
|
Password string `valid:"optional"`
|
||||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
|
||||||
SSHKeyID uint `gorm:"index"`
|
SSHKeyID uint `gorm:"index"`
|
||||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
HostKey []byte `sql:"size:10000" valid:"optional"`
|
||||||
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
|
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||||
Comment string `valid:"optional"`
|
Comment string `valid:"optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserKey defines a user public key used by sshportal to identify the user
|
||||||
type UserKey struct {
|
type UserKey struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
|
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||||
|
|
22
dbinit.go
22
dbinit.go
|
@ -370,6 +370,28 @@ func dbInit(db *gorm.DB) error {
|
||||||
Rollback: func(tx *gorm.DB) error {
|
Rollback: func(tx *gorm.DB) error {
|
||||||
return fmt.Errorf("not implemented")
|
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 {
|
if err := m.Migrate(); err != nil {
|
||||||
|
|
2
main.go
2
main.go
|
@ -167,7 +167,7 @@ func server(c *cli.Context) error {
|
||||||
fmt.Fprintf(s, "error: %v\n", err)
|
fmt.Fprintf(s, "error: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := proxy(s, host)
|
err := proxy(s, host, DynamicHostKey(db, host))
|
||||||
sessUpdate := Session{}
|
sessUpdate := Session{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(s, "error: %v\n", err)
|
fmt.Fprintf(s, "error: %v\n", err)
|
||||||
|
|
10
proxy.go
10
proxy.go
|
@ -10,8 +10,8 @@ import (
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func proxy(s ssh.Session, host *Host) error {
|
func proxy(s ssh.Session, host *Host, hk gossh.HostKeyCallback) error {
|
||||||
config, err := host.ClientConfig(s)
|
config, err := host.ClientConfig(s, hk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func proxy(s ssh.Session, host *Host) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("SSH Connectin established")
|
log.Println("SSH Connection established")
|
||||||
return pipe(s.MaskedReqs(), rreqs, s, rch)
|
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
|
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{
|
config := gossh.ClientConfig{
|
||||||
User: host.User,
|
User: host.User,
|
||||||
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
HostKeyCallback: hk,
|
||||||
Auth: []gossh.AuthMethod{},
|
Auth: []gossh.AuthMethod{},
|
||||||
}
|
}
|
||||||
if host.SSHKey != nil {
|
if host.SSHKey != nil {
|
||||||
|
|
5
shell.go
5
shell.go
|
@ -539,7 +539,6 @@ GLOBAL OPTIONS:
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"},
|
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: "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: "comment, c"},
|
||||||
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
cli.StringFlag{Name: "key, k", Usage: "`KEY` to use for authentication"},
|
||||||
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
cli.StringSliceFlag{Name: "group, g", Usage: "Assigns the host to `HOSTGROUPS` (default: \"default\")"},
|
||||||
|
@ -560,7 +559,6 @@ GLOBAL OPTIONS:
|
||||||
if c.String("password") != "" {
|
if c.String("password") != "" {
|
||||||
host.Password = c.String("password")
|
host.Password = c.String("password")
|
||||||
}
|
}
|
||||||
host.Fingerprint = c.String("fingerprint")
|
|
||||||
host.Name = strings.Split(host.Hostname(), ".")[0]
|
host.Name = strings.Split(host.Hostname(), ".")[0]
|
||||||
|
|
||||||
if c.String("name") != "" {
|
if c.String("name") != "" {
|
||||||
|
@ -708,7 +706,6 @@ GLOBAL OPTIONS:
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{Name: "name, n", Usage: "Rename the host"},
|
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: "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: "comment, c", Usage: "Update/set a host comment"},
|
||||||
cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"},
|
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`"},
|
cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"},
|
||||||
|
@ -736,7 +733,7 @@ GLOBAL OPTIONS:
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
model := tx.Model(&host)
|
model := tx.Model(&host)
|
||||||
// simple fields
|
// simple fields
|
||||||
for _, fieldname := range []string{"name", "comment", "password", "fingerprint"} {
|
for _, fieldname := range []string{"name", "comment", "password"} {
|
||||||
if c.String(fieldname) != "" {
|
if c.String(fieldname) != "" {
|
||||||
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
36
ssh.go
Normal file
36
ssh.go
Normal 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
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue