diff --git a/client/orbit_client.go b/client/orbit_client.go index 3fdecdefac2..ac1bb19e4a2 100644 --- a/client/orbit_client.go +++ b/client/orbit_client.go @@ -94,8 +94,9 @@ type OrbitClient struct { // >>> OPENFRAME(agent-openframe-mode): openframe-mode flag + bearer-token auth manager on the orbit client — openframe/docs/agent-openframe-mode.md // openframe mode - openFrameMode bool - authManager *openframe.OpenFrameAuthorizationManager + openFrameMode bool + authManager *openframe.OpenFrameAuthorizationManager + machineIdProvider *openframe.OpenFrameMachineIdProvider // <<< OPENFRAME(agent-openframe-mode) } @@ -177,7 +178,7 @@ func (oc *OrbitClient) requestWithExternal(verb string, pathOrURL string, params request.Header.Set("Content-Type", "application/json") } // <<< OPENFRAME(agent-json-content-type) - // >>> OPENFRAME(agent-openframe-mode): inject Bearer auth header on every request when in openframe mode — openframe/docs/agent-openframe-mode.md + // >>> OPENFRAME(agent-openframe-mode): inject Bearer auth + x-machine-id headers on every request when in openframe mode — openframe/docs/agent-openframe-mode.md if oc.openFrameMode { authToken := oc.authManager.GetToken() if authToken != "" { @@ -185,6 +186,13 @@ func (oc *OrbitClient) requestWithExternal(verb string, pathOrURL string, params } else { log.Debug().Msg("authToken is empty, not adding Authorization header") } + + if oc.machineIdProvider != nil { + machineId := oc.machineIdProvider.GetMachineId() + if machineId != "" { + request.Header.Add("x-machine-id", machineId) + } + } } // <<< OPENFRAME(agent-openframe-mode) } @@ -259,6 +267,13 @@ func NewOrbitClient( nodeKeyFilePath := filepath.Join(rootDir, constant.OrbitNodeKeyFileName) ctx, cancelFunc := context.WithCancel(context.Background()) + // >>> OPENFRAME(agent-openframe-mode): machine-id provider for the x-machine-id header — openframe/docs/agent-openframe-mode.md + var machineIdProvider *openframe.OpenFrameMachineIdProvider + if openFrameMode { + machineIdProvider = openframe.NewOpenFrameMachineIdProvider() + } + // <<< OPENFRAME(agent-openframe-mode) + return &OrbitClient{ nodeKeyFilePath: nodeKeyFilePath, BaseClient: bc, @@ -272,8 +287,9 @@ func NewOrbitClient( receiverUpdateCancelFunc: cancelFunc, hostIdentityCertPath: hostIdentityCertPath, // >>> OPENFRAME(agent-openframe-mode): wire openframe fields into the OrbitClient literal — openframe/docs/agent-openframe-mode.md - authManager: authManager, - openFrameMode: openFrameMode, + authManager: authManager, + openFrameMode: openFrameMode, + machineIdProvider: machineIdProvider, // <<< OPENFRAME(agent-openframe-mode) }, nil } diff --git a/openframe/docs/agent-openframe-mode.md b/openframe/docs/agent-openframe-mode.md index 4bf3c47cd90..783017f5fe6 100644 --- a/openframe/docs/agent-openframe-mode.md +++ b/openframe/docs/agent-openframe-mode.md @@ -140,6 +140,10 @@ extra constructor arguments — `openFrameMode bool` and `Authorization: Bearer ` to every request. An empty token logs a debug line and sends no header (the gateway then rejects the request, and the next refresh cycle will repopulate the token). +- **Machine-id header.** Also in `requestWithExternal`, `OpenFrameMachineIdProvider` + reads the shared OpenFrame `machine_id` file (written by openframe-client) and, + if non-empty, adds `x-machine-id: ` to every request for gateway/firewall + machine identification. A missing or empty file sends no header. The node-key enrollment behavior is unchanged by OpenFrame mode and is documented separately in [node-key-management.md](node-key-management.md). @@ -194,6 +198,7 @@ host API (see [api-expose-osquery-host-id.md](api-expose-osquery-host-id.md)). | `server/service/openframe/openframe-encryption-service.go` | AES-GCM decryption | | `server/service/openframe/openframe-token-extractor.go` | Read + decrypt token file | | `server/service/openframe/openframe_authorization_manager.go` | Thread-safe token holder | +| `server/service/openframe/openframe_machine_id_provider.go` | Cached reader of the shared OpenFrame `machine_id` file | | `server/service/openframe/openframe_token_refresher.go` | 5-second cron token refresh | | `server/service/base_client.go` | Additional enrollment/debug logging | diff --git a/server/service/openframe/openframe_machine_id_provider.go b/server/service/openframe/openframe_machine_id_provider.go new file mode 100644 index 00000000000..193a8e09c55 --- /dev/null +++ b/server/service/openframe/openframe_machine_id_provider.go @@ -0,0 +1,92 @@ +package openframe + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/rs/zerolog/log" +) + +// OpenFrameMachineIdProvider reads the shared machine ID written by openframe-client. +type OpenFrameMachineIdProvider struct { + machineId string + initialized bool + mu sync.RWMutex +} + +func NewOpenFrameMachineIdProvider() *OpenFrameMachineIdProvider { + return &OpenFrameMachineIdProvider{} +} + +// GetMachineId returns the machine ID, reading from file if not cached +func (p *OpenFrameMachineIdProvider) GetMachineId() string { + p.mu.RLock() + if p.initialized { + machineId := p.machineId + p.mu.RUnlock() + return machineId + } + p.mu.RUnlock() + + p.mu.Lock() + defer p.mu.Unlock() + + if p.initialized { + return p.machineId + } + + p.machineId = p.readFromFile() + // Don't cache an empty read: the file may not be written yet, retry on the next call + p.initialized = p.machineId != "" + return p.machineId +} + +// Refresh forces a re-read of machine ID from file +func (p *OpenFrameMachineIdProvider) Refresh() { + p.mu.Lock() + defer p.mu.Unlock() + + p.machineId = p.readFromFile() + p.initialized = true +} + +func (p *OpenFrameMachineIdProvider) getFilePath() string { + switch runtime.GOOS { + case "windows": + programData := os.Getenv("ProgramData") + if programData == "" { + return "" + } + return filepath.Join(programData, "OpenFrame", "machine_id") + case "darwin": + return "/Library/Application Support/OpenFrame/machine_id" + default: + return "/var/lib/openframe/machine_id" + } +} + +func (p *OpenFrameMachineIdProvider) readFromFile() string { + path := p.getFilePath() + if path == "" { + log.Warn().Msg("Could not determine machine_id file path") + return "" + } + + data, err := os.ReadFile(path) + if err != nil { + log.Debug().Str("path", path).Err(err).Msg("Could not read machine_id file") + return "" + } + + machineId := strings.TrimSpace(string(data)) + if machineId == "" { + log.Warn().Str("path", path).Msg("Machine ID file is empty") + return "" + } + + log.Debug().Str("path", path).Msg("Read machine ID from file") + return machineId +}