diff --git a/README.md b/README.md index 8eba50d..4c3a03a 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ agent_evidence: See [configuration](./docs/configuration.md) for more information. -The agent adds `_agent_config_hash` to plugin and agent evidence labels. The value is a deterministic SHA-256 hash of -the runtime plugin and agent evidence configuration, which prevents multiple unauthenticated agents using the fallback -`_agent=ccf` identity from writing to the same evidence seed when their configurations differ. +The agent sets the `_agent` label using the following fallback chain: `api.auth.client_id` when available, then +`KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally a deterministic SHA-256 hash of the runtime plugin and agent +evidence configuration. This prevents multiple unauthenticated agents from writing to the same evidence seed when their +configurations differ. ### Environment variables diff --git a/cmd/agent.go b/cmd/agent.go index 7b8eefe..31c54b7 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -200,7 +200,6 @@ const RunnerV2ProtocolVersion int32 = 2 const AnnotationProtocolVersionKey = "org.ccf.plugin.protocol.version" const daemonCronStopTimeout = 30 * time.Second const agentEvidenceErrorArtifactMaxBytes = 1024 * 1024 -const agentConfigHashLabel = "_agent_config_hash" type pluginRunStatus string @@ -836,15 +835,14 @@ func agentIdentityLabel(config *agentConfig) string { } } - return "ccf" + return agentConfigurationHash(config) } func agentFoundationalLabels(config *agentConfig) map[string]string { return map[string]string{ - "_agent": agentIdentityLabel(config), - agentConfigHashLabel: agentConfigurationHash(config), - "tool": "ccf", - "type": "operations", + "_agent": agentIdentityLabel(config), + "tool": "ccf", + "type": "operations", } } @@ -962,7 +960,6 @@ func pluginEvidenceLabelsWithHash(config *agentConfig, pluginName string, plugin labels[k] = v } } - labels[agentConfigHashLabel] = configHash return labels } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 164043c..1520ed3 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1301,10 +1301,9 @@ func TestAgentRunEvidenceIncludesPluginRunSummaryAndErrorArtifacts(t *testing.T) t.Fatalf("expected readable failure reason, got %q", evidence.Status.Reason) } expectedLabels := map[string]string{ - "_agent": clientID, - agentConfigHashLabel: agentConfigurationHash(agentRunner.getConfig()), - "tool": "ccf", - "type": "operations", + "_agent": clientID, + "tool": "ccf", + "type": "operations", } for key, expected := range expectedLabels { if evidence.Labels[key] != expected { @@ -1654,14 +1653,13 @@ func TestSendAgentRunEvidenceAllowsNoPlugins(t *testing.T) { if !ok { t.Fatalf("expected labels object, got %#v", submitted["labels"]) } - if labels["_agent"] != "ccf" || labels["tool"] != "ccf" || labels["type"] != "operations" { + // When no client_id or pod env vars, _agent should be the config hash + expectedHash := agentConfigurationHash(agentRunner.getConfig()) + if labels["_agent"] != expectedHash || labels["tool"] != "ccf" || labels["type"] != "operations" { t.Fatalf("unexpected foundational labels: %#v", labels) } - if labels[agentConfigHashLabel] != agentConfigurationHash(agentRunner.getConfig()) { - t.Fatalf("expected agent config hash label, got %#v", labels) - } - if len(labels) != 4 { - t.Fatalf("expected only four foundational labels, got %#v", labels) + if len(labels) != 3 { + t.Fatalf("expected only three foundational labels, got %#v", labels) } if _, ok := submitted["back-matter"]; ok { t.Fatalf("expected no backmatter for passing no-plugin evidence, got %#v", submitted["back-matter"]) @@ -1722,6 +1720,17 @@ func TestAgentIdentityLabelFallsBackToKubernetesPodName(t *testing.T) { if got != "123e4567-e89b-12d3-a456-426614174000" { t.Fatalf("expected API auth client id to take precedence, got %q", got) } + + // Test that hash is final fallback when no client_id or pod name + t.Setenv("KUBERNETES_POD_NAME", "") + t.Setenv("KUBERNETES_POD", "") + config := newRuntimeHashTestConfig() + config.ApiConfig.Auth = nil // Remove client_id to test hash fallback + expectedHash := agentConfigurationHash(config) + got = agentIdentityLabel(config) + if got != expectedHash { + t.Fatalf("expected config hash as final fallback, got %q (expected %q)", got, expectedHash) + } } func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { @@ -1807,24 +1816,28 @@ func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() + config.ApiConfig.Auth = nil // Remove client_id to test hash fallback labels := agentFoundationalLabels(config) - if labels[agentConfigHashLabel] != agentConfigurationHash(config) { - t.Fatalf("expected foundational labels to include config hash, got %#v", labels) + if labels["_agent"] != agentConfigurationHash(config) { + t.Fatalf("expected _agent label to contain config hash when no client_id, got %#v", labels) } } func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { now := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC) + firstConfig := newRuntimeHashTestConfig() + firstConfig.ApiConfig.Auth = nil // Remove client_id to test hash fallback firstRunner := NewAgentRunner() - firstRunner.UpdateConfig(newRuntimeHashTestConfig()) + firstRunner.UpdateConfig(firstConfig) firstEvidence, err := firstRunner.buildAgentRunEvidence(now) if err != nil { t.Fatalf("build first agent run evidence: %v", err) } secondConfig := newRuntimeHashTestConfig() + secondConfig.ApiConfig.Auth = nil // Remove client_id to test hash fallback secondConfig.Plugins["plugin-a"].Source = "ghcr.io/example/plugin-a:v2" secondRunner := NewAgentRunner() secondRunner.UpdateConfig(secondConfig) @@ -1833,8 +1846,8 @@ func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { t.Fatalf("build second agent run evidence: %v", err) } - if firstEvidence.Labels[agentConfigHashLabel] == secondEvidence.Labels[agentConfigHashLabel] { - t.Fatalf("expected different config hash labels, got %q", firstEvidence.Labels[agentConfigHashLabel]) + if firstEvidence.Labels["_agent"] == secondEvidence.Labels["_agent"] { + t.Fatalf("expected different _agent labels, got %q", firstEvidence.Labels["_agent"]) } if firstEvidence.UUID == secondEvidence.UUID { t.Fatalf("expected config hash change to alter agent evidence UUID %s", firstEvidence.UUID) @@ -1843,11 +1856,12 @@ func TestAgentRunEvidenceUUIDUsesAgentConfigurationHash(t *testing.T) { func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() + config.ApiConfig.Auth = nil // Remove client_id to test hash fallback labels := pluginEvidenceLabels(config, "plugin-a", config.Plugins["plugin-a"]) - if labels[agentConfigHashLabel] != agentConfigurationHash(config) { - t.Fatalf("expected plugin labels to include config hash, got %#v", labels) + if labels["_agent"] != agentConfigurationHash(config) { + t.Fatalf("expected _agent label to contain config hash when no client_id, got %#v", labels) } if labels["_plugin"] != "plugin-a" { t.Fatalf("expected plugin label, got %#v", labels) @@ -1859,7 +1873,7 @@ func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { config := newRuntimeHashTestConfig() - config.ApiConfig.Auth = nil + config.ApiConfig.Auth = nil // Remove client_id to test hash fallback var submittedLabels map[string]string client := newTestHTTPClient(func(r *http.Request) (*http.Response, error) { if r.URL.Path != "/api/evidence" { @@ -1902,8 +1916,8 @@ func TestPluginEvidenceSubmissionIncludesAgentConfigurationHash(t *testing.T) { t.Fatalf("create evidence: %v", err) } - if submittedLabels[agentConfigHashLabel] != agentConfigurationHash(config) { - t.Fatalf("expected submitted plugin evidence to include config hash, got %#v", submittedLabels) + if submittedLabels["_agent"] != agentConfigurationHash(config) { + t.Fatalf("expected submitted plugin evidence to include config hash in _agent, got %#v", submittedLabels) } if submittedLabels["_plugin"] != "plugin-a" || submittedLabels["team"] != "security" { t.Fatalf("expected submitted plugin evidence to include plugin labels, got %#v", submittedLabels) diff --git a/docs/configuration.md b/docs/configuration.md index 4324cd3..2cca8cd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,11 +39,11 @@ agent_evidence: The `plugin_identifier` is a unique identifier for the plugin, and is used to identify the plugin in the logs, you can name this whatever you like but it must be unique. -The `labels` should uniquely identify this agent instance. The agent also sets the `_agent` label on plugin evidence -using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally `ccf`. The agent -also sets `_agent_config_hash`, a deterministic hash of the runtime plugin and agent evidence configuration. Because -evidence UUIDs are seeded from labels, changing either the agent identity or runtime configuration changes the evidence -stream for plugin evidence. +The `labels` should uniquely identify this agent instance. The agent sets the `_agent` label on plugin evidence using +the following fallback chain: `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and +finally a deterministic SHA-256 hash of the runtime plugin and agent evidence configuration. Because evidence UUIDs are +seeded from labels, changing either the agent identity or runtime configuration changes the evidence stream for plugin +evidence. The `plugin_source` is the path to the plugin binary that the agent will run. This can be a relative or absolute path or even a URL to a remote plugin. @@ -64,11 +64,11 @@ is `satisfied`. A plugin remains in the `Plugins with errors` summary until it f Plugins that have never run are listed as pending. Failed plugin errors are attached as back-matter resources and linked from the evidence so they can be downloaded. -Agent evidence uses these labels: `_agent`, `_agent_config_hash`, `tool`, and `type`. The `_agent` label uses -`api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally defaults to `ccf`. The -`_agent_config_hash` label is a SHA-256 hash of plugin names, sources, protocol versions, schedules, policies, plugin -config, plugin labels, and `agent_evidence` settings. It does not include API URL, API auth, or verbosity. The `tool` -label is `ccf`; the `type` label is `operations`. +Agent evidence uses these labels: `_agent`, `tool`, and `type`. The `_agent` label uses the following fallback chain: +`api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally a SHA-256 hash of +plugin names, sources, protocol versions, schedules, policies, plugin config, plugin labels, and `agent_evidence` +settings. The hash does not include API URL, API auth, or verbosity. The `tool` label is `ccf`; the `type` label is +`operations`. If no plugins are configured, ccf-agent still emits passing agent evidence on the configured interval when running in daemon mode. In non-daemon mode, ccf-agent can emit agent evidence only once per invocation.