Skip to content

Commit f0e22b2

Browse files
authored
Merge pull request #1 from codeGROOVE-dev/sigstore
Add sigstore verification support
2 parents 9adeda8 + ac9786a commit f0e22b2

File tree

4 files changed

+138
-32
lines changed

4 files changed

+138
-32
lines changed

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all build server agent clean test lint run-server run-agent help
1+
.PHONY: all build server agent sign clean test lint run-server run-agent help
22

33
GOCMD=go
44
GOBUILD=$(GOCMD) build
@@ -9,25 +9,31 @@ GOMOD=$(GOCMD) mod
99

1010
SERVER_BINARY=./out/gitmdm-server
1111
AGENT_BINARY=./out/gitmdm-agent
12+
SIGN_BINARY=./out/gitmdm-sign
1213
SERVER_PATH=./cmd/server
1314
AGENT_PATH=./cmd/agent
15+
SIGN_PATH=./cmd/sign
1416

1517
BUILD_FLAGS=-ldflags="-s -w" -trimpath
1618

1719
all: build
1820

19-
build: server agent
21+
build: server agent sign
2022

2123
server:
2224
$(GOBUILD) $(BUILD_FLAGS) -o $(SERVER_BINARY) $(SERVER_PATH)
2325

2426
agent:
2527
$(GOBUILD) $(BUILD_FLAGS) -o $(AGENT_BINARY) $(AGENT_PATH)
2628

29+
sign:
30+
$(GOBUILD) $(BUILD_FLAGS) -o $(SIGN_BINARY) $(SIGN_PATH)
31+
2732
clean:
2833
$(GOCLEAN)
2934
rm -f $(SERVER_BINARY)
3035
rm -f $(AGENT_BINARY)
36+
rm -f $(SIGN_BINARY)
3137

3238
test:
3339
$(GOTEST) -v ./...

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ The SOC-2 compliance solution for the discerningly paranoid security engineer.
44

55
![logo](./media/logo_small.png "gitMDM logo")
66

7+
## What Happens When a Security Engineer Builds an MDM
8+
9+
gitMDM is what you get when you ask a security engineer to make an MDM tool. Traditional MDMs operate on the assumption that the central server is trustworthy and should have root access to execute arbitrary code on all endpoints. We think that's insane.
10+
11+
**Core Security Principle**: A compromise of the MDM server should NOT result in a compromise of all agents reporting to it.
12+
13+
This is why gitMDM:
14+
- **Cannot execute remote commands** - The server literally lacks the code to push commands to agents
15+
- **Uses cryptographic signatures** - All agent configurations are signed with Sigstore, preventing a compromised server from injecting malicious checks
16+
- **Runs without privileges** - Agents run as regular users, not root/SYSTEM
17+
- **Reports only** - Information flows one way: from agents to server, never the reverse
18+
719
## Your Problem
820

921
Your startup just hit the enterprise sales milestone where someone asks "are you SOC 2 compliant?" Meanwhile, your engineering team runs OpenBSD on ThinkPads, Arch on Frameworks, and that one person still dailying Plan 9.
@@ -25,6 +37,7 @@ Your Team: "...continue"
2537
### Why Your Security Team Will Actually Approve This
2638

2739
- **Zero Remote Execution**: Can't push commands or install software. The server only receives data.
40+
- **Cryptographically Signed Configs**: All agent configurations require Sigstore signatures. A compromised server can't inject malicious checks.
2841
- **No Auto-Updates**: No downloading binaries from the internet. Updates require YOU to rebuild and redeploy.
2942
- **Runs as User**: No root, no SYSTEM. Can't execute arbitrary code or modify your system.
3043
- **You Own Everything**: Your server, your git repo, your data. No third-party cloud with root access to your fleet.
@@ -102,11 +115,47 @@ We detect 11+ desktop environments because your team refuses to standardize.
102115

103116
The server literally cannot execute commands. We removed the code. It's not there.
104117

