Skip to content

Commit

Permalink
Add TLS for internal gRPC client.
Browse files Browse the repository at this point in the history
  • Loading branch information
scudette committed Jan 24, 2019
1 parent f21766b commit bc20d35
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 12 deletions.
43 changes: 41 additions & 2 deletions bindings/python/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,47 @@ this is all that is needed to automate everything about the server.
Server side VQL is all powerful! It allows callers to do everything
with the server without any limitation. Callers can collect
artifacts on arbitrary clients in your deployment. You must protect
access to the API. By default the API is served from a unix domain
socket and not over TCP.
access to the API as explained below.

Security
--------

By default the API is bound to the loopback address 127.0.0.1. You can
change this by setting `API.bind_address` to 0.0.0.0 in the config
file. Regardless, the API is protected with TLS and clients must be
authenticated using certificates.

Velociraptor uses mutual authentication - the client verifies that the
server's certificate is signed by the CA and the server verifies that
the client presents certificate signed by the CA. At the core of this
scheme is the security of the Velociraptor CA (i.e. the CA.private_key
field in the server.config.yaml file).

When you first create the Velociraptor configuration (using
`velociraptor config generate`) the CA private_key is also included in
the config file. For extra secure deployments you should keep that
entire config file offline, and reduct that field from the running
server. This way you will only be able to sign offline, even if the
server were compromised.

Before you may connect to the API you will need a "api_client"
configuration file. This file is a simple YAML file which contains the
relevant keys and certificates to authenticate to the API
endpoint. You can prepare one from a server config containing the ca:

.. code-block::

$ velociraptor --config server.config.yaml \
config api_client --name fred > freds_api_client.yaml

This command generates the relevant certificates to allow the API
client to connect. The name "Fred" is the common name of the
certificate (this name will appear in server Auditing logs). Typically
the API clients are automated processes and you can distinguish
between different automated processes using the common name.

You may now use this yaml file to connect to the API endpoint - see
the client_example.py for an example of a program which runs a query.


Example queries
Expand Down
45 changes: 37 additions & 8 deletions bindings/python/client_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,32 @@
This example demonstrates how to connect to the Velociraptor server
and issue a server side VQL query.
In this example we issue an event query which streams results slowly
In this example we may issue event queries which stream results slowly
to the api client. This demonstrates how to build reactive post
processing scripts as required.
Some example queries to try:
Get basic information about the server:
SELECT * from info()
Get server load as a data stream (This will never terminate and return
data points every 10 seconds):
SELECT * from Artifact.Generic.Client.Stats()
Get an event for every execution of psexecsvc on any deployed
machine. The event can be handled in this python loop as required. The
python script will block until psexec is detected.
SELECT * from watch_monitoring(
artifact='Windows.Events.ProcessCreation')
WHERE Name =~ '(?i)psexesvc'
"""
import argparse
import json
Expand Down Expand Up @@ -41,27 +64,33 @@ def run(config, query):
max_wait=1,
Query=[api_pb2.VQLRequest(
Name="Test",
VQL="select * from info()",
VQL=query,
)])

# This will block as responses are streamed from the
# server. If the query is an event query we will block here
# server. If the query is an event query we will run this loop
# forever.
for response in stub.Query(request):

# Each response represents a set of rows. The columns are
# listed in their own field to ensure column order is
# preserved.
# Each response represents a list of rows. The columns are
# provided in their own field as an array, to ensure
# column order is preserved if required. If you dont care
# about column order just ignore the Columns field.
print(response.Columns)

# The actual payload is a list of dicts. Each dict has
# column names as keys and arbitrary values.
# column names as keys and arbitrary (possibly nested)
# values.
package = json.loads(response.Response)
print (package)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Example Velociraptor client.')
parser = argparse.ArgumentParser(
description="Sample Velociraptor query client.",
epilog='Example: client_example.py api_client.yaml '
'" SELECT * from Artifact.Generic.Client.Stats() "')

parser.add_argument('config', type=str,
help='Path to the api_client config. You can generate such '
'a file with "velociraptor config api_client"')
Expand Down
3 changes: 2 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ func GetDefaultConfig() *api_proto.Config {
API: &api_proto.APIConfig{
// Bind port for gRPC endpoint - this should not
// normally be exposed.
BindAddress: "/tmp/velociraptor.sock",
BindAddress: "127.0.0.1",
BindPort: 8001,
BindScheme: "tcp",
},
GUI: &api_proto.GUIConfig{
Expand Down
42 changes: 41 additions & 1 deletion grpc_client/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,56 @@
package grpc_client

import (
"crypto/tls"
"crypto/x509"
"fmt"
"sync"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
api_proto "www.velocidex.com/golang/velociraptor/api/proto"
"www.velocidex.com/golang/velociraptor/constants"
)

var (
// Cache the creds for internal gRPC connections.
mu sync.Mutex
creds credentials.TransportCredentials
)

func getCreds(config_obj *api_proto.Config) credentials.TransportCredentials {
mu.Lock()
defer mu.Unlock()

if creds == nil {
// We use the Frontend's certificate because this connection
// represents an internal connection.
cert, err := tls.X509KeyPair(
[]byte(config_obj.Frontend.Certificate),
[]byte(config_obj.Frontend.PrivateKey))
if err != nil {
return nil
}

// The server cert must be signed by our CA.
CA_Pool := x509.NewCertPool()
CA_Pool.AppendCertsFromPEM([]byte(config_obj.Client.CaCertificate))

creds = credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: CA_Pool,
ServerName: constants.FRONTEND_NAME,
})
}

return creds
}

// TODO- Return a cluster dialer.
func GetChannel(config_obj *api_proto.Config) *grpc.ClientConn {
address := GetAPIConnectionString(config_obj)
con, err := grpc.Dial(address, grpc.WithInsecure())
con, err := grpc.Dial(address, grpc.WithTransportCredentials(
getCreds(config_obj)))
if err != nil {
panic(fmt.Sprintf("Unable to connect to self: %v: %v", address, err))
}
Expand Down

0 comments on commit bc20d35

Please sign in to comment.