added option to secure channel communications

Allow to cypher channel communications with certificates.

There are 3 authentication types: simple, tls-simple and tls-mutual.

 - 'simple' wont't cypher communications.
 - 'tls-simple' uses a server key and certificate for the server, and a
   common CA certificate or the server certificate to authenticate all
   nodes.
 - 'tls-mutual' uses a server key and certificate for the server, and a
   client key and certificate per node.

There are 2 options to verify how gRPC validates credentials:
 - SkipVerify: https://pkg.go.dev/crypto/tls#Config
 - ClientAuthType: https://pkg.go.dev/crypto/tls#ClientAuthType

Example configuration:
    "Server": {
        "Address": "127.0.0.1:12345",
        "Authentication": {
            "Type": "tls-simple",
            "TLSOptions": {
                "CACert": "/etc/opensnitchd/auth/ca-cert.pem",
                "ServerCert": "/etc/opensnitchd/auth/server-cert.pem",
                "ClientCert": "/etc/opensnitchd/auth/client-cert.pem",
                "ClientKey": "/etc/opensnitchd/auth/client-key.pem",
                "SkipVerify": false,
                "ClientAuthType": "req-and-verify-cert"
            }
        }
    }

More info: https://github.com/evilsocket/opensnitch/wiki/Nodes
This commit is contained in:
Gustavo Iñiguez Goia 2023-06-23 16:51:36 +02:00
parent 0d6b9101b0
commit 12b4cf3104
Failed to generate hash of commit
5 changed files with 282 additions and 118 deletions

102
daemon/ui/auth/auth.go Normal file
View file

@ -0,0 +1,102 @@
package auth
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/ui/config"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// client auth types:
// https://pkg.go.dev/crypto/tls#ClientAuthType
var (
clientAuthType = map[string]tls.ClientAuthType{
"no-client-cert": tls.NoClientCert,
"req-cert": tls.RequestClientCert,
"req-any-cert": tls.RequireAnyClientCert,
"verify-cert": tls.VerifyClientCertIfGiven,
"req-and-verify-cert": tls.RequireAndVerifyClientCert,
}
)
const (
// AuthSimple will use WithInsecure()
AuthSimple = "simple"
// AuthTLSSimple will use a common CA certificate, shared between the server
// and all the clients.
AuthTLSSimple = "tls-simple"
// AuthTLSMutual will use a CA certificate and a client cert and key
// to authenticate each client.
AuthTLSMutual = "tls-mutual"
)
// New returns the configuration that the UI will use
// to connect with the server.
func New(config *config.Config) (grpc.DialOption, error) {
config.RLock()
credsType := config.Server.Authentication.Type
tlsOpts := config.Server.Authentication.TLSOptions
config.RUnlock()
if credsType == "" || credsType == AuthSimple {
log.Debug("UI auth: simple")
return grpc.WithInsecure(), nil
}
certPool := x509.NewCertPool()
// use CA certificate to authenticate clients if supplied
if tlsOpts.CACert != "" {
if caPem, err := ioutil.ReadFile(tlsOpts.CACert); err != nil {
log.Warning("reading UI auth CA certificate (%s): %s", credsType, err)
} else {
if !certPool.AppendCertsFromPEM(caPem) {
log.Warning("adding UI auth CA certificate (%s): %s", credsType, err)
}
}
}
// use server certificate to authenticate clients if supplied
if tlsOpts.ServerCert != "" {
if serverPem, err := ioutil.ReadFile(tlsOpts.ServerCert); err != nil {
log.Warning("reading auth server cert: %s", err)
} else {
if !certPool.AppendCertsFromPEM(serverPem) {
log.Warning("adding UI auth server cert (%s): %s", credsType, err)
}
}
}
// set config of tls credential
// https://pkg.go.dev/crypto/tls#Config
tlsCfg := &tls.Config{
InsecureSkipVerify: tlsOpts.SkipVerify,
RootCAs: certPool,
}
// https://pkg.go.dev/google.golang.org/grpc/credentials#SecurityLevel
if credsType == AuthTLSMutual {
tlsCfg.ClientAuth = clientAuthType[tlsOpts.ClientAuthType]
clientCert, err := tls.LoadX509KeyPair(
tlsOpts.ClientCert,
tlsOpts.ClientKey,
)
if err != nil {
return nil, err
}
log.Debug(" using client cert: %s", tlsOpts.ClientCert)
log.Debug(" using client key: %s", tlsOpts.ClientKey)
tlsCfg.Certificates = []tls.Certificate{clientCert}
}
return grpc.WithTransportCredentials(
credentials.NewTLS(tlsCfg),
), nil
}

