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 {
HTTP HTTP `bson:"http"`
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 {

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.
ElectionDate string `bson:"electionDate"` // For the current primary, an ISODate formatted date string that reflects the election date
Set string `bson:"-"`
StorageEngine StorageEngine
}
// Struct for replSetGetStatus

View File

@@ -25,10 +25,17 @@ type ServerStatus struct {
Mem *MemStats `bson:"mem"`
Repl *ReplStatus `bson:"repl"`
ShardCursorType map[string]interface{} `bson:"shardCursorType"`
StorageEngine map[string]string `bson:"storageEngine"`
StorageEngine StorageEngine `bson:"storageEngine"`
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.
type WiredTiger struct {
Transaction TransactionStats `bson:"transaction"`

View File

@@ -2,11 +2,13 @@ package main
import (
"fmt"
"net"
"os"
"strings"
"text/template"
"time"
version "github.com/hashicorp/go-version"
"github.com/howeyc/gopass"
"github.com/pborman/getopt"
"github.com/percona/percona-toolkit/src/go/lib/config"
@@ -82,6 +84,9 @@ type security struct {
Roles int
Auth string
SSL string
BindIP string
Port int64
WarningMsgs []string
}
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)
} else {
log.Debugf("hostInfo:\n%+v\n", hostInfo)
@@ -226,7 +233,7 @@ func main() {
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)
} else {
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)
for _, hostname := range hostnames {
di.Addrs = []string{hostname}
session, err := dialer.DialWithInfo(di)
tmpdi := *di
tmpdi.Addrs = []string{hostname}
log.Debugf("GetReplicasetMembers connecting to %s", hostname)
session, err := dialer.DialWithInfo(&tmpdi)
if err != nil {
log.Debugf("getReplicasetMembers. cannot connect to %s: %s", hostname, err.Error())
return nil, errors.Wrapf(err, "getReplicasetMembers. cannot connect to %s", hostname)
}
defer session.Close()
rss := proto.ReplicaSetStatus{}
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)
for _, m := range rss.Members {
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)
}
session.Close()
}
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{
Auth: "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{}
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 {
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 != "" {
s.Auth = "enabled"
}
if cmdOpts.Parsed.Net.SSL.Mode != "" && cmdOpts.Parsed.Net.SSL.Mode != "disabled" {
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()
if err != nil {
return nil, errors.Wrap(err, "cannot get users count")
@@ -698,3 +767,62 @@ func GetShardingChangelogStatus(session pmgo.SessionManager) (*proto.ShardingCha
Items: &qresults,
}, 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,
Auth: "disabled",
SSL: "disabled",
BindIP: "",
Port: 0,
WarningMsgs: nil,
},
// 2
&security{
@@ -133,6 +136,8 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2,
Auth: "enabled",
SSL: "disabled",
BindIP: "", Port: 0,
WarningMsgs: nil,
},
// 3
&security{
@@ -140,6 +145,9 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2,
Auth: "enabled",
SSL: "disabled",
BindIP: "",
Port: 0,
WarningMsgs: nil,
},
// 4
&security{
@@ -147,6 +155,9 @@ func TestSecurityOpts(t *testing.T) {
Roles: 2,
Auth: "disabled",
SSL: "super secure",
BindIP: "",
Port: 0,
WarningMsgs: nil,
},
}
@@ -171,13 +182,13 @@ func TestSecurityOpts(t *testing.T) {
database.EXPECT().C("system.roles").Return(rolesCol)
rolesCol.EXPECT().Count().Return(2, nil)
got, err := GetSecuritySettings(session)
got, err := GetSecuritySettings(session, "3.2")
if err != nil {
t.Errorf("cannot get sec settings: %v", err)
}
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",
}}
database := pmgomock.NewMockDatabaseManager(ctrl)
ss := proto.ServerStatus{}
test.LoadJson("test/sample/serverstatus.json", &ss)
dialer.EXPECT().DialWithInfo(gomock.Any()).Return(session, nil)
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()
di := &mgo.DialInfo{Addrs: []string{"localhost"}}
@@ -335,7 +366,7 @@ func TestGetReplicasetMembers(t *testing.T) {
t.Errorf("getReplicasetMembers: %v", err)
}
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 {
ss.Opcounters.Command += increment
ss.Opcounters.Delete += increment

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
package templates
const Replicas = `
# Instances ####################################################################################
ID Host Type ReplSet
# Instances ##############################################################################################
ID Host Type ReplSet Engine
{{- if . -}}
{{- 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}}
{{else}}
No replica sets found
no replica sets found
{{end}}
`

View File

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

View File

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