mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Orbit remote management for flags (#7246)
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
This commit is contained in:
parent
db7d1c5bf5
commit
7d4e2e2b4b
22 changed files with 843 additions and 11 deletions
1
changes/feature-6581-remote-flags-manaement-with-orbit
Normal file
1
changes/feature-6581-remote-flags-manaement-with-orbit
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added new endpoints for orbit to enroll and get startup flags
|
||||
1
orbit/changes/6581-remote-flags-management
Normal file
1
orbit/changes/6581-remote-flags-management
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Orbit now enrolls with fleet using it's own orbit_node_key, and periodically checks for new flags (using the new API), and applies them to osquery
|
||||
|
|
@ -2,13 +2,17 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
|
@ -26,7 +30,6 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/pkg/certificate"
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/pkg/secure"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oklog/run"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -199,7 +202,7 @@ func main() {
|
|||
return errors.New("enroll-secret and enroll-secret-path may not be specified together")
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(c.String("enroll-secret-path"))
|
||||
b, err := os.ReadFile(c.String("enroll-secret-path"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read enroll secret file: %w", err)
|
||||
}
|
||||
|
|
@ -350,6 +353,13 @@ func main() {
|
|||
return fmt.Errorf("cleanup old files: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Msg("running single query (SELECT uuid FROM system_info)")
|
||||
uuidStr, err := getUUID(osquerydPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get UUID: %w", err)
|
||||
}
|
||||
log.Debug().Msg("UUID is " + uuidStr)
|
||||
|
||||
var options []osquery.Option
|
||||
options = append(options, osquery.WithDataPath(c.String("root-dir")))
|
||||
options = append(options, osquery.WithLogPath(filepath.Join(c.String("root-dir"), "osquery_log")))
|
||||
|
|
@ -373,6 +383,7 @@ func main() {
|
|||
)
|
||||
}
|
||||
|
||||
var certPath string
|
||||
if fleetURL != "https://" && c.Bool("insecure") {
|
||||
proxy, err := insecure.NewTLSProxy(fleetURL)
|
||||
if err != nil {
|
||||
|
|
@ -401,10 +412,10 @@ func main() {
|
|||
return fmt.Errorf("there was a problem creating the proxy directory: %w", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(proxyDirectory, "fleet.crt")
|
||||
certPath = filepath.Join(proxyDirectory, "fleet.crt")
|
||||
|
||||
// Write cert that proxy uses
|
||||
err = ioutil.WriteFile(certPath, []byte(insecure.ServerCert), os.ModePerm)
|
||||
err = os.WriteFile(certPath, []byte(insecure.ServerCert), os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write server cert: %w", err)
|
||||
}
|
||||
|
|
@ -443,7 +454,7 @@ func main() {
|
|||
osquery.WithFlags(osquery.FleetFlags(parsedURL)),
|
||||
)
|
||||
|
||||
if certPath := c.String("fleet-certificate"); certPath != "" {
|
||||
if certPath = c.String("fleet-certificate"); certPath != "" {
|
||||
// Check and log if there are any errors with TLS connection.
|
||||
pool, err := certificate.LoadPEM(certPath)
|
||||
if err != nil {
|
||||
|
|
@ -457,7 +468,7 @@ func main() {
|
|||
osquery.WithFlags([]string{"--tls_server_certs", certPath}),
|
||||
)
|
||||
} else {
|
||||
certPath := filepath.Join(c.String("root-dir"), "certs.pem")
|
||||
certPath = filepath.Join(c.String("root-dir"), "certs.pem")
|
||||
if exists, err := file.Exists(certPath); err == nil && exists {
|
||||
_, err = certificate.LoadPEM(certPath)
|
||||
if err != nil {
|
||||
|
|
@ -468,8 +479,35 @@ func main() {
|
|||
log.Info().Msg("No cert chain available. Relying on system store.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
orbitClient, err := service.NewOrbitClient(fleetURL, c.String("fleet-certificate"), c.Bool("insecure"), enrollSecret, uuidStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error new orbit client: %w", err)
|
||||
}
|
||||
orbitNodeKey, err := getOrbitNodeKeyOrEnroll(orbitClient, c.String("root-dir"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error enroll: %w", err)
|
||||
}
|
||||
|
||||
const orbitFlagsUpdateInterval = 30 * time.Second
|
||||
flagRunner, err := update.NewFlagRunner(orbitClient, update.FlagUpdateOptions{
|
||||
CheckInterval: orbitFlagsUpdateInterval,
|
||||
RootDir: c.String("root-dir"),
|
||||
OrbitNodeKey: orbitNodeKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// do the initial flags update
|
||||
_, err = flagRunner.DoFlagsUpdate()
|
||||
if err != nil {
|
||||
// just log, OK to continue, since we will retry
|
||||
log.Info().Err(err).Msg("Initial flags update failed")
|
||||
}
|
||||
g.Add(flagRunner.Execute, flagRunner.Interrupt)
|
||||
|
||||
// --force is sometimes needed when an older osquery process has not
|
||||
// exited properly
|
||||
options = append(options, osquery.WithFlags([]string{"--force"}))
|
||||
|
|
@ -651,6 +689,86 @@ func (d *desktopRunner) interrupt(err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// shell out to osquery (on Linux and macOS) or to wmic (on Windows), and get the system uuid
|
||||
func getUUID(osqueryPath string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
args := []string{"/C", "wmic csproduct get UUID"}
|
||||
out, err := exec.Command("cmd", args...).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uuidOutputStr := string(out)
|
||||
if len(uuidOutputStr) == 0 {
|
||||
return "", errors.New("get UUID: output from wmi is empty")
|
||||
}
|
||||
outputByLines := strings.Split(strings.TrimRight(uuidOutputStr, "\n"), "\n")
|
||||
if len(outputByLines) < 2 {
|
||||
return "", errors.New("get UUID: unexpected output")
|
||||
}
|
||||
return strings.TrimSpace(outputByLines[1]), nil
|
||||
}
|
||||
type UuidOutput struct {
|
||||
UuidString string `json:"uuid"`
|
||||
}
|
||||
|
||||
args := []string{"-S", "--json", "select uuid from system_info"}
|
||||
out, err := exec.Command(osqueryPath, args...).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var uuids []UuidOutput
|
||||
err = json.Unmarshal(out, &uuids)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(uuids) != 1 {
|
||||
return "", fmt.Errorf("invalid number of rows from system_info query: %d", len(uuids))
|
||||
}
|
||||
return uuids[0].UuidString, nil
|
||||
|
||||
}
|
||||
|
||||
// getOrbitNodeKeyOrEnroll attempts to read the orbit node key if the file exists on disk
|
||||
// otherwise it enrolls the host with Fleet and saves the node key to disk
|
||||
func getOrbitNodeKeyOrEnroll(orbitClient *service.OrbitClient, rootDir string) (string, error) {
|
||||
nodeKeyFilePath := filepath.Join(rootDir, "secret-orbit-node-key.txt")
|
||||
orbitNodeKey, err := ioutil.ReadFile(nodeKeyFilePath)
|
||||
switch {
|
||||
case err == nil:
|
||||
return string(orbitNodeKey), nil
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
// OK
|
||||
default:
|
||||
return "", fmt.Errorf("read orbit node key file: %w", err)
|
||||
}
|
||||
const (
|
||||
orbitEnrollMaxRetries = 10
|
||||
orbitEnrollRetrySleep = 5 * time.Second
|
||||
)
|
||||
for retries := 0; retries < orbitEnrollMaxRetries; retries++ {
|
||||
orbitNodeKey, err := enrollAndWriteNodeKeyFile(orbitClient, nodeKeyFilePath)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("enroll failed, retrying")
|
||||
time.Sleep(orbitEnrollRetrySleep)
|
||||
continue
|
||||
}
|
||||
return orbitNodeKey, nil
|
||||
}
|
||||
return "", fmt.Errorf("orbit node key enroll failed, attempts=%d", orbitEnrollMaxRetries)
|
||||
}
|
||||
|
||||
func enrollAndWriteNodeKeyFile(orbitClient *service.OrbitClient, nodeKeyFilePath string) (string, error) {
|
||||
orbitNodeKey, err := orbitClient.DoEnroll()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("enroll request: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(nodeKeyFilePath, []byte(orbitNodeKey), constant.DefaultFileMode); err != nil {
|
||||
return "", fmt.Errorf("write orbit node key file: %w", err)
|
||||
}
|
||||
return orbitNodeKey, nil
|
||||
}
|
||||
|
||||
func loadOrGenerateToken(rootDir string) (string, error) {
|
||||
filePath := filepath.Join(rootDir, "identifier")
|
||||
id, err := ioutil.ReadFile(filePath)
|
||||
|
|
@ -662,7 +780,7 @@ func loadOrGenerateToken(rootDir string) (string, error) {
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("generate identifier: %w", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filePath, []byte(id.String()), constant.DefaultFileMode); err != nil {
|
||||
if err := os.WriteFile(filePath, []byte(id.String()), constant.DefaultFileMode); err != nil {
|
||||
return "", fmt.Errorf("write identifier file %q: %w", filePath, err)
|
||||
}
|
||||
return id.String(), nil
|
||||
|
|
|
|||
190
orbit/pkg/update/flag_runner.go
Normal file
190
orbit/pkg/update/flag_runner.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FlagRunner is a specialized runner to periodically check and update flags from Fleet
|
||||
// It is designed with Execute and Interrupt functions to be compatible with oklog/run
|
||||
//
|
||||
// It uses an OrbitClient, along with FlagUpdateOptions to connect to Fleet
|
||||
type FlagRunner struct {
|
||||
orbitClient *service.OrbitClient
|
||||
opt FlagUpdateOptions
|
||||
cancel chan struct{}
|
||||
}
|
||||
|
||||
// FlagUpdateOptions is options provided for the flag update runner
|
||||
type FlagUpdateOptions struct {
|
||||
// CheckInterval is the interval to check for updates
|
||||
CheckInterval time.Duration
|
||||
// RootDir is the root directory for orbit state
|
||||
RootDir string
|
||||
// OrbitNodeKey is the orbit node key for the enrolled host
|
||||
OrbitNodeKey string
|
||||
}
|
||||
|
||||
// NewFlagRunner creates a new runner with provided options
|
||||
// The runner must be started with Execute
|
||||
func NewFlagRunner(orbitClient *service.OrbitClient, opt FlagUpdateOptions) (*FlagRunner, error) {
|
||||
r := &FlagRunner{
|
||||
orbitClient: orbitClient,
|
||||
opt: opt,
|
||||
cancel: make(chan struct{}, 1),
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Execute starts the loop checking for updates
|
||||
func (r *FlagRunner) Execute() error {
|
||||
log.Debug().Msg("starting flag updater")
|
||||
|
||||
ticker := time.NewTicker(r.opt.CheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Run until cancel or returning an error
|
||||
for {
|
||||
select {
|
||||
case <-r.cancel:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("calling flags update")
|
||||
didUpdate, err := r.DoFlagsUpdate()
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("flags updates failed")
|
||||
}
|
||||
if didUpdate {
|
||||
log.Info().Msg("flags updated, exiting")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interrupt is the oklog/run interrupt method that stops orbit when interrupt is received
|
||||
func (r *FlagRunner) Interrupt(err error) {
|
||||
close(r.cancel)
|
||||
log.Debug().Err(err).Msg("interrupt for flags updater")
|
||||
}
|
||||
|
||||
// DoFlagsUpdate checks for update of flags from Fleet
|
||||
// It gets the flags from the Fleet server, and compares them to locally stored flagfile (if it exists)
|
||||
// If the flag comparison from disk and server are not equal, it writes the flags to disk, and returns true
|
||||
func (r *FlagRunner) DoFlagsUpdate() (bool, error) {
|
||||
flagFileExists := true
|
||||
|
||||
// first off try and read osquery.flags from disk
|
||||
osqueryFlagMapFromFile, err := readFlagFile(r.opt.RootDir)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, err
|
||||
}
|
||||
// flag file may not exist on disk on first "boot"
|
||||
flagFileExists = false
|
||||
}
|
||||
|
||||
// next GetConfig from Fleet API
|
||||
flagsJSON, err := r.orbitClient.GetConfig(r.opt.OrbitNodeKey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error getting flags from fleet %w", err)
|
||||
}
|
||||
if len(flagsJSON) == 0 {
|
||||
// command_line_flags not set in YAML, nothing to do
|
||||
return false, nil
|
||||
}
|
||||
|
||||
osqueryFlagMapFromFleet, err := getFlagsFromJSON(flagsJSON)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error parsing flags %w", err)
|
||||
}
|
||||
|
||||
// compare both flags, if they are equal, nothing to do
|
||||
if flagFileExists && reflect.DeepEqual(osqueryFlagMapFromFile, osqueryFlagMapFromFleet) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// flags are not equal, write the fleet flags to disk
|
||||
err = writeFlagFile(r.opt.RootDir, osqueryFlagMapFromFleet)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error writing flags to disk %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getFlagsFromJSON converts the json of the type below
|
||||
// {"number": 5, "string": "str", "boolean": true}
|
||||
// to a map[string]string
|
||||
// this map will get compared and written to the filesystem and passed to osquery
|
||||
// this only supports simple key:value pairs and not nested structures
|
||||
func getFlagsFromJSON(flags json.RawMessage) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
var data map[string]interface{}
|
||||
err := json.Unmarshal([]byte(flags), &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
result["--"+k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// writeFlagFile writes the contents of the data map as a osquery flagfile to disk
|
||||
// given a map[string]string, of the form: {"--foo":"bar","--value":"5"}
|
||||
// it writes the contents of key=value, one line per pair to the file
|
||||
// this only supports simple key:value pairs and not nested structures
|
||||
func writeFlagFile(rootDir string, data map[string]string) error {
|
||||
flagfile := filepath.Join(rootDir, "osquery.flags")
|
||||
var sb strings.Builder
|
||||
for k, v := range data {
|
||||
if k != "" && v != "" {
|
||||
sb.WriteString(k + "=" + v + "\n")
|
||||
} else if v == "" {
|
||||
sb.WriteString(k + "\n")
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(flagfile, []byte(sb.String()), constant.DefaultFileMode); err != nil {
|
||||
return fmt.Errorf("writing flagfile %s failed: %w", flagfile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readFlagFile reads and parses the osquery.flags file on disk
|
||||
// and returns a map[string]string, of the form:
|
||||
// {"--foo":"bar","--value":"5"}
|
||||
// this only supports simple key:value pairs and not nested structures
|
||||
func readFlagFile(rootDir string) (map[string]string, error) {
|
||||
flagfile := filepath.Join(rootDir, "osquery.flags")
|
||||
bytes, err := os.ReadFile(flagfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading flagfile %s failed: %w", flagfile, err)
|
||||
}
|
||||
result := make(map[string]string)
|
||||
lines := strings.Split(strings.TrimSpace(string(bytes)), "\n")
|
||||
for _, line := range lines {
|
||||
// skip line starting with "#" indicating that it's a comment
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
// split each line by "="
|
||||
str := strings.Split(strings.TrimSpace(line), "=")
|
||||
if len(str) == 2 {
|
||||
result[str[0]] = str[1]
|
||||
}
|
||||
if len(str) == 1 {
|
||||
result[str[0]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
59
orbit/pkg/update/flag_runner_test.go
Normal file
59
orbit/pkg/update/flag_runner_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var rawJSONFlags = json.RawMessage(`{"verbose":true, "num":5, "hello":"world"}`)
|
||||
|
||||
func TestGetFlagsFromJson(t *testing.T) {
|
||||
flagsJson, err := getFlagsFromJSON(rawJSONFlags)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, flagsJson)
|
||||
|
||||
value, ok := flagsJson["--verbose"]
|
||||
if !ok {
|
||||
t.Errorf(`key ""--verbose" expected but not found`)
|
||||
}
|
||||
if value != "true" {
|
||||
t.Errorf(`expected "true", got %s`, value)
|
||||
}
|
||||
|
||||
value, ok = flagsJson["--num"]
|
||||
if !ok {
|
||||
t.Errorf(`key "--num" expected but not found`)
|
||||
}
|
||||
if value != "5" {
|
||||
t.Errorf(`expected "5", got %s`, value)
|
||||
}
|
||||
|
||||
value, ok = flagsJson["--hello"]
|
||||
if !ok {
|
||||
t.Errorf(`key "--hello" expected but not found`)
|
||||
}
|
||||
if value != "world" {
|
||||
t.Errorf(`expected "world", got %s`, value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFlagFile(t *testing.T) {
|
||||
flags, err := getFlagsFromJSON(rawJSONFlags)
|
||||
require.NoError(t, err)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
err = writeFlagFile(tempDir, flags)
|
||||
require.NoError(t, err)
|
||||
|
||||
diskFlags, err := readFlagFile(tempDir)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, diskFlags)
|
||||
|
||||
if !reflect.DeepEqual(flags, diskFlags) {
|
||||
t.Errorf("expected flags to be equal: %v, %v", flags, diskFlags)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,10 @@ const (
|
|||
// which only allows limited access to the device's own host information.
|
||||
// This authentication mode does not support granular authorization.
|
||||
AuthnDeviceToken
|
||||
// AuthnOrbitToken is when authentication is done via the orbit host
|
||||
// authentication token. This authentication mode does not support granular
|
||||
// authorization.
|
||||
AuthnOrbitToken
|
||||
)
|
||||
|
||||
// AuthorizationContext contains the context information used for the
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
|
|||
h.team_id,
|
||||
h.policy_updated_at,
|
||||
h.public_ip,
|
||||
h.orbit_node_key,
|
||||
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
|
||||
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
|
||||
COALESCE(hst.seen_time, h.created_at) AS seen_time,
|
||||
|
|
@ -837,6 +838,60 @@ func shouldCleanTeamPolicies(currentTeamID, newTeamID *uint) bool {
|
|||
return *currentTeamID != *newTeamID
|
||||
}
|
||||
|
||||
func (ds *Datastore) EnrollOrbit(ctx context.Context, hardwareUUID string, orbitNodeKey string, teamID *uint) (*fleet.Host, error) {
|
||||
if orbitNodeKey == "" {
|
||||
return nil, ctxerr.New(ctx, "orbit node key is empty")
|
||||
}
|
||||
|
||||
if hardwareUUID == "" {
|
||||
return nil, ctxerr.New(ctx, "hardware uuid is empty")
|
||||
}
|
||||
|
||||
var host fleet.Host
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
err := sqlx.GetContext(ctx, tx, &host, `SELECT id FROM hosts WHERE uuid = ?`, hardwareUUID)
|
||||
switch {
|
||||
case err == nil:
|
||||
sqlUpdate := `UPDATE hosts SET orbit_node_key = ? WHERE uuid = ? `
|
||||
_, err := tx.ExecContext(ctx, sqlUpdate, orbitNodeKey, hardwareUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "orbit enroll error updating host details")
|
||||
}
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
zeroTime := time.Unix(0, 0).Add(24 * time.Hour)
|
||||
// Create new host record. We always create newly enrolled hosts with refetch_requested = true
|
||||
// so that the frontend automatically starts background checks to update the page whenever
|
||||
// the refetch is completed.
|
||||
// We are also initially setting node_key to be the same as orbit_node_key because node_key has a unique
|
||||
// constraint
|
||||
sqlInsert := `
|
||||
INSERT INTO hosts (
|
||||
last_enrolled_at,
|
||||
detail_updated_at,
|
||||
label_updated_at,
|
||||
policy_updated_at,
|
||||
osquery_host_id,
|
||||
node_key,
|
||||
team_id,
|
||||
refetch_requested,
|
||||
orbit_node_key
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)
|
||||
`
|
||||
_, err := tx.ExecContext(ctx, sqlInsert, zeroTime, zeroTime, zeroTime, zeroTime, hardwareUUID, orbitNodeKey, teamID, orbitNodeKey)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "orbit enroll error inserting host details")
|
||||
}
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, err, "orbit enroll error selecting host details")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// EnrollHost enrolls a host
|
||||
func (ds *Datastore) EnrollHost(ctx context.Context, osqueryHostID, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
|
||||
if osqueryHostID == "" {
|
||||
|
|
@ -869,9 +924,11 @@ func (ds *Datastore) EnrollHost(ctx context.Context, osqueryHostID, nodeKey stri
|
|||
`
|
||||
result, err := tx.ExecContext(ctx, sqlInsert, zeroTime, zeroTime, zeroTime, osqueryHostID, nodeKey, teamID)
|
||||
if err != nil {
|
||||
level.Info(ds.logger).Log("hostIDError", err.Error())
|
||||
return ctxerr.Wrap(ctx, err, "insert host")
|
||||
}
|
||||
hostID, _ = result.LastInsertId()
|
||||
level.Info(ds.logger).Log("hostID", hostID)
|
||||
default:
|
||||
// Prevent hosts from enrolling too often with the same identifier.
|
||||
// Prior to adding this we saw many hosts (probably VMs) with the
|
||||
|
|
@ -947,6 +1004,7 @@ func (ds *Datastore) EnrollHost(ctx context.Context, osqueryHostID, nodeKey stri
|
|||
h.team_id,
|
||||
h.policy_updated_at,
|
||||
h.public_ip,
|
||||
h.orbit_node_key,
|
||||
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
|
||||
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available
|
||||
FROM
|
||||
|
|
@ -1027,6 +1085,7 @@ func (ds *Datastore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fl
|
|||
h.team_id,
|
||||
h.policy_updated_at,
|
||||
h.public_ip,
|
||||
h.orbit_node_key,
|
||||
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
|
||||
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available
|
||||
FROM
|
||||
|
|
@ -1046,6 +1105,22 @@ func (ds *Datastore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fl
|
|||
}
|
||||
}
|
||||
|
||||
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
|
||||
// If the node key is invalid it returns a NotFoundError.
|
||||
func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
||||
query := `SELECT * FROM hosts WHERE orbit_node_key = ?`
|
||||
|
||||
var host fleet.Host
|
||||
switch err := ds.getContextTryStmt(ctx, &host, query, nodeKey); {
|
||||
case err == nil:
|
||||
return &host, nil
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ctxerr.Wrap(ctx, notFound("Host"))
|
||||
default:
|
||||
return nil, ctxerr.Wrap(ctx, err, "find host")
|
||||
}
|
||||
}
|
||||
|
||||
// LoadHostByDeviceAuthToken loads the whole host identified by the device auth token.
|
||||
// If the token is invalid it returns a NotFoundError.
|
||||
func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken string) (*fleet.Host, error) {
|
||||
|
|
@ -1198,6 +1273,7 @@ func (ds *Datastore) SearchHosts(ctx context.Context, filter fleet.TeamFilter, m
|
|||
h.team_id,
|
||||
h.policy_updated_at,
|
||||
h.public_ip,
|
||||
h.orbit_node_key,
|
||||
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
|
||||
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
|
||||
COALESCE(hst.seen_time, h.created_at) AS seen_time
|
||||
|
|
@ -1299,6 +1375,7 @@ func (ds *Datastore) HostByIdentifier(ctx context.Context, identifier string) (*
|
|||
h.team_id,
|
||||
h.policy_updated_at,
|
||||
h.public_ip,
|
||||
h.orbit_node_key,
|
||||
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
|
||||
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
|
||||
COALESCE(hst.seen_time, h.created_at) AS seen_time
|
||||
|
|
@ -2553,7 +2630,8 @@ func (ds *Datastore) UpdateHost(ctx context.Context, host *fleet.Host) error {
|
|||
primary_ip = ?,
|
||||
primary_mac = ?,
|
||||
public_ip = ?,
|
||||
refetch_requested = ?
|
||||
refetch_requested = ?,
|
||||
orbit_node_key = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
_, err := ds.writer.ExecContext(ctx, sqlStatement,
|
||||
|
|
@ -2589,6 +2667,7 @@ func (ds *Datastore) UpdateHost(ctx context.Context, host *fleet.Host) error {
|
|||
host.PrimaryMac,
|
||||
host.PublicIP,
|
||||
host.RefetchRequested,
|
||||
host.OrbitNodeKey,
|
||||
host.ID,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20220908181826, Down_20220908181826)
|
||||
}
|
||||
|
||||
func Up_20220908181826(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`ALTER TABLE hosts ADD COLUMN orbit_node_key VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL;`)
|
||||
return err
|
||||
}
|
||||
|
||||
func Down_20220908181826(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUp_20220908181826(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
zeroTime := time.Unix(0, 0).Add(24 * time.Hour)
|
||||
sqlInsert := `
|
||||
INSERT INTO hosts (
|
||||
detail_updated_at,
|
||||
label_updated_at,
|
||||
policy_updated_at,
|
||||
osquery_host_id,
|
||||
node_key,
|
||||
team_id,
|
||||
refetch_requested
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
_, err := db.Exec(sqlInsert, zeroTime, zeroTime, zeroTime, "host_id", "node_key", nil, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
applyNext(t, db)
|
||||
sqlUpdate := `UPDATE hosts SET orbit_node_key = ? WHERE osquery_host_id = ?`
|
||||
_, err = db.Exec(sqlUpdate, "orbit_node_key", "host_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -14,6 +14,8 @@ type AgentOptions struct {
|
|||
Config json.RawMessage `json:"config"`
|
||||
// Overrides includes any platform-based overrides.
|
||||
Overrides AgentOptionsOverrides `json:"overrides,omitempty"`
|
||||
// CommandLineStartUpFlags are the osquery CLI_FLAGS
|
||||
CommandLineStartUpFlags json.RawMessage `json:"command_line_flags,omitempty"`
|
||||
}
|
||||
|
||||
type AgentOptionsOverrides struct {
|
||||
|
|
|
|||
|
|
@ -519,6 +519,10 @@ type Datastore interface {
|
|||
// If the node key is invalid it returns a NotFoundError.
|
||||
LoadHostByNodeKey(ctx context.Context, nodeKey string) (*Host, error)
|
||||
|
||||
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
|
||||
// If the node key is invalid it returns a NotFoundError.
|
||||
LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) (*Host, error)
|
||||
|
||||
// HostLite will load the primary data of the host with the given id.
|
||||
// We define "primary data" as all host information except the
|
||||
// details (like cpu, memory, gigs_disk_space_available, etc.).
|
||||
|
|
@ -600,6 +604,9 @@ type Datastore interface {
|
|||
// within the cooldown period.
|
||||
EnrollHost(ctx context.Context, osqueryHostId, nodeKey string, teamID *uint, cooldown time.Duration) (*Host, error)
|
||||
|
||||
// EnrollOrbit will enroll a new orbit host with the given uuid, setting the orbit node key
|
||||
EnrollOrbit(ctx context.Context, hardwareUUID string, orbitNodeKey string, teamID *uint) (*Host, error)
|
||||
|
||||
SerialUpdateHost(ctx context.Context, host *Host) error
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ type Host struct {
|
|||
SeenTime time.Time `json:"seen_time" db:"seen_time" csv:"seen_time"` // Time that the host was last "seen"
|
||||
RefetchRequested bool `json:"refetch_requested" db:"refetch_requested" csv:"refetch_requested"`
|
||||
NodeKey string `json:"-" db:"node_key" csv:"-"`
|
||||
OrbitNodeKey *string `json:"-" db:"orbit_node_key" csv:"-"`
|
||||
Hostname string `json:"hostname" db:"hostname" csv:"hostname"` // there is a fulltext index on this field
|
||||
UUID string `json:"uuid" db:"uuid" csv:"uuid"` // there is a fulltext index on this field
|
||||
// Platform is the host's platform as defined by osquery's os_version.platform.
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@ type OsqueryService interface {
|
|||
type Service interface {
|
||||
OsqueryService
|
||||
|
||||
// AuthenticateOrbitHost loads host identified by orbit's nodeKey. Returns an error if that nodeKey doesn't exist
|
||||
AuthenticateOrbitHost(ctx context.Context, nodeKey string) (host *Host, debug bool, err error)
|
||||
// EnrollOrbit enrolls orbit to Fleet by using the enrollSecret and returns the orbitNodeKey if successful
|
||||
EnrollOrbit(ctx context.Context, hardwareUUID string, enrollSecret string) (orbitNodeKey string, err error)
|
||||
// GetOrbitFlags returns team specific flags in agent options if the team id is not nil for host, otherwise it returns flags from global agent options
|
||||
GetOrbitFlags(ctx context.Context) (flags json.RawMessage, err error)
|
||||
|
||||
// SetEnterpriseOverrides allows the enterprise service to override specific methods
|
||||
// that can't be easily overridden via embedding.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ type Store struct {
|
|||
DataStore
|
||||
}
|
||||
|
||||
func (m *Store) EnrollOrbit(ctx context.Context, hardwareUUID string, orbitNodeKey string, teamID *uint) (*fleet.Host, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Store) LoadHostByOrbitNodeKey(ctx context.Context, orbitNodeKey string) (*fleet.Host, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *Store) Drop() error { return nil }
|
||||
func (m *Store) MigrateTables(ctx context.Context) error { return nil }
|
||||
func (m *Store) MigrateData(ctx context.Context) error { return nil }
|
||||
|
|
|
|||
|
|
@ -393,6 +393,8 @@ type UpdateQueryAggregatedStatsFunc func(ctx context.Context) error
|
|||
|
||||
type LoadHostByNodeKeyFunc func(ctx context.Context, nodeKey string) (*fleet.Host, error)
|
||||
|
||||
type LoadHostByOrbitNodeKeyFunc func(ctx context.Context, nodeKey string) (*fleet.Host, error)
|
||||
|
||||
type HostLiteFunc func(ctx context.Context, hostID uint) (*fleet.Host, error)
|
||||
|
||||
type UpdateHostOsqueryIntervalsFunc func(ctx context.Context, hostID uint, intervals fleet.HostOsqueryIntervals) error
|
||||
|
|
@ -437,6 +439,8 @@ type VerifyEnrollSecretFunc func(ctx context.Context, secret string) (*fleet.Enr
|
|||
|
||||
type EnrollHostFunc func(ctx context.Context, osqueryHostId string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error)
|
||||
|
||||
type EnrollOrbitFunc func(ctx context.Context, hardwareUUID string, orbitNodeKey string, teamID *uint) (*fleet.Host, error)
|
||||
|
||||
type SerialUpdateHostFunc func(ctx context.Context, host *fleet.Host) error
|
||||
|
||||
type NewJobFunc func(ctx context.Context, job *fleet.Job) (*fleet.Job, error)
|
||||
|
|
@ -1022,6 +1026,9 @@ type DataStore struct {
|
|||
LoadHostByNodeKeyFunc LoadHostByNodeKeyFunc
|
||||
LoadHostByNodeKeyFuncInvoked bool
|
||||
|
||||
LoadHostByOrbitNodeKeyFunc LoadHostByOrbitNodeKeyFunc
|
||||
LoadHostByOrbitNodeKeyFuncInvoked bool
|
||||
|
||||
HostLiteFunc HostLiteFunc
|
||||
HostLiteFuncInvoked bool
|
||||
|
||||
|
|
@ -1088,6 +1095,9 @@ type DataStore struct {
|
|||
EnrollHostFunc EnrollHostFunc
|
||||
EnrollHostFuncInvoked bool
|
||||
|
||||
EnrollOrbitFunc EnrollOrbitFunc
|
||||
EnrollOrbitFuncInvoked bool
|
||||
|
||||
SerialUpdateHostFunc SerialUpdateHostFunc
|
||||
SerialUpdateHostFuncInvoked bool
|
||||
|
||||
|
|
@ -2060,6 +2070,11 @@ func (s *DataStore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fle
|
|||
return s.LoadHostByNodeKeyFunc(ctx, nodeKey)
|
||||
}
|
||||
|
||||
func (s *DataStore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) (*fleet.Host, error) {
|
||||
s.LoadHostByOrbitNodeKeyFuncInvoked = true
|
||||
return s.LoadHostByOrbitNodeKeyFunc(ctx, nodeKey)
|
||||
}
|
||||
|
||||
func (s *DataStore) HostLite(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
s.HostLiteFuncInvoked = true
|
||||
return s.HostLiteFunc(ctx, hostID)
|
||||
|
|
@ -2170,6 +2185,11 @@ func (s *DataStore) EnrollHost(ctx context.Context, osqueryHostId string, nodeKe
|
|||
return s.EnrollHostFunc(ctx, osqueryHostId, nodeKey, teamID, cooldown)
|
||||
}
|
||||
|
||||
func (s *DataStore) EnrollOrbit(ctx context.Context, hardwareUUID string, orbitNodeKey string, teamID *uint) (*fleet.Host, error) {
|
||||
s.EnrollOrbitFuncInvoked = true
|
||||
return s.EnrollOrbitFunc(ctx, hardwareUUID, orbitNodeKey, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) SerialUpdateHost(ctx context.Context, host *fleet.Host) error {
|
||||
s.SerialUpdateHostFuncInvoked = true
|
||||
return s.SerialUpdateHostFunc(ctx, host)
|
||||
|
|
|
|||
|
|
@ -114,6 +114,5 @@ func newBaseClient(addr string, insecureSkipVerify bool, rootCA, urlPrefix strin
|
|||
insecureSkipVerify: insecureSkipVerify,
|
||||
urlPrefix: urlPrefix,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||
|
|
@ -121,6 +122,50 @@ func authenticatedHost(svc fleet.Service, logger log.Logger, next endpoint.Endpo
|
|||
return logged(authHostFunc)
|
||||
}
|
||||
|
||||
func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
nodeKey, err := getOrbitNodeKey(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, debug, err := svc.AuthenticateOrbitHost(ctx, nodeKey)
|
||||
if err != nil {
|
||||
logging.WithErr(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hlogger := log.With(logger, "host-id", host.ID)
|
||||
if debug {
|
||||
logJSON(hlogger, request, "request")
|
||||
}
|
||||
|
||||
ctx = hostctx.NewContext(ctx, host)
|
||||
instrumentHostLogger(ctx)
|
||||
if ac, ok := authz_ctx.FromContext(ctx); ok {
|
||||
ac.SetAuthnMethod(authz_ctx.AuthnOrbitToken)
|
||||
}
|
||||
|
||||
resp, err := next(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if debug {
|
||||
logJSON(hlogger, resp, "response")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
return logged(authHostFunc)
|
||||
}
|
||||
|
||||
func getOrbitNodeKey(r interface{}) (string, error) {
|
||||
if onk, err := r.(interface{ orbitHostNodeKey() string }); err {
|
||||
return onk.orbitHostNodeKey(), nil
|
||||
}
|
||||
return "", errors.New("error getting orbit node key")
|
||||
}
|
||||
|
||||
func getNodeKey(r interface{}) (string, error) {
|
||||
if hnk, ok := r.(interface{ hostNodeKey() string }); ok {
|
||||
return hnk.hostNodeKey(), nil
|
||||
|
|
|
|||
|
|
@ -326,6 +326,19 @@ func newHostAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts [
|
|||
}
|
||||
}
|
||||
|
||||
func newOrbitAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router, versions ...string) *authEndpointer {
|
||||
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
return authenticatedOrbitHost(svc, logger, next)
|
||||
}
|
||||
return &authEndpointer{
|
||||
svc: svc,
|
||||
opts: opts,
|
||||
r: r,
|
||||
authFunc: authFunc,
|
||||
versions: versions,
|
||||
}
|
||||
}
|
||||
|
||||
func newNoAuthEndpointer(svc fleet.Service, opts []kithttp.ServerOption, r *mux.Router, versions ...string) *authEndpointer {
|
||||
return &authEndpointer{
|
||||
svc: svc,
|
||||
|
|
|
|||
|
|
@ -454,6 +454,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
he.WithAltPaths("/api/v1/osquery/log").
|
||||
POST("/api/osquery/log", submitLogsEndpoint, submitLogsRequest{})
|
||||
|
||||
// orbit authenticated endpoints
|
||||
oe := newOrbitAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
||||
oe.POST("/api/fleet/orbit/config", getOrbitConfigEndpoint, orbitGetConfigRequest{})
|
||||
|
||||
// unauthenticated endpoints - most of those are either login-related,
|
||||
// invite-related or host-enrolling. So they typically do some kind of
|
||||
// one-time authentication by verifying that a valid secret token is provided
|
||||
|
|
@ -462,6 +466,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ne.WithAltPaths("/api/v1/osquery/enroll").
|
||||
POST("/api/osquery/enroll", enrollAgentEndpoint, enrollAgentRequest{})
|
||||
|
||||
ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, enrollOrbitRequest{})
|
||||
|
||||
// For some reason osquery does not provide a node key with the block data.
|
||||
// Instead the carve session ID should be verified in the service method.
|
||||
ne.WithAltPaths("/api/v1/osquery/carve/block").
|
||||
|
|
|
|||
144
server/service/orbit.go
Normal file
144
server/service/orbit.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
type orbitError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
type enrollOrbitRequest struct {
|
||||
EnrollSecret string `json:"enroll_secret"`
|
||||
HardwareUUID string `json:"hardware_uuid"`
|
||||
}
|
||||
|
||||
type enrollOrbitResponse struct {
|
||||
OrbitNodeKey string `json:"orbit_node_key,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type orbitGetConfigRequest struct {
|
||||
OrbitNodeKey string `json:"orbit_node_key"`
|
||||
}
|
||||
|
||||
func (r *orbitGetConfigRequest) orbitHostNodeKey() string {
|
||||
return r.OrbitNodeKey
|
||||
}
|
||||
|
||||
type orbitGetConfigResponse struct {
|
||||
Flags json.RawMessage `json:"command_line_startup_flags,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (e orbitError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (r enrollOrbitResponse) error() error { return r.Err }
|
||||
|
||||
func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
req := request.(*enrollOrbitRequest)
|
||||
nodeKey, err := svc.EnrollOrbit(ctx, req.HardwareUUID, req.EnrollSecret)
|
||||
if err != nil {
|
||||
return enrollOrbitResponse{Err: err}, nil
|
||||
}
|
||||
return enrollOrbitResponse{OrbitNodeKey: nodeKey}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) AuthenticateOrbitHost(ctx context.Context, orbitNodeKey string) (*fleet.Host, bool, error) {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
if orbitNodeKey == "" {
|
||||
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing orbit node key"))
|
||||
}
|
||||
|
||||
host, err := svc.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
|
||||
switch {
|
||||
case err == nil:
|
||||
// OK
|
||||
case fleet.IsNotFound(err):
|
||||
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid orbit node key"))
|
||||
default:
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "authentication error orbit")
|
||||
}
|
||||
|
||||
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
||||
}
|
||||
|
||||
// EnrollOrbit returns an orbit nodeKey on successful enroll
|
||||
func (svc *Service) EnrollOrbit(ctx context.Context, hardwareUUID string, enrollSecret string) (string, error) {
|
||||
// this is not a user-authenticated endpoint
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
logging.WithExtras(ctx, "hardware_uuid", hardwareUUID)
|
||||
|
||||
secret, err := svc.ds.VerifyEnrollSecret(ctx, enrollSecret)
|
||||
if err != nil {
|
||||
return "", orbitError{message: "orbit enroll failed: " + err.Error()}
|
||||
}
|
||||
|
||||
orbitNodeKey, err := server.GenerateRandomText(svc.config.Osquery.NodeKeySize)
|
||||
if err != nil {
|
||||
return "", orbitError{message: "failed to generate orbit node key: " + err.Error()}
|
||||
}
|
||||
|
||||
_, err = svc.ds.EnrollOrbit(ctx, hardwareUUID, orbitNodeKey, secret.TeamID)
|
||||
if err != nil {
|
||||
return "", orbitError{message: "failed to enroll " + err.Error()}
|
||||
}
|
||||
|
||||
return orbitNodeKey, nil
|
||||
}
|
||||
|
||||
func getOrbitConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
||||
opts, err := svc.GetOrbitFlags(ctx)
|
||||
if err != nil {
|
||||
return orbitGetConfigResponse{Err: err}, nil
|
||||
}
|
||||
return orbitGetConfigResponse{Flags: opts}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetOrbitFlags(ctx context.Context) (json.RawMessage, error) {
|
||||
// this is not a user-authenticated endpoint
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
host, ok := hostctx.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, orbitError{message: "internal error: missing host from request context"}
|
||||
}
|
||||
|
||||
// team ID is not nil, get team specific flags and options
|
||||
if host.TeamID != nil {
|
||||
teamAgentOptions, err := svc.ds.TeamAgentOptions(ctx, *host.TeamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if teamAgentOptions != nil && len(*teamAgentOptions) > 0 {
|
||||
var opts fleet.AgentOptions
|
||||
if err := json.Unmarshal(*teamAgentOptions, &opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return opts.CommandLineStartUpFlags, nil
|
||||
}
|
||||
}
|
||||
|
||||
// team ID is nil, get global flags and options
|
||||
config, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var opts fleet.AgentOptions
|
||||
if config.AgentOptions != nil {
|
||||
if err := json.Unmarshal(*config.AgentOptions, &opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return opts.CommandLineStartUpFlags, nil
|
||||
}
|
||||
77
server/service/orbit_client.go
Normal file
77
server/service/orbit_client.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type OrbitClient struct {
|
||||
*baseClient
|
||||
enrollSecret string
|
||||
hardwareUUID string
|
||||
}
|
||||
|
||||
func (oc *OrbitClient) request(verb string, path string, params interface{}, resp interface{}) error {
|
||||
var bodyBytes []byte
|
||||
var err error
|
||||
if params != nil {
|
||||
bodyBytes, err = json.Marshal(params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making requst json marshalling : %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(
|
||||
verb,
|
||||
oc.url(path, "").String(),
|
||||
bytes.NewBuffer(bodyBytes),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := oc.http.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", verb, path, err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
return oc.parseResponse(verb, path, response, resp)
|
||||
}
|
||||
|
||||
func NewOrbitClient(addr string, rootCA string, insecureSkipVerify bool, enrollSecret, hardwareUUID string) (*OrbitClient, error) {
|
||||
bc, err := newBaseClient(addr, insecureSkipVerify, rootCA, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &OrbitClient{
|
||||
baseClient: bc,
|
||||
enrollSecret: enrollSecret,
|
||||
hardwareUUID: hardwareUUID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (oc *OrbitClient) DoEnroll() (string, error) {
|
||||
verb, path := "POST", "/api/fleet/orbit/enroll"
|
||||
params := enrollOrbitRequest{EnrollSecret: oc.enrollSecret, HardwareUUID: oc.hardwareUUID}
|
||||
var resp enrollOrbitResponse
|
||||
err := oc.request(verb, path, params, &resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.OrbitNodeKey, nil
|
||||
}
|
||||
|
||||
func (oc *OrbitClient) GetConfig(orbitNodeKey string) (json.RawMessage, error) {
|
||||
verb, path := "POST", "/api/fleet/orbit/config"
|
||||
params := orbitGetConfigRequest{OrbitNodeKey: orbitNodeKey}
|
||||
var resp orbitGetConfigResponse
|
||||
err := oc.request(verb, path, params, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Flags, nil
|
||||
}
|
||||
Loading…
Reference in a new issue