In order to configure an agent you must make a YAML, JSON or TOML config file at a location of your choice and pass the path to the agent when you run it as follows:
$ ccf-agent -c /path/to/config.yamlThe configuration file must include api. Configure plugins when the agent should collect plugin evidence; if no
plugins are configured, daemon mode still emits its own passing run evidence on the configured interval.
api:
url: http://localhost:8080
plugins:
<plugin_identifier>: # Can have as many of these as you like
labels:
<key>: <value>
<key>: <value>
source: <plugin_source>
policies:
- <policy1>
- <policy2>
...
config:
<config1>: <value1>
<config2>: <value2>
...
policy_data: # Optional dynamic data for policies
<key>: <value>
...
agent_evidence:
enabled: true
emit_on_run_completion: true
interval: 1hThe 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 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.
The policies field is a list of paths to the policy files that the plugin will use to assess the data it collects.
The config field is a map of configuration values that the plugin will use to connect to the data source. The values
will be passed to the plugin when it is run.
The policy_data field is an optional map of dynamic data that will be passed to the plugin's policy manager. This data
can be of any shape and is made available to OPA/Rego policies during evaluation. This allows you to provide runtime
configuration to policies without modifying the policy files themselves.
Usage: satisfied if input.value == data.allowed_value
You can specify as many plugins as you wish, as long as each identifier is unique. You can even reuse the same plugin multiple times with different configurations.
The agent_evidence field configures evidence emitted by ccf-agent about its own plugin collection run. By default,
ccf-agent emits this evidence when a run reaches a terminal point, such as after the first complete plugin run, after a
non-daemon run completes, or when startup plugin or policy downloads fail. The daemon also emits evidence every 1h
whether or not every plugin has run yet. If any plugin has failed, the evidence status is not-satisfied; otherwise it
is satisfied. A plugin remains in the Plugins with errors summary until it finishes a later run successfully.
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 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.
As an example, a configuration file might look like this:
api:
url: http://localhost:8080
auth:
client_id: "123e4567-e89b-12d3-a456-426614174000"
client_secret: "agent-client-secret"
plugins:
local-ssh-security:
labels:
type: ssh
group: production
source: "../plugin-local-ssh/cf-plugin-local-ssh"
policies:
- "../plugin-local-ssh-policies/dist/bundle.tar.gz"
config:
host: "10.0.0.4"
username: "user"
password: "password"
local-ssh-security2:
labels:
type: ssh
group: production
source: "../plugin-local-ssh/cf-plugin-local-ssh"
policies:
- "../plugin-local-ssh-policies/dist/bundle.tar.gz"
config:
host: "10.0.0.5"
username: "user"
password: "password"The following fields are optional:
api:
auth:
client_id: ""
client_secret: ""
plugins:
<plugin_identifier>:
schedule: <cron_expression>
agent_evidence:
enabled: true|false
emit_on_run_completion: true|false
interval: <duration>
verbosity: <log_level>The schedule field is a cron expression that specifies when the plugin should run. If this field is not present the
plugin will run on a default * * * * *. The schedule is in the format minute hour day month day_of_week.
The api.auth fields are optional. If you set either client_id or client_secret, you must set both. The
client_id must be a valid UUID.
The agent_evidence.interval value is a Go-style duration such as 30m, 1h, or 2h45m. Set it to 0s to disable
periodic agent evidence while keeping emit_on_run_completion behavior enabled. Set agent_evidence.enabled to
false to disable all ccf-agent self-evidence. Agent evidence expires after five configured intervals, so the default
1h interval produces a 5h expiry. When interval is 0s, periodic agent evidence is disabled and agent evidence has
no expiry. Set agent_evidence.emit_on_run_completion to false to disable immediate agent evidence on run completion
and startup failures while leaving periodic daemon evidence controlled by interval.
The log_level is one of the following, defaulting to 0 if not specified (a remote overlay's verbosity wins over
the -v flag):
- 0: Shows all ERROR, WARN and INFO
- 1: Shows all of 0 plus DEBUG logs
- 2: Shows all of 1 plus TRACE logs
plugins:
<plugin_identifier>:
enabled: false # default trueA disabled plugin gets no schedule, no download and no run state, but it stays in the configuration the agent reports.
Values in the config file keep viper's weak typing exactly as before: collect_ip_allow_list: false reaches the plugin
as "0", account_id: 123456789012 as "123456789012", and port: 22 as "22". Values set by a remote overlay
must already be strings: a remote port: 2222 (a number) is rejected with invalid-type (R27, R51).
Viper lowercases keys and splits them on dots. Plugin names and config keys in the file are therefore lowercase and
cannot contain dots; a remote overlay that uses GitHub addresses a different plugin than the file's github. Plugin
names an overlay introduces must match ^[a-z0-9][a-z0-9_-]{0,62}$ (R28).
A plugins.<p>.config value may reference environment variables, whole or embedded:
plugins:
postgres:
config:
password: "${env:PG_PASSWORD}"
dsn: "postgres://app:${env:PG_PASSWORD}@db:5432/app"Placeholders are resolved only in plugins.*.config, in the file and in a remote overlay. Anywhere else (for
example policy_data or labels) a placeholder is not resolved: in the file it is passed through as a literal string,
as it always was, and reported as a warning; a remote overlay that puts one there is rejected. CCF_API_AUTH_* may
never be referenced. An unset variable that the file references is a warning, and the value reaches the plugin
unchanged (the literal ${env:NAME}), exactly as before placeholders were resolved (R60). An unset variable that a
remote overlay introduces fails the revision with env-missing; the error names the variable, never a value. Reports,
redaction and the configuration digest always use the unresolved placeholder, so rotating a secret never changes them
(R24).
Plugin values set through viper environment variables (CCF_PLUGINS_<P>_CONFIG_<K>, see the README) are masked as
•••• in every report and in the configuration digest (R25). The rest of the redaction is the API's
pkg/agentconfig (Redact, Digest), which the agent uses as is and which is the source of truth. In short, it masks
values under secret-like keys (for example password, token, secret, api_key, dsn, auth) and secret-looking
values under any key (a password in a URL, a PEM private key, a password= assignment, known token formats). Literal
text mixed with a ${env:NAME} placeholder under a secret-like key is masked too; a value made only of placeholders is
reported as written.
A plugin schedule in the file that does not parse does not stop the agent: that plugin is skipped, the others run,
and the problem is logged and reported as a warning (R34). A few other file values that always loaded are also only
warnings, and are kept unchanged: a negative verbosity (-1 logs WARN and above), a literal ${env:...} outside
plugins.*.config, and an unset variable referenced from the file's plugins.*.config (see above). Every other
invalid value in the file (for example a missing api.url) still fails startup, and on a live reload the agent keeps
running its last good configuration. Values set by a remote overlay are always validated strictly.
An agent with api.auth credentials can pick up a configuration overlay stored in the API. The remote_config block
controls it. It is set locally only (file, host environment, CLI flags), never remotely (R30):
remote_config:
mode: report # off | report | apply_safe | apply_all
poll_interval: 60s # at least 15s
trusted_sources: [] # glob list of plugin/policy sources an overlay may introduce
overridable_config_flags: [] # glob list of plugins.*.config keys an overlay may change
allow_local_sources: falseDefaults (R29): mode is report when api.auth is set and off otherwise (no credentials always forces
off), so an agent applies an overlay only when mode is set to apply_safe or apply_all; poll_interval is 60s; trusted_sources and overridable_config_flags are empty;
allow_local_sources is false. CCF_REMOTE_CONFIG_MODE sets the mode even when the file has no
remote_config block.
| Mode | Behaviour |
|---|---|
off |
No report, no fetch. The heartbeat carries no configuration fields. |
report |
The agent reports its configuration (status not-applicable) but never fetches an overlay. |
apply_safe |
The agent fetches the overlay and applies it only when every change is safe (table below). |
apply_all |
The agent applies safe and unsafe changes. Forbidden changes are still rejected. |
A change is classified as follows (the agent is the authority; the API preview uses the same rules):
| Change | Class |
|---|---|
api, daemon or remote_config in the overlay |
forbidden (the whole revision is rejected in every mode) |
verbosity, agent_evidence.* |
safe |
a plugin's schedule, labels, policy_behavior, protocol_version, policy_data |
safe |
disabling a plugin (enabled: false) |
safe |
| re-enabling a plugin the file disables | unsafe, unless its source matches trusted_sources (then safe); its kept policies, local source and ${env:} references are classified as if new (so a local one is forbidden unless apply_all with allow_local_sources: true) |
| removing a plugin or a policy entry | safe |
| a plugin source or policy entry already used by an enabled plugin in the file (a disabled plugin's sources do not count) | safe |
a new source matching trusted_sources |
safe |
a new OCI source not in trusted_sources |
unsafe |
| a new local path | forbidden, unless apply_all with allow_local_sources: true (then unsafe) |
a plugins.<p>.config.<k> change matching overridable_config_flags (key, plugin:key or *) |
safe |
| any other plugin config change | unsafe |
a new ${env:NAME} reference |
unsafe (CCF_API_AUTH_*: forbidden) |
A rejected or failed revision never interrupts the running configuration: the agent prepares the whole new
configuration (validation, downloads) first and swaps only when it is ready. Every outcome is reported to the API with
a reason (unsafe-changes, forbidden-changes, invalid-config, invalid-type, unknown-field, env-missing,
download-failed, cache-corrupt, internal). When a new configuration is applied,
in-flight plugin runs get up to 5 minutes to finish (R33). Evidence produced under an overlay carries the prop
agent-config-revision (namespace https://compliance-framework.github.io/ns).
The agent caches the last fetched and applied overlay in <state>/remote-config.json (mode 0600, bound to api.url
and api.auth.client_id), so it keeps running the last good overlay when the API is unreachable. At startup it tries,
in order: the freshly fetched overlay, the cached applied overlay, the file alone. Only an unusable file stops the agent.
A fetched overlay already rejected for the same file is skipped, and its rejection (with the unsafe changes) is reported
again, so the instance still shows as rejected after a restart.
When a configuration report is too large for the API, the agent drops its base document and marks it truncated;
the effective document and digest are kept.
The report lists the instance's plugins as plugins[] (name, source, lib-version), where lib-version is
the version of this agent library the plugin binary was built with, read from its Go build info without starting it.
It is empty when unknown (a replaced or (devel) build, or a binary without build info). It is diagnostic only:
nothing is gated on it.
Each agent instance keeps state in .compliance-framework/state/<key>/, relative to the working directory, where
<key> is derived from the absolute path of the config file (R31): the instance ID (instance-id) and the remote
configuration cache. The OCI download caches in .compliance-framework/plugins and
.compliance-framework/policies are shared.
| Setting | Flag | Environment |
|---|---|---|
| State directory | --state-dir |
CCF_STATE_DIR |
| Instance ID (a UUID; not persisted) | --instance-id |
CCF_INSTANCE_ID |
Because the default key depends on the config file's path, moving or renaming the config file creates a new
instance (R52). The agent logs the state directory, where it came from and the instance ID at startup. Containers and
Helm deployments should pin CCF_STATE_DIR to a mounted volume; ephemeral one-shot runs (CI, Kubernetes jobs) can
set CCF_INSTANCE_ID so repeated runs report as one instance.
Plugins receive the agent's environment except CCF_API_AUTH_* (R26).