From fd1ef2fa0a7ad137cd61ac1a1768fc3ba9a1a6dc Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 09:06:28 -0300 Subject: [PATCH 1/3] fix: agent config hash as last resource attempt to name the agent Signed-off-by: Gustavo Carvalho --- README.md | 6 +++--- cmd/agent.go | 11 ++++------- cmd/agent_test.go | 33 ++++++++++++++++----------------- docs/configuration.md | 18 ++++++++---------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 8eba50d..a445976 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ 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 to a deterministic SHA-256 hash of the runtime plugin and agent evidence configuration +(using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). +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..f8abe45 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": agentConfigurationHash(agentRunner.getConfig()), + "tool": "ccf", + "type": "operations", } for key, expected := range expectedLabels { if evidence.Labels[key] != expected { @@ -1654,14 +1653,14 @@ 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" { + if 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 labels["_agent"] != agentConfigurationHash(agentRunner.getConfig()) { + t.Fatalf("expected _agent label to contain config hash, 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"]) @@ -1810,8 +1809,8 @@ func TestAgentFoundationalLabelsIncludeAgentConfigurationHash(t *testing.T) { 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, got %#v", labels) } } @@ -1833,8 +1832,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) @@ -1846,8 +1845,8 @@ func TestPluginEvidenceLabelsIncludeAgentConfigurationHash(t *testing.T) { 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, got %#v", labels) } if labels["_plugin"] != "plugin-a" { t.Fatalf("expected plugin label, got %#v", labels) @@ -1902,8 +1901,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..ad072a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,11 +39,10 @@ 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 to a +deterministic SHA-256 hash of the runtime plugin and agent evidence configuration (using `api.auth.client_id` when +available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). 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 +63,10 @@ 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 contains a SHA-256 hash of plugin +names, sources, protocol versions, schedules, policies, plugin config, plugin labels, and `agent_evidence` settings +(using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). +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. From 12e1490e1b6d525a5679792f0ebb102a5746718e Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 09:23:11 -0300 Subject: [PATCH 2/3] fix: more fixes Signed-off-by: Gustavo Carvalho --- cmd/agent.go | 12 ------------ cmd/agent_test.go | 24 ------------------------ 2 files changed, 36 deletions(-) diff --git a/cmd/agent.go b/cmd/agent.go index 31c54b7..8a54da3 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -823,18 +823,6 @@ func apiAuthClientSecretSet(config *apiConfig) bool { } func agentIdentityLabel(config *agentConfig) string { - if config != nil && config.ApiConfig != nil && config.ApiConfig.Auth != nil { - if clientID := strings.TrimSpace(config.ApiConfig.Auth.ClientID); clientID != "" { - return clientID - } - } - - for _, envName := range []string{"KUBERNETES_POD_NAME", "KUBERNETES_POD"} { - if podName := strings.TrimSpace(os.Getenv(envName)); podName != "" { - return podName - } - } - return agentConfigurationHash(config) } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index f8abe45..89a8b48 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1699,30 +1699,6 @@ func TestSendAgentRunEvidenceIncludesAPIErrorBody(t *testing.T) { } } -func TestAgentIdentityLabelFallsBackToKubernetesPodName(t *testing.T) { - t.Setenv("KUBERNETES_POD_NAME", "ccf-agent-7b6f") - t.Setenv("KUBERNETES_POD", "ignored") - - got := agentIdentityLabel(&agentConfig{ - ApiConfig: &apiConfig{Url: "http://example.test"}, - }) - if got != "ccf-agent-7b6f" { - t.Fatalf("expected Kubernetes pod name identity, got %q", got) - } - - got = agentIdentityLabel(&agentConfig{ - ApiConfig: &apiConfig{ - Url: "http://example.test", - Auth: &apiAuthConfig{ - ClientID: "123e4567-e89b-12d3-a456-426614174000", - }, - }, - }) - if got != "123e4567-e89b-12d3-a456-426614174000" { - t.Fatalf("expected API auth client id to take precedence, got %q", got) - } -} - func TestAgentConfigurationHashUsesRuntimeConfigOnly(t *testing.T) { base := newRuntimeHashTestConfig() baseHash := agentConfigurationHash(base) From cc9a3fda39dfa544b6ddd71b76b5dfc53135e43d Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Fri, 8 May 2026 09:34:46 -0300 Subject: [PATCH 3/3] fix: revert the changes Signed-off-by: Gustavo Carvalho --- README.md | 7 +++--- cmd/agent.go | 12 +++++++++ cmd/agent_test.go | 57 ++++++++++++++++++++++++++++++++++++------- docs/configuration.md | 18 ++++++++------ 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a445976..4c3a03a 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ agent_evidence: See [configuration](./docs/configuration.md) for more information. -The agent sets the `_agent` label to a deterministic SHA-256 hash of the runtime plugin and agent evidence configuration -(using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). -This prevents multiple unauthenticated agents 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 8a54da3..31c54b7 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -823,6 +823,18 @@ func apiAuthClientSecretSet(config *apiConfig) bool { } func agentIdentityLabel(config *agentConfig) string { + if config != nil && config.ApiConfig != nil && config.ApiConfig.Auth != nil { + if clientID := strings.TrimSpace(config.ApiConfig.Auth.ClientID); clientID != "" { + return clientID + } + } + + for _, envName := range []string{"KUBERNETES_POD_NAME", "KUBERNETES_POD"} { + if podName := strings.TrimSpace(os.Getenv(envName)); podName != "" { + return podName + } + } + return agentConfigurationHash(config) } diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 89a8b48..1520ed3 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -1301,7 +1301,7 @@ func TestAgentRunEvidenceIncludesPluginRunSummaryAndErrorArtifacts(t *testing.T) t.Fatalf("expected readable failure reason, got %q", evidence.Status.Reason) } expectedLabels := map[string]string{ - "_agent": agentConfigurationHash(agentRunner.getConfig()), + "_agent": clientID, "tool": "ccf", "type": "operations", } @@ -1653,12 +1653,11 @@ func TestSendAgentRunEvidenceAllowsNoPlugins(t *testing.T) { if !ok { t.Fatalf("expected labels object, got %#v", submitted["labels"]) } - if 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["_agent"] != agentConfigurationHash(agentRunner.getConfig()) { - t.Fatalf("expected _agent label to contain config hash, got %#v", labels) - } if len(labels) != 3 { t.Fatalf("expected only three foundational labels, got %#v", labels) } @@ -1699,6 +1698,41 @@ func TestSendAgentRunEvidenceIncludesAPIErrorBody(t *testing.T) { } } +func TestAgentIdentityLabelFallsBackToKubernetesPodName(t *testing.T) { + t.Setenv("KUBERNETES_POD_NAME", "ccf-agent-7b6f") + t.Setenv("KUBERNETES_POD", "ignored") + + got := agentIdentityLabel(&agentConfig{ + ApiConfig: &apiConfig{Url: "http://example.test"}, + }) + if got != "ccf-agent-7b6f" { + t.Fatalf("expected Kubernetes pod name identity, got %q", got) + } + + got = agentIdentityLabel(&agentConfig{ + ApiConfig: &apiConfig{ + Url: "http://example.test", + Auth: &apiAuthConfig{ + ClientID: "123e4567-e89b-12d3-a456-426614174000", + }, + }, + }) + 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) { base := newRuntimeHashTestConfig() baseHash := agentConfigurationHash(base) @@ -1782,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["_agent"] != agentConfigurationHash(config) { - t.Fatalf("expected _agent label to contain config hash, got %#v", labels) + 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) @@ -1818,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["_agent"] != agentConfigurationHash(config) { - t.Fatalf("expected _agent label to contain config hash, got %#v", labels) + 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) @@ -1834,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" { diff --git a/docs/configuration.md b/docs/configuration.md index ad072a6..2cca8cd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,10 +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 sets the `_agent` label on plugin evidence to a -deterministic SHA-256 hash of the runtime plugin and agent evidence configuration (using `api.auth.client_id` when -available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). 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. @@ -63,10 +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`, `tool`, and `type`. The `_agent` label contains a SHA-256 hash of plugin -names, sources, protocol versions, schedules, policies, plugin config, plugin labels, and `agent_evidence` settings -(using `api.auth.client_id` when available, then `KUBERNETES_POD_NAME` or `KUBERNETES_POD`, and finally the hash). -The hash 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.