View file

@ -12,6 +12,8 @@ import (
"github.com/evilsocket/opensnitch/daemon/log/loggers"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/statistics"
"github.com/evilsocket/opensnitch/daemon/ui/auth"
"github.com/evilsocket/opensnitch/daemon/ui/config"
"github.com/evilsocket/opensnitch/daemon/ui/protocol"
"github.com/fsnotify/fsnotify"
@ -28,32 +30,11 @@ var (
// While the GUI is connected, deny by default everything until the user takes an action.
clientConnectedRule = rule.Create("ui.client.connected", "", true, false, false, rule.Deny, rule.Once, dummyOperator)
clientErrorRule = rule.Create("ui.client.error", "", true, false, false, rule.Allow, rule.Once, dummyOperator)
config Config
clientConfig config.Config
maxQueuedAlerts = 1024
)
type serverConfig struct {
Address string `json:"Address"`
LogFile string `json:"LogFile"`
Loggers []loggers.LoggerConfig `json:"Loggers"`
}
// Config holds the values loaded from configFile
type Config struct {
sync.RWMutex
Server serverConfig `json:"Server"`
DefaultAction string `json:"DefaultAction"`
DefaultDuration string `json:"DefaultDuration"`
InterceptUnknown bool `json:"InterceptUnknown"`
ProcMonitorMethod string `json:"ProcMonitorMethod"`
LogLevel *uint32 `json:"LogLevel"`
LogUTC bool `json:"LogUTC"`
LogMicro bool `json:"LogMicro"`
Firewall string `json:"Firewall"`
Stats statistics.StatsConfig `json:"Stats"`
}
// Client holds the connection information of a client.
type Client struct {
sync.RWMutex
@ -98,8 +79,8 @@ func NewClient(socketPath string, stats *statistics.Statistics, rules *rule.Load
if socketPath != "" {
c.setSocketPath(c.getSocketPath(socketPath))
}
loggers.Load(config.Server.Loggers, config.Stats.Workers)
stats.SetLimits(config.Stats)
loggers.Load(clientConfig.Server.Loggers, clientConfig.Stats.Workers)
stats.SetLimits(clientConfig.Stats)
stats.SetLoggers(loggers)
return c
@ -118,26 +99,26 @@ func (c *Client) Close() {
// ProcMonitorMethod returns the monitor method configured.
// If it's not present in the config file, it'll return an empty string.
func (c *Client) ProcMonitorMethod() string {
config.RLock()
defer config.RUnlock()
return config.ProcMonitorMethod
clientConfig.RLock()
defer clientConfig.RUnlock()
return clientConfig.ProcMonitorMethod
}
// InterceptUnknown returns
func (c *Client) InterceptUnknown() bool {
config.RLock()
defer config.RUnlock()
return config.InterceptUnknown
clientConfig.RLock()
defer clientConfig.RUnlock()
return clientConfig.InterceptUnknown
}
// GetFirewallType returns the firewall to use
func (c *Client) GetFirewallType() string {
config.RLock()
defer config.RUnlock()
if config.Firewall == "" {
clientConfig.RLock()
defer clientConfig.RUnlock()
if clientConfig.Firewall == "" {
return iptables.Name
}
return config.Firewall
return clientConfig.Firewall
}
// DefaultAction returns the default configured action for
@ -280,7 +261,11 @@ func (c *Client) openSocket() (err error) {
PermitWithoutStream: true,
}
c.con, err = grpc.Dial(c.socketPath, grpc.WithInsecure(), grpc.WithKeepaliveParams(kacp))
dialOption, err := auth.New(&clientConfig)
if err != nil {
return fmt.Errorf("Invalid client auth options: %s", err)
}
c.con, err = grpc.Dial(c.socketPath, dialOption, grpc.WithKeepaliveParams(kacp))
}
return err

View file

@ -0,0 +1,54 @@
package config
import (
"sync"
"github.com/evilsocket/opensnitch/daemon/log/loggers"
"github.com/evilsocket/opensnitch/daemon/statistics"
)
type serverTLSOptions struct {
CACert string `json:"CACert"`
ServerCert string `json:"ServerCert"`
ServerKey string `json:"ServerKey"`
ClientCert string `json:"ClientCert"`
ClientKey string `json:"ClientKey"`
// https://pkg.go.dev/crypto/tls#Config
SkipVerify bool `json:"SkipVerify"`
//https://pkg.go.dev/crypto/tls#ClientAuthType
ClientAuthType string `json:"ClientAuthType"`
// https://pkg.go.dev/crypto/tls#Conn.VerifyHostname
//VerifyHostname bool
// https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection
// VerifyConnection bool
// VerifyPeerCertificate bool
}
type serverAuth struct {
// token?, google?, simple-tls, mutual-tls
Type string `json:"Type"`
TLSOptions serverTLSOptions `json:"TLSOptions"`
}
type serverConfig struct {
Address string `json:"Address"`
Authentication serverAuth `json:"Authentication"`
LogFile string `json:"LogFile"`
Loggers []loggers.LoggerConfig `json:"Loggers"`
}
// Config holds the values loaded from configFile
type Config struct {
sync.RWMutex
Server serverConfig `json:"Server"`
DefaultAction string `json:"DefaultAction"`
DefaultDuration string `json:"DefaultDuration"`
InterceptUnknown bool `json:"InterceptUnknown"`
ProcMonitorMethod string `json:"ProcMonitorMethod"`
LogLevel *uint32 `json:"LogLevel"`
LogUTC bool `json:"LogUTC"`
LogMicro bool `json:"LogMicro"`
Firewall string `json:"Firewall"`
Stats statistics.StatsConfig `json:"Stats"`
}

View file

@ -10,6 +10,7 @@ import (
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/evilsocket/opensnitch/daemon/procmon/monitor"
"github.com/evilsocket/opensnitch/daemon/rule"
"github.com/evilsocket/opensnitch/daemon/ui/config"
)
func (c *Client) getSocketPath(socketPath string) string {
@ -33,13 +34,13 @@ func (c *Client) setSocketPath(socketPath string) {
}
func (c *Client) isProcMonitorEqual(newMonitorMethod string) bool {
config.RLock()
defer config.RUnlock()
clientConfig.RLock()
defer clientConfig.RUnlock()
return newMonitorMethod == config.ProcMonitorMethod
return newMonitorMethod == clientConfig.ProcMonitorMethod
}
func (c *Client) parseConf(rawConfig string) (conf Config, err error) {
func (c *Client) parseConf(rawConfig string) (conf config.Config, err error) {
err = json.Unmarshal([]byte(rawConfig), &conf)
return conf, err
}
@ -68,45 +69,45 @@ func (c *Client) loadDiskConfiguration(reload bool) {
}
func (c *Client) loadConfiguration(rawConfig []byte) bool {
config.Lock()
defer config.Unlock()
clientConfig.Lock()
defer clientConfig.Unlock()
if err := json.Unmarshal(rawConfig, &config); err != nil {
if err := json.Unmarshal(rawConfig, &clientConfig); err != nil {
msg := fmt.Sprintf("Error parsing configuration %s: %s", configFile, err)
log.Error(msg)
c.SendWarningAlert(msg)
return false
}
// firstly load config level, to detect further errors if any
if config.LogLevel != nil {
log.SetLogLevel(int(*config.LogLevel))
if clientConfig.LogLevel != nil {
log.SetLogLevel(int(*clientConfig.LogLevel))
}
log.SetLogUTC(config.LogUTC)
log.SetLogMicro(config.LogMicro)
if config.Server.LogFile != "" {
log.SetLogUTC(clientConfig.LogUTC)
log.SetLogMicro(clientConfig.LogMicro)
if clientConfig.Server.LogFile != "" {
log.Close()
log.OpenFile(config.Server.LogFile)
log.OpenFile(clientConfig.Server.LogFile)
}
if config.Server.Address != "" {
tempSocketPath := c.getSocketPath(config.Server.Address)
if clientConfig.Server.Address != "" {
tempSocketPath := c.getSocketPath(clientConfig.Server.Address)
if tempSocketPath != c.socketPath {
// disconnect, and let the connection poller reconnect to the new address
c.disconnect()
}
c.setSocketPath(tempSocketPath)
}
if config.DefaultAction != "" {
clientDisconnectedRule.Action = rule.Action(config.DefaultAction)
clientErrorRule.Action = rule.Action(config.DefaultAction)
if clientConfig.DefaultAction != "" {
clientDisconnectedRule.Action = rule.Action(clientConfig.DefaultAction)
clientErrorRule.Action = rule.Action(clientConfig.DefaultAction)
}
if config.DefaultDuration != "" {
clientDisconnectedRule.Duration = rule.Duration(config.DefaultDuration)
clientErrorRule.Duration = rule.Duration(config.DefaultDuration)
if clientConfig.DefaultDuration != "" {
clientDisconnectedRule.Duration = rule.Duration(clientConfig.DefaultDuration)
clientErrorRule.Duration = rule.Duration(clientConfig.DefaultDuration)
}
if config.ProcMonitorMethod != "" {
if err := monitor.ReconfigureMonitorMethod(config.ProcMonitorMethod); err != nil {
msg := fmt.Sprintf("Unable to set new process monitor (%s) method from disk: %v", config.ProcMonitorMethod, err)
if clientConfig.ProcMonitorMethod != "" {
if err := monitor.ReconfigureMonitorMethod(clientConfig.ProcMonitorMethod); err != nil {
msg := fmt.Sprintf("Unable to set new process monitor (%s) method from disk: %v", clientConfig.ProcMonitorMethod, err)
log.Warning(msg)
c.SendWarningAlert(msg)
}

View file

@ -42,6 +42,7 @@ from opensnitch.utils import Themes, Utils, Versions, Message
from opensnitch.utils.xdg import xdg_runtime_dir
import opensnitch.ui_pb2
from opensnitch.ui_pb2_grpc import add_UIServicer_to_server
from opensnitch import auth
def on_exit():
server.stop(0)
@ -69,6 +70,9 @@ Examples:
* Use unix:///run/1000/YOUR_USER/opensnitch/osui.sock for better privacy.
- Listening on port 50051, all interfaces: opensnitch-ui --socket "[::]:50051"
''', metavar="FILE")
parser.add_argument("--socket-auth", dest="socket_auth", help="Auth type: simple, tls-simple, tls-mutual")
parser.add_argument("--tls-cert", dest="tls_cert", help="path to the server cert")
parser.add_argument("--tls-key", dest="tls_key", help="path to the server key")
parser.add_argument("--max-clients", dest="serverWorkers", default=10, help="Max number of allowed clients (incoming connections).")
parser.add_argument("--debug", dest="debug", action="store_true", help="Enable debug logs")
parser.add_argument("--debug-grpc", dest="debug_grpc", action="store_true", help="Enable gRPC debug logs")
@ -97,72 +101,90 @@ Examples:
app = QtWidgets.QApplication(sys.argv)
lockfile = QtCore.QLockFile(os.path.join(xdg_runtime_dir, 'osui.lock'))
if lockfile.tryLock(100):
if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):
app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
thm = Themes.instance()
thm.load_theme(app)
Utils.create_socket_dirs()
cfg = Config.get()
if args.socket == None:
# default
args.socket = "unix:///tmp/osui.sock"
addr = cfg.getSettings(Config.DEFAULT_SERVER_ADDR)
if addr != None and addr != "":
if addr.startswith("unix://"):
if not os.path.exists(os.path.dirname(addr[7:])):
print("WARNING: unix socket path does not exist, using unix:///tmp/osui.sock, ", addr)
else:
args.socket = addr
else:
args.socket = addr
print("Using server address:", args.socket)
maxmsglen = cfg.getMaxMsgLength()
service = UIService(app, on_exit)
# @doc: https://grpc.github.io/grpc/python/grpc.html#server-object
server = grpc.server(futures.ThreadPoolExecutor(),
options=(
# https://github.com/grpc/grpc/blob/master/doc/keepalive.md
# https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
# send keepalive ping every 5 second, default is 2 hours)
('grpc.keepalive_time_ms', 5000),
# after 5s of inactivity, wait 20s and close the connection if
# there's no response.
('grpc.keepalive_timeout_ms', 20000),
('grpc.keepalive_permit_without_calls', True),
('grpc.max_send_message_length', maxmsglen),
('grpc.max_receive_message_length', maxmsglen),
))
add_UIServicer_to_server(service, server)
if args.socket.startswith("unix://"):
socket = args.socket[7:]
socket = os.path.abspath(socket)
server.add_insecure_port("unix:%s" % socket)
else:
server.add_insecure_port(args.socket)
# https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt
signal.signal(signal.SIGINT, signal.SIG_DFL)
# print "OpenSnitch UI service running on %s ..." % socket
server.start()
app.exec_()
else:
if not lockfile.tryLock(100):
Message.ok(
QC.translate("stats", "Error"),
QC.translate("stats", "OpenSnitch UI is already running!"),
QtWidgets.QMessageBox.Warning
)
raise Exception("GUI already running, exiting.")
if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):
app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
thm = Themes.instance()
thm.load_theme(app)
Utils.create_socket_dirs()
cfg = Config.get()
if args.socket == None:
# default
args.socket = "unix:///tmp/osui.sock"
addr = cfg.getSettings(Config.DEFAULT_SERVER_ADDR)
if addr != None and addr != "":
if addr.startswith("unix://"):
if not os.path.exists(os.path.dirname(addr[7:])):
print("WARNING: unix socket path does not exist, using unix:///tmp/osui.sock, ", addr)
else:
args.socket = addr
else:
args.socket = addr
print("Using server address:", args.socket)
maxmsglen = cfg.getMaxMsgLength()
service = UIService(app, on_exit)
# @doc: https://grpc.github.io/grpc/python/grpc.html#server-object
server = grpc.server(futures.ThreadPoolExecutor(),
options=(
# https://github.com/grpc/grpc/blob/master/doc/keepalive.md
# https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
# send keepalive ping every 5 second, default is 2 hours)
('grpc.keepalive_time_ms', 5000),
# after 5s of inactivity, wait 20s and close the connection if
# there's no response.
('grpc.keepalive_timeout_ms', 20000),
('grpc.keepalive_permit_without_calls', True),
('grpc.max_send_message_length', maxmsglen),
('grpc.max_receive_message_length', maxmsglen),
))
add_UIServicer_to_server(service, server)
if args.socket.startswith("unix://"):
socket = args.socket[7:]
socket = os.path.abspath(socket)
server.add_insecure_port("unix:%s" % socket)
#server.add_secure_port("unix:%s" % socket, tls_creds)
else:
auth_type = auth.Simple
if args.socket_auth != None:
auth_type = args.socket_auth
elif cfg.getSettings(Config.AUTH_TYPE) != None:
auth_type = cfg.getSettings(Config.AUTH_TYPE)
if auth_type == auth.Simple or auth_type == "":
server.add_insecure_port(args.socket)
else:
auth_cert = cfg.getSettings(Config.AUTH_CERT)
auth_certkey = cfg.getSettings(Config.AUTH_CERTKEY)
tls_creds = auth.get_tls_credentials(args.tls_cert, args.tls_key)
if tls_creds == None:
raise Exception("Invalid TLS credentials. Review the server key and cert files.")
server.add_secure_port(args.socket, tls_creds)
# https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt
signal.signal(signal.SIGINT, signal.SIG_DFL)
# print "OpenSnitch UI service running on %s ..." % socket
server.start()
app.exec_()
except KeyboardInterrupt:
on_exit()
except Exception as e:
print(e)
finally:
lockfile.unlock()