118+
### Configuration Integrity via Sigstore
119+
120+
Every agent configuration is cryptographically signed using Sigstore's keyless signing:
121+
122+
```bash
123+
# Sign configuration with your GitHub identity
124+
gitmdm-sign --config cmd/agent/checks.yaml
125+
126+
# Agent verifies signature at runtime
127+
gitmdm-agent --signed-by "github:yourusername@example.com"
128+
```
129+
130+
This means:
131+
- **Configurations are tamper-proof** - Any modification breaks the signature
132+
- **Identity-based trust** - You know exactly who signed each configuration (GitHub, Google, etc.)
133+
- **No key management** - Sigstore handles the PKI complexity
134+
- **Transparency logs** - All signatures are recorded in an immutable ledger
135+
136+
Even if an attacker compromises your server, they cannot:
137+
- Inject malicious compliance checks
138+
- Modify existing check definitions
139+
- Bypass signature verification on agents
140+
141+
### Future: Check-Build-Check
142+
143+
We're building automated remediation that maintains our security principles:
144+
- **Check**: Agent identifies non-compliance
145+
- **Build**: Server generates a fix script (signed, of course)
146+
- **Check**: Agent verifies the fix worked
147+
148+
Even remediation scripts will require cryptographic signatures. No unsigned code execution, ever.
149+
105150
## FAQ
106151

107152
> "What happens if someone compromises the server?"
108153
109-
Nothing. Perhaps they can clean up the old stale check-in data while they are there.
154+
They get read-only access to compliance reports. They cannot:
155+
- Push commands to agents (no code for it)
156+
- Modify agent behavior (signatures prevent it)
157+
- Install malware (agents don't accept commands)
158+
Perhaps they can clean up the old stale check-in data while they are there.
110159

111160
> "What if someone tampers with the agent?"
112161

cmd/agent/install.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ const (
2222

2323
// AgentConfig stores the agent configuration.
2424
type AgentConfig struct {
25-
ServerURL string `json:"server_url"`
26-
JoinKey string `json:"join_key"`
25+
ServerURL string `json:"server_url"`
26+
JoinKey string `json:"join_key"`
27+
ValidSigners []string `json:"valid_signers,omitempty"` // Allowed config file signers
2728
}
2829

2930
// configDir returns the appropriate configuration directory for the platform.
@@ -99,7 +100,7 @@ func installExecutable(exePath, targetPath string) error {
99100
}
100101

101102
// installAgent installs the agent to run automatically at system startup.
102-
func installAgent(serverURL, joinKey string) error {
103+
func installAgent(serverURL, joinKey string, allowedSigners []string) error {
103104
// Get home directory
104105
homeDir, err := os.UserHomeDir()
105106
if err != nil {
@@ -149,8 +150,9 @@ func installAgent(serverURL, joinKey string) error {
149150

150151
configPath := filepath.Join(configDir, configName)
151152
config := AgentConfig{
152-
ServerURL: serverURL,
153-
JoinKey: joinKey,
153+
ServerURL: serverURL,
154+
JoinKey: joinKey,
155+
ValidSigners: allowedSigners,
154156
}
155157
configData, err := json.MarshalIndent(config, "", " ")
156158
if err != nil {

cmd/agent/main.go

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ var (
8787
verbose = flag.Bool("verbose", false, "Show all check outputs, not just failures (with --run all)")
8888
install = flag.Bool("install", false, "Install agent to run automatically at startup")
8989
uninstall = flag.Bool("uninstall", false, "Uninstall agent and remove autostart")
90-
quiet = false // Set to true to suppress INFO logs (used for interactive mode)
90+
signedBy = flag.String("signed-by", "github:t+github@stromberg.org",
91+
"Comma-separated list of provider:identity pairs allowed to sign configs (e.g., github:username, google:email@example.com)")
92+
skipSignatureCheck = flag.Bool("skip-signature-check", false, "Skip signature verification (INSECURE - for development only)")
93+
quiet = false // Set to true to suppress INFO logs (used for interactive mode)
9194
)
9295

9396
// Agent represents the gitMDM agent that collects compliance data.
@@ -142,7 +145,8 @@ func (a *Agent) handleInstall() error {
142145
log.Printf("✓ Device registered as: %s (%s)", a.hostname, a.hardwareID)
143146

144147
// Now proceed with installation
145-
if err := installAgent(*server, *join); err != nil {
148+
allowedSigners := parseAllowedSigners(*signedBy)
149+
if err := installAgent(*server, *join, allowedSigners); err != nil {
146150
return fmt.Errorf("installation failed: %w", err)
147151
}
148152
log.Println("✓ Agent installed successfully and will run automatically at startup")
@@ -225,6 +229,19 @@ func (a *Agent) configureServerConnection() error {
225229

226230
// initializeAgent creates and initializes an Agent instance.
227231
func initializeAgent() (*Agent, error) {
232+
// First, verify the embedded checks.yaml is properly signed
233+
switch {
234+
case !*skipSignatureCheck:
235+
if err := verifyEmbeddedConfig(); err != nil {
236+
return nil, fmt.Errorf("configuration signature verification failed: %w", err)
237+
}
238+
case isDevelopmentMode():
239+
log.Print("[INFO] Development mode: signature verification disabled (running via 'go run')")
240+
default:
241+
log.Print("[WARN] ⚠️ SIGNATURE VERIFICATION SKIPPED (--skip-signature-check flag)")
242+
log.Print("[WARN] Running with UNVERIFIED configuration - this is INSECURE!")
243+
}
244+
228245
var cfg config.Config
229246
if err := yaml.Unmarshal(checksConfig, &cfg); err != nil {
230247
return nil, fmt.Errorf("failed to parse checks config: %w", err)
@@ -247,7 +264,7 @@ func initializeAgent() (*Agent, error) {
247264

248265
return &Agent{
249266
config: &cfg,
250-
hardwareID: hardwareID(),
267+
hardwareID: hardwareID(context.Background()),
251268
hostname: hostname,
252269
user: user,
253270
httpClient: &http.Client{
@@ -309,6 +326,10 @@ func checkAndCreatePIDFile() (exists bool, cleanup func()) {
309326
}
310327
}
311328
log.Printf("[INFO] Removing stale PID file for non-existent process %d", oldPID)
329+
// Remove the stale PID file so we can create our own
330+
if err := os.Remove(pidPath); err != nil && !os.IsNotExist(err) {
331+
log.Printf("[WARN] Failed to remove stale PID file: %v", err)
332+
}
312333
}
313334
}
314335

@@ -318,7 +339,8 @@ func checkAndCreatePIDFile() (exists bool, cleanup func()) {
318339
if err != nil {
319340
if os.IsExist(err) {
320341
// Another process created the file between our check and now
321-
log.Print("[WARN] PID file was created by another process, exiting")
342+
// This is an actual race condition with another starting instance
343+
log.Print("[INFO] Another agent instance is starting up, exiting")
322344
return false, func() {}
323345
}
324346
log.Printf("[WARN] Failed to create PID file: %v", err)
@@ -350,6 +372,28 @@ func checkAndCreatePIDFile() (exists bool, cleanup func()) {
350372
return true, cleanup
351373
}
352374

375+
// isDevelopmentMode detects if the agent is running via "go run" instead of as a built binary.
376+
func isDevelopmentMode() bool {
377+
// When go run executes, it creates binaries in temp with specific patterns:
378+
// /tmp/go-build####/b###/exe/binary-name
379+
380+
exePath, err := os.Executable()
381+
if err != nil {
382+
return false
383+
}
384+
385+
// Resolve symlinks to get the real path
386+
realPath, err := filepath.EvalSymlinks(exePath)
387+
if err != nil {
388+
realPath = exePath
389+
}
390+
391+
// Check if path matches go run's pattern
392+
// Must be in temp AND contain "go-build" AND have b### directory structure
393+
return strings.Contains(realPath, filepath.Join(os.TempDir(), "go-build")) ||
394+
strings.Contains(realPath, filepath.Join("/private"+os.TempDir(), "go-build")) // macOS symlink
395+
}
396+
353397
func main() {
354398
// Set up panic recovery first
355399
defer func() {
@@ -361,6 +405,13 @@ func main() {
361405

362406
flag.Parse()
363407

408+
// Auto-detect development mode
409+
isDev := isDevelopmentMode()
410+
if isDev && !*skipSignatureCheck {
411+
// Automatically skip signature checks in development
412+
*skipSignatureCheck = true
413+
}
414+
364415
// Set up file logging (non-fatal if it fails)
365416
var logFile *os.File
366417
if lf, err := setupLogging(); err != nil {
@@ -954,9 +1005,7 @@ func (*Agent) osVersion(ctx context.Context) string {
9541005
return "unknown"
9551006
}
9561007

957-
func darwinHardwareID() string {
958-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
959-
defer cancel()
1008+
func darwinHardwareID(ctx context.Context) string {
9601009
cmd := exec.CommandContext(ctx, "ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
9611010
output, err := cmd.Output()
9621011
if err != nil {
@@ -982,7 +1031,7 @@ func darwinHardwareID() string {
9821031
return ""
9831032
}
9841033

985-
func linuxHardwareID() string {
1034+
func linuxHardwareID(_ context.Context) string {
9861035
data, err := os.ReadFile("/sys/class/dmi/id/product_uuid")
9871036
if err == nil {
9881037
id := strings.TrimSpace(string(data))
@@ -1007,8 +1056,8 @@ func linuxHardwareID() string {
10071056
return ""
10081057
}
10091058

1010-
func bsdHardwareID() string {
1011-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1059+
func bsdHardwareID(ctx context.Context) string {
1060+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
10121061
defer cancel()
10131062
cmd := exec.CommandContext(ctx, "sysctl", "-n", "kern.hostuuid")
10141063
output, err := cmd.Output()
@@ -1025,8 +1074,8 @@ func bsdHardwareID() string {
10251074
return id
10261075
}
10271076

1028-
func solarisHardwareID() string {
1029-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1077+
func solarisHardwareID(ctx context.Context) string {
1078+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
10301079
defer cancel()
10311080
cmd := exec.CommandContext(ctx, "hostid")
10321081
output, err := cmd.Output()
@@ -1043,9 +1092,9 @@ func solarisHardwareID() string {
10431092
return id
10441093
}
10451094

1046-
func illumosHardwareID() string {
1095+
func illumosHardwareID(ctx context.Context) string {
10471096
// Try sysinfo first (Illumos specific)
1048-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1097+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
10491098
defer cancel()
10501099
cmd := exec.CommandContext(ctx, "sysinfo", "-p")
10511100
output, err := cmd.Output()
@@ -1063,11 +1112,11 @@ func illumosHardwareID() string {
10631112
}
10641113
}
10651114
// Fall back to hostid
1066-
return solarisHardwareID()
1115+
return solarisHardwareID(ctx)
10671116
}
10681117

1069-
func windowsHardwareID() string {
1070-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1118+
func windowsHardwareID(ctx context.Context) string {
1119+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
10711120
defer cancel()
10721121
cmd := exec.CommandContext(ctx, wmicCmd, "csproduct", wmicGetArg, "UUID")
10731122
output, err := cmd.Output()
@@ -1091,7 +1140,7 @@ func windowsHardwareID() string {
10911140
return ""
10921141
}
10931142

1094-
func hardwareID() string {
1143+
func hardwareID(ctx context.Context) string {
10951144
start := time.Now()
10961145
if *debugMode {
10971146
log.Printf("[DEBUG] Detecting hardware ID for OS: %s", runtime.GOOS)
@@ -1100,17 +1149,17 @@ func hardwareID() string {
11001149
var id string
11011150
switch runtime.GOOS {
11021151
case osDarwin:
1103-
id = darwinHardwareID()
1152+
id = darwinHardwareID(ctx)
11041153
case osLinux:
1105-
id = linuxHardwareID()
1154+
id = linuxHardwareID(ctx)
11061155
case "freebsd", "openbsd", "netbsd", "dragonfly":
1107-
id = bsdHardwareID()
1156+
id = bsdHardwareID(ctx)
11081157
case "solaris":
1109-
id = solarisHardwareID()
1158+
id = solarisHardwareID(ctx)
11101159
case "illumos":
1111-
id = illumosHardwareID()
1160+
id = illumosHardwareID(ctx)
11121161
case osWindows:
1113-
id = windowsHardwareID()
1162+
id = windowsHardwareID(ctx)
11141163
default:
11151164
if *debugMode {
11161165
log.Printf("[DEBUG] Unsupported OS for hardware ID detection: %s", runtime.GOOS)

0 commit comments

Comments
 (0)