Added security checks & server storage engine

This commit is contained in:
Carlos Salguero
2017-01-19 17:19:13 -03:00
parent 6e3b2ce3ad
commit 98fa733c0c
11 changed files with 283 additions and 47 deletions

View File

@@ -60,6 +60,16 @@ type Security struct {
type Net struct { type Net struct {
HTTP HTTP `bson:"http"` HTTP HTTP `bson:"http"`
SSL SSL `bson:"ssl"` SSL SSL `bson:"ssl"`
Port int64 `bson:"port"`
BindIP string `bson:"bindIp"`
MaxIncomingConnections int `bson:"maxIncomingConnections"`
WireObjectCheck bool `bson:"wireObjectCheck"`
IPv6 bool `bson:"ipv6"`
UnixDomainSocket struct {
Enabled bool `bson:"enabled"`
PathPrefix string `bson:"pathPrefix"`
FilePermissions int64 `bson:"filePermissions"`
} `bson:"unixDomainSocket"`
} }
type HTTP struct { type HTTP struct {

View File

@@ -20,6 +20,7 @@ type Members struct {
ElectionTime int64 `bson:"electionTime"` // For the current primary, information regarding the election Timestamp from the operation log. ElectionTime int64 `bson:"electionTime"` // For the current primary, information regarding the election Timestamp from the operation log.
ElectionDate string `bson:"electionDate"` // For the current primary, an ISODate formatted date string that reflects the election date ElectionDate string `bson:"electionDate"` // For the current primary, an ISODate formatted date string that reflects the election date
Set string `bson:"-"` Set string `bson:"-"`
StorageEngine StorageEngine
} }
// Struct for replSetGetStatus // Struct for replSetGetStatus

View File

@@ -25,10 +25,17 @@ type ServerStatus struct {
Mem *MemStats `bson:"mem"` Mem *MemStats `bson:"mem"`
Repl *ReplStatus `bson:"repl"` Repl *ReplStatus `bson:"repl"`
ShardCursorType map[string]interface{} `bson:"shardCursorType"` ShardCursorType map[string]interface{} `bson:"shardCursorType"`
StorageEngine map[string]string `bson:"storageEngine"` StorageEngine StorageEngine `bson:"storageEngine"`
WiredTiger *WiredTiger `bson:"wiredTiger"` WiredTiger *WiredTiger `bson:"wiredTiger"`
} }
type StorageEngine struct {
Name string `bson:"name"`
SupportCommittedREads bool `bson:supportsCommittedReads"`
ReadOnly bool `bson:"readOnly"`
Persistent bool `bson:"persistent"`
}
// WiredTiger stores information related to the WiredTiger storage engine. // WiredTiger stores information related to the WiredTiger storage engine.
type WiredTiger struct { type WiredTiger struct {
Transaction TransactionStats `bson:"transaction"` Transaction TransactionStats `bson:"transaction"`

View File

@@ -2,11 +2,13 @@ package main
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"strings" "strings"
"text/template" "text/template"
"time" "time"
version "github.com/hashicorp/go-version"
"github.com/howeyc/gopass" "github.com/howeyc/gopass"
"github.com/pborman/getopt" "github.com/pborman/getopt"
"github.com/percona/percona-toolkit/src/go/lib/config" "github.com/percona/percona-toolkit/src/go/lib/config"
@@ -82,6 +84,9 @@ type security struct {
Roles int Roles int
Auth string Auth string
SSL string SSL string
BindIP string
Port int64
WarningMsgs []string
} }
type databases struct { type databases struct {
@@ -209,7 +214,9 @@ func main() {
} }
// //
if hostInfo, err := GetHostinfo(session); err != nil {
hostInfo, err := GetHostinfo(session)
if err != nil {
log.Printf("[Error] cannot get host info: %v\n", err) log.Printf("[Error] cannot get host info: %v\n", err)
} else { } else {
log.Debugf("hostInfo:\n%+v\n", hostInfo) log.Debugf("hostInfo:\n%+v\n", hostInfo)
@@ -226,7 +233,7 @@ func main() {
t.Execute(os.Stdout, rops) t.Execute(os.Stdout, rops)
} }
if security, err := GetSecuritySettings(session); err != nil { if security, err := GetSecuritySettings(session, hostInfo.Version); err != nil {
log.Printf("[Error] cannot get security settings: %v\n", err) log.Printf("[Error] cannot get security settings: %v\n", err)
} else { } else {
t := template.Must(template.New("ssl").Parse(templates.Security)) t := template.Must(template.New("ssl").Parse(templates.Security))
@@ -396,13 +403,14 @@ func GetReplicasetMembers(dialer pmgo.Dialer, hostnames []string, di *mgo.DialIn
log.Debugf("hostnames: %+v", hostnames) log.Debugf("hostnames: %+v", hostnames)
for _, hostname := range hostnames { for _, hostname := range hostnames {
di.Addrs = []string{hostname} tmpdi := *di
session, err := dialer.DialWithInfo(di) tmpdi.Addrs = []string{hostname}
log.Debugf("GetReplicasetMembers connecting to %s", hostname)
session, err := dialer.DialWithInfo(&tmpdi)
if err != nil { if err != nil {
log.Debugf("getReplicasetMembers. cannot connect to %s: %s", hostname, err.Error()) log.Debugf("getReplicasetMembers. cannot connect to %s: %s", hostname, err.Error())
return nil, errors.Wrapf(err, "getReplicasetMembers. cannot connect to %s", hostname) return nil, errors.Wrapf(err, "getReplicasetMembers. cannot connect to %s", hostname)
} }
defer session.Close()
rss := proto.ReplicaSetStatus{} rss := proto.ReplicaSetStatus{}
err = session.Run(bson.M{"replSetGetStatus": 1}, &rss) err = session.Run(bson.M{"replSetGetStatus": 1}, &rss)
@@ -413,21 +421,55 @@ func GetReplicasetMembers(dialer pmgo.Dialer, hostnames []string, di *mgo.DialIn
log.Debugf("replSetGetStatus result:\n%#v", rss) log.Debugf("replSetGetStatus result:\n%#v", rss)
for _, m := range rss.Members { for _, m := range rss.Members {
m.Set = rss.Set m.Set = rss.Set
if serverStatus, err := getServerStatus(dialer, di, m.Name); err == nil {
m.StorageEngine = serverStatus.StorageEngine
} else {
log.Warnf("getReplicasetMembers. cannot get server status: %v", err.Error())
}
replicaMembers = append(replicaMembers, m) replicaMembers = append(replicaMembers, m)
} }
session.Close()
} }
return replicaMembers, nil return replicaMembers, nil
} }
func GetSecuritySettings(session pmgo.SessionManager) (*security, error) { func getServerStatus(dialer pmgo.Dialer, di *mgo.DialInfo, hostname string) (proto.ServerStatus, error) {
ss := proto.ServerStatus{}
tmpdi := *di
tmpdi.Addrs = []string{hostname}
log.Debugf("GetReplicasetMembers connecting to %s", hostname)
session, err := dialer.DialWithInfo(&tmpdi)
if err != nil {
return ss, errors.Wrapf(err, "getReplicasetMembers. cannot connect to %s", hostname)
}
defer session.Close()
if err := session.DB("admin").Run(bson.D{{"serverStatus", 1}, {"recordStats", 1}}, &ss); err != nil {
return ss, errors.Wrap(err, "GetHostInfo.serverStatus")
}
return ss, nil
}
func GetSecuritySettings(session pmgo.SessionManager, ver string) (*security, error) {
s := security{ s := security{
Auth: "disabled", Auth: "disabled",
SSL: "disabled", SSL: "disabled",
} }
v26, _ := version.NewVersion("2.6")
mongoVersion, err := version.NewVersion(ver)
prior26 := false
if err == nil && mongoVersion.LessThan(v26) {
prior26 = true
}
cmdOpts := proto.CommandLineOptions{} cmdOpts := proto.CommandLineOptions{}
err := session.DB("admin").Run(bson.D{{"getCmdLineOpts", 1}, {"recordStats", 1}}, &cmdOpts) err = session.DB("admin").Run(bson.D{{"getCmdLineOpts", 1}, {"recordStats", 1}}, &cmdOpts)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot get command line options") return nil, errors.Wrap(err, "cannot get command line options")
} }
@@ -435,10 +477,37 @@ func GetSecuritySettings(session pmgo.SessionManager) (*security, error) {
if cmdOpts.Security.Authorization != "" || cmdOpts.Security.KeyFile != "" { if cmdOpts.Security.Authorization != "" || cmdOpts.Security.KeyFile != "" {
s.Auth = "enabled" s.Auth = "enabled"
} }
if cmdOpts.Parsed.Net.SSL.Mode != "" && cmdOpts.Parsed.Net.SSL.Mode != "disabled" { if cmdOpts.Parsed.Net.SSL.Mode != "" && cmdOpts.Parsed.Net.SSL.Mode != "disabled" {
s.SSL = cmdOpts.Parsed.Net.SSL.Mode s.SSL = cmdOpts.Parsed.Net.SSL.Mode
} }
s.BindIP = cmdOpts.Parsed.Net.BindIP
s.Port = cmdOpts.Parsed.Net.Port
if cmdOpts.Parsed.Net.BindIP == "" {
if prior26 {
s.WarningMsgs = append(s.WarningMsgs, "WARNING: You might be insecure. There is no IP binding")
}
} else {
ips := strings.Split(cmdOpts.Parsed.Net.BindIP, ",")
extIP, _ := externalIP()
for _, ip := range ips {
isPrivate, err := isPrivateNetwork(strings.TrimSpace(ip))
if !isPrivate && err == nil {
if s.Auth == "enabled" {
s.WarningMsgs = append(s.WarningMsgs, fmt.Sprintf("Warning: You might be insecure (bind ip %s is public)", ip))
} else {
s.WarningMsgs = append(s.WarningMsgs, fmt.Sprintf("Error. You are insecure: bind ip %s is public and auth is disabled", ip))
}
} else {
if ip != "127.0.0.1" && ip != extIP {
s.WarningMsgs = append(s.WarningMsgs, fmt.Sprintf("WARNING: You might be insecure. IP binding %s is not localhost"))
}
}
}
}
s.Users, err = session.DB("admin").C("system.users").Count() s.Users, err = session.DB("admin").C("system.users").Count()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot get users count") return nil, errors.Wrap(err, "cannot get users count")
@@ -698,3 +767,62 @@ func GetShardingChangelogStatus(session pmgo.SessionManager) (*proto.ShardingCha
Items: &qresults, Items: &qresults,
}, nil }, nil
} }
func isPrivateNetwork(ip string) (bool, error) {
privateCIDRs := []string{"10.0.0.0/24", "172.16.0.0/20", "192.168.0.0/16"}
if net.ParseIP(ip).String() == "127.0.0.1" {
return true, nil
}
for _, cidr := range privateCIDRs {
_, cidrnet, err := net.ParseCIDR(cidr)
if err != nil {
return false, err
}
addr := net.ParseIP(ip)
if cidrnet.Contains(addr) {
return true, nil
}
}
return false, nil
}
func externalIP() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", errors.New("are you connected to the network?")
}

View File

@@ -126,6 +126,9 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2, Roles: 2,
Auth: "disabled", Auth: "disabled",
SSL: "disabled", SSL: "disabled",
BindIP: "",
Port: 0,
WarningMsgs: nil,
}, },
// 2 // 2
&security{ &security{
@@ -133,6 +136,8 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2, Roles: 2,
Auth: "enabled", Auth: "enabled",
SSL: "disabled", SSL: "disabled",
BindIP: "", Port: 0,
WarningMsgs: nil,
}, },
// 3 // 3
&security{ &security{
@@ -140,6 +145,9 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2, Roles: 2,
Auth: "enabled", Auth: "enabled",
SSL: "disabled", SSL: "disabled",
BindIP: "",
Port: 0,
WarningMsgs: nil,
}, },
// 4 // 4
&security{ &security{
@@ -147,6 +155,9 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2, Roles: 2,
Auth: "disabled", Auth: "disabled",
SSL: "super secure", SSL: "super secure",
BindIP: "",
Port: 0,
WarningMsgs: nil,
}, },
} }
@@ -171,13 +182,13 @@ func TestSecurityOpts(t *testing.T) {
database.EXPECT().C("system.roles").Return(rolesCol) database.EXPECT().C("system.roles").Return(rolesCol)
rolesCol.EXPECT().Count().Return(2, nil) rolesCol.EXPECT().Count().Return(2, nil)
got, err := GetSecuritySettings(session) got, err := GetSecuritySettings(session, "3.2")
if err != nil { if err != nil {
t.Errorf("cannot get sec settings: %v", err) t.Errorf("cannot get sec settings: %v", err)
} }
if !reflect.DeepEqual(got, expect[i]) { if !reflect.DeepEqual(got, expect[i]) {
t.Errorf("got: %+v, expect: %+v\n", got, expect[i]) t.Errorf("got: %#v\nwant: %#v\n", got, expect[i])
} }
} }
} }
@@ -325,8 +336,28 @@ func TestGetReplicasetMembers(t *testing.T) {
Set: "r1", Set: "r1",
}} }}
database := pmgomock.NewMockDatabaseManager(ctrl)
ss := proto.ServerStatus{}
test.LoadJson("test/sample/serverstatus.json", &ss)
dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil) dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil)
session.EXPECT().Run(bson.M{"replSetGetStatus": 1}, gomock.Any()).SetArg(1, mockrss) session.EXPECT().Run(bson.M{"replSetGetStatus": 1}, gomock.Any()).SetArg(1, mockrss)
dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil)
session.EXPECT().DB("admin").Return(database)
database.EXPECT().Run(bson.D{{"serverStatus", 1}, {"recordStats", 1}}, gomock.Any()).SetArg(1, ss)
session.EXPECT().Close()
dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil)
session.EXPECT().DB("admin").Return(database)
database.EXPECT().Run(bson.D{{"serverStatus", 1}, {"recordStats", 1}}, gomock.Any()).SetArg(1, ss)
session.EXPECT().Close()
dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil)
session.EXPECT().DB("admin").Return(database)
database.EXPECT().Run(bson.D{{"serverStatus", 1}, {"recordStats", 1}}, gomock.Any()).SetArg(1, ss)
session.EXPECT().Close()
session.EXPECT().Close() session.EXPECT().Close()
di := &mgo.DialInfo{Addrs: []string{"localhost"}} di := &mgo.DialInfo{Addrs: []string{"localhost"}}
@@ -335,7 +366,7 @@ func TestGetReplicasetMembers(t *testing.T) {
t.Errorf("getReplicasetMembers: %v", err) t.Errorf("getReplicasetMembers: %v", err)
} }
if !reflect.DeepEqual(rss, expect) { if !reflect.DeepEqual(rss, expect) {
t.Errorf("getReplicasetMembers: got %+v, expected: %+v\n", rss, expect) t.Errorf("getReplicasetMembers:\ngot %#v\nwant: %#v\n", rss, expect)
} }
} }
@@ -376,6 +407,58 @@ func TestGetHostnames(t *testing.T) {
} }
} }
func TestIsPrivateNetwork(t *testing.T) {
//privateCIDRs := []string{"10.0.0.0/24", "172.16.0.0/20", "192.168.0.0/16"}
testdata :=
[]struct {
ip string
want bool
err error
}{
{
ip: "127.0.0.1",
want: true,
err: nil,
},
{
ip: "10.0.0.1",
want: true,
err: nil,
},
{
ip: "10.0.1.1",
want: false,
err: nil,
},
{
ip: "172.16.1.2",
want: true,
err: nil,
},
{
ip: "192.168.1.2",
want: true,
err: nil,
},
{
ip: "8.8.8.8",
want: false,
err: nil,
},
}
for _, in := range testdata {
got, err := isPrivateNetwork(in.ip)
if err != in.err {
t.Errorf("ip %s. got err: %s, want err: %v", in.ip, err, in.err)
}
if got != in.want {
t.Errorf("ip %s. got: %v, want : %v", in.ip, got, in.want)
}
}
}
func addToCounters(ss proto.ServerStatus, increment int64) proto.ServerStatus { func addToCounters(ss proto.ServerStatus, increment int64) proto.ServerStatus {
ss.Opcounters.Command += increment ss.Opcounters.Command += increment
ss.Opcounters.Delete += increment ss.Opcounters.Delete += increment

View File

@@ -1,7 +1,7 @@
package templates package templates
const Clusterwide = ` const Clusterwide = `
# Cluster wide ################################################################################# # Cluster wide ###########################################################################################
Databases: {{.TotalDBsCount}} Databases: {{.TotalDBsCount}}
Collections: {{.TotalCollectionsCount}} Collections: {{.TotalCollectionsCount}}
Sharded Collections: {{.ShardedColsCount}} Sharded Collections: {{.ShardedColsCount}}

View File

@@ -1,7 +1,7 @@
package templates package templates
const HostInfo = `# This host const HostInfo = `# This host
# Mongo Executable ############################################################################# # Mongo Executable #######################################################################################
Path to executable | {{.ProcPath}} Path to executable | {{.ProcPath}}
# Report On {{.ThisHostID}} ######################################## # Report On {{.ThisHostID}} ########################################
User | {{.ProcUserName}} User | {{.ProcUserName}}

View File

@@ -1,7 +1,7 @@
package templates package templates
const Oplog = ` const Oplog = `
# Oplog ######################################################################################## # Oplog ##################################################################################################
Oplog Size {{.Size}} Mb Oplog Size {{.Size}} Mb
Oplog Used {{.UsedMB}} Mb Oplog Used {{.UsedMB}} Mb
Oplog Length {{.Running}} Oplog Length {{.Running}}

View File

@@ -1,13 +1,13 @@
package templates package templates
const Replicas = ` const Replicas = `
# Instances #################################################################################### # Instances ##############################################################################################
ID Host Type ReplSet ID Host Type ReplSet Engine
{{- if . -}} {{- if . -}}
{{- range . }} {{- range . }}
{{printf "% 3d" .Id}} {{printf "%-30s" .Name}} {{printf "%-30s" .StateStr}} {{printf "%10s" .Set -}} {{printf "% 3d" .Id}} {{printf "%-30s" .Name}} {{printf "%-30s" .StateStr}} {{printf "%10s" .Set }} {{printf "%20s" .StorageEngine.Name -}}
{{end}} {{end}}
{{else}} {{else}}
No replica sets found no replica sets found
{{end}} {{end}}
` `

View File

@@ -1,7 +1,7 @@
package templates package templates
const RunningOps = ` const RunningOps = `
# Running Ops ################################################################################## # Running Ops ############################################################################################
Type Min Max Avg Type Min Max Avg
Insert {{printf "% 8d" .Insert.Min}} {{printf "% 8d" .Insert.Max}} {{printf "% 8d" .Insert.Avg}}/{{.SampleRate}} Insert {{printf "% 8d" .Insert.Min}} {{printf "% 8d" .Insert.Max}} {{printf "% 8d" .Insert.Avg}}/{{.SampleRate}}

View File

@@ -1,9 +1,16 @@
package templates package templates
const Security = ` const Security = `
# Security ##################################################################################### # Security ###############################################################################################
Users {{.Users}} Users : {{.Users}}
Roles {{.Roles}} Roles : {{.Roles}}
Auth {{.Auth}} Auth : {{.Auth}}
SSL {{.SSL}} SSL : {{.SSL}}
` Port : {{.Port}}
Bind IP: {{.BindIP}}
{{- if .WarningMsgs -}}
{{- range .WarningMsgs }}
{{ . }}
{{end}}
{{end }}
`