diff --git a/.gitignore b/.gitignore index ef415a7..36ea1ed 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ node_modules/ # Left behind by `pip install .` of the engine (setuptools metadata), not source. /behavision.egg-info/ +/.prod/ diff --git a/agent/main.go b/agent/main.go index d1575c1..8c55b98 100644 --- a/agent/main.go +++ b/agent/main.go @@ -119,6 +119,11 @@ func cmdClaim(args []string) error { cfg.BrokerPassword = b.MQTTPass cfg.AgentToken = b.AgentToken cfg.CloudBase = base + caPath, err := enrol.SaveCA(b.CACert, paths.BrokerCA()) + if err != nil { + return err + } + cfg.BrokerCAFile = caPath // A PC that was running on its own and has now been linked is no longer // standalone. cfg.Standalone = false diff --git a/agent/pkg/enrol/enrol.go b/agent/pkg/enrol/enrol.go index 4d944e6..7ee7bff 100644 --- a/agent/pkg/enrol/enrol.go +++ b/agent/pkg/enrol/enrol.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "net/http" + "os" "strings" "time" ) @@ -31,7 +32,7 @@ type Bootstrap struct { MQTTURL string `json:"mqtt_url"` MQTTUser string `json:"mqtt_username"` MQTTPass string `json:"mqtt_password"` - CAPem string `json:"ca_pem,omitempty"` + CACert string `json:"ca_cert,omitempty"` AgentToken string `json:"agent_token"` } @@ -90,3 +91,23 @@ func Claim(ctx context.Context, base, code string) (Bootstrap, error) { } return out, nil } + +// SaveCA writes the broker's CA beside the agent config and returns its path. +// +// The server hands the CA out at enrolment precisely so it never has to be +// shipped in an installer - and for a while nothing on the receiving end +// wrote it anywhere. Every claimed PC then dialled tls://mcp.loyaly.ai:8883 +// with the system trust store, the private CA failed verification, and the +// agent reported "the broker did not accept this PC" (a TLS failure is +// indistinguishable from a refusal at that layer). No real site could ever +// publish a visit. An empty CA returns "" so a deployment on a public +// certificate keeps working unchanged. +func SaveCA(pem, path string) (string, error) { + if strings.TrimSpace(pem) == "" { + return "", nil + } + if err := os.WriteFile(path, []byte(pem), 0o600); err != nil { + return "", fmt.Errorf("write broker CA: %w", err) + } + return path, nil +} diff --git a/agent/pkg/paths/paths.go b/agent/pkg/paths/paths.go index cbdcc6a..1eef4ac 100644 --- a/agent/pkg/paths/paths.go +++ b/agent/pkg/paths/paths.go @@ -57,8 +57,11 @@ func InstallRoot() string { } func AgentConfig() string { return filepath.Join(StateRoot(), "agent.json") } -func SpoolDir() string { return filepath.Join(StateRoot(), "spool") } -func EngineLog() string { return filepath.Join(StateRoot(), "engine.log") } + +// BrokerCA is the broker's CA certificate, written at enrolment. +func BrokerCA() string { return filepath.Join(StateRoot(), "broker-ca.crt") } +func SpoolDir() string { return filepath.Join(StateRoot(), "spool") } +func EngineLog() string { return filepath.Join(StateRoot(), "engine.log") } // APICredentials is the file the engine writes when it generates its own // Basic credentials. The agent reads it rather than storing a second copy, diff --git a/desktop/app.go b/desktop/app.go index 4bf36f1..df52b3a 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -23,6 +23,7 @@ import ( agentcameras "github.com/loyaly/behavision-agent/pkg/cameras" agentcfg "github.com/loyaly/behavision-agent/pkg/config" agentengine "github.com/loyaly/behavision-agent/pkg/engine" + "github.com/loyaly/behavision-agent/pkg/enrol" agentmqtt "github.com/loyaly/behavision-agent/pkg/mqtt" agentpaths "github.com/loyaly/behavision-agent/pkg/paths" agentspool "github.com/loyaly/behavision-agent/pkg/spool" @@ -434,6 +435,11 @@ func (a *App) Claim(code string) (SessionInfo, error) { a.cfg.BrokerPassword = b.MQTTPass a.cfg.AgentToken = b.AgentToken a.cfg.CloudBase = a.cloud.Base + caPath, err := enrol.SaveCA(b.CACert, agentpaths.BrokerCA()) + if err != nil { + return SessionInfo{}, err + } + a.cfg.BrokerCAFile = caPath // A PC that was running on its own and has now been linked is no longer // standalone. Leaving the flag set would keep the head-office screens // hidden on the one machine that just earned them.