mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-11 13:40:07 +00:00
Added security checks & server storage engine
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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"`
|
||||
|
@@ -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?")
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package templates
|
||||
|
||||
const Clusterwide = `
|
||||
# Cluster wide #################################################################################
|
||||
# Cluster wide ###########################################################################################
|
||||
Databases: {{.TotalDBsCount}}
|
||||
Collections: {{.TotalCollectionsCount}}
|
||||
Sharded Collections: {{.ShardedColsCount}}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package templates
|
||||
|
||||
const HostInfo = `# This host
|
||||
# Mongo Executable #############################################################################
|
||||
# Mongo Executable #######################################################################################
|
||||
Path to executable | {{.ProcPath}}
|
||||
# Report On {{.ThisHostID}} ########################################
|
||||
User | {{.ProcUserName}}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package templates
|
||||
|
||||
const Oplog = `
|
||||
# Oplog ########################################################################################
|
||||
# Oplog ##################################################################################################
|
||||
Oplog Size {{.Size}} Mb
|
||||
Oplog Used {{.UsedMB}} Mb
|
||||
Oplog Length {{.Running}}
|
||||
|
@@ -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}}
|
||||
`
|
||||
|
@@ -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}}
|
||||
|
@@ -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 }}
|
||||
`
|
||||
|
Reference in New Issue
Block a user