Skip to main content

Elastic Agent module

Verified on the wire against Elastic Agent 9.5.5 (2026-10-22) with the spike in spikes/fleet-emulation, and against the source of elastic/elastic-agent main 44a9334 (2026-10-16), which imports the Fleet Server API types from github.com/elastic/fleet-server/pkg/api. Facts from the source alone are marked (source).

Strataplane manages Elastic Agent in Fleet mode without Kibana and without Fleet Server: the module implements the agent-facing side of the Fleet Server API. Agents are installed with --url=<strataplane agent listener> and an enrollment token issued by Strataplane.

1. Scope​

  • Stock Elastic Agent 9.0 or later in Fleet mode, Linux and Windows.
  • Policies rendered from layers of kind elastic-agent, with the same selectors, precedence and routes as Telegraf: inputs, outputs (via routes), agent settings, processors.
  • Actions: POLICY_CHANGE, UNENROLL, SETTINGS (log level).

2. Protocol​

Bodies and answers: openapi.yaml, tag agent-fleet (the fields the server reads). The table adds what the stock agent does.

EndpointMethodBehaviour
/api/statusGETCalled by the agent before it switches to changed fleet client settings from a POLICY_CHANGE, and before a migration; not before enrollment. Only the status code counts: a 500 makes the policy change fail; 429/503 count as recoverable (source). Strataplane answers HEALTHY.
/api/fleet/agents/enrollPOSTAuthorization: ApiKey <enrollment token>. The agent always sends id and replace_token ("" when unset) and may send metadata.tags as null; of metadata.local (ECS metadata), elastic.agent.version and host.hostname are used. Of the answer's item the agent requires id, type and access_api_key; an empty id makes it retry enrollment.
/api/fleet/agents/{id}/checkinPOSTAuthorization: ApiKey <access_api_key>, gzip body. agent_policy_id and policy_revision_idx come from the last applied action's data.policy.id and revision (a JSON number). Long-poll: held open until an action is pending or the poll timeout passes; holds up to 280 s are verified, the agent's own timeout is 10 min.
/api/fleet/agents/{id}/acksPOSTOne ACTION_RESULT event per action, about 30 ms after the checkin response that carried it. The response must contain "action":"acks" and per-item results, or the agent queues the ack and retries it later.
/api/fleet/agents/{id}/audit/unenrollPOSTSent by uninstall of an installed agent, reason uninstall or migrated (source). Strataplane marks the instance forgotten.

Agent backoff: the next checkin follows 1–2 s after each response. On errors the agent retries with jittered backoff: the first retry after 30–90 s, doubling up to about 10 min per step. In the default checkin mode a state change does not end a held checkin, so health can lag by up to one pollTimeout. Strataplane holds checkins for up to pollTimeout (the elastic-agent Settings object, spec-format.md §3.8, default 4 min) to keep 10,000 agents at well under 100 requests per second.

Protocol conformance. The Fleet agent API is not a public, versioned contract. Mitigations:

  • Rust types mirror the fleet-server API model that the agent compiles against (fleet-server/pkg/api), tolerate unknown fields, and are checked against every supported minor version by the conformance suite.
  • A conformance suite runs real elastic-agent binaries of every supported minor version (container on Linux, VM job on Windows) against Strataplane in CI: enroll, checkin, policy change, ack, unenroll, re-enroll after token rotation.
  • The supported version range is part of the catalog bundle; agents outside it are accepted but flagged unsupported_version.

3. Identity and enrollment​

PathHowAssurance
Existing PEM host cert (Puppet, ACME, internal PKI; on Windows exportable)elastic-agent install --url=https://sp:8444 --enrollment-token=<token> --elastic-agent-cert=… --elastic-agent-cert-key=… --certificate-authorities=…. Strataplane binds the instance to the cert SAN at enrollment and requires the same cert on every checkin.high
Token only--enrollment-token=<token>; optional "requires approval" on first contact.low
  • Cert-bound enrollment needs install or enroll with --elastic-agent-cert; the container image's FLEET_* variables do not present the client certificate at enrollment. The agent presents it on every later request. An http:// URL is refused unless --insecure is given, so the agent listener serves https.
  • host.id from the agent's metadata is not unique (cloned images, containers on one VM) and never identifies an instance.
  • Enrollment tokens are opaque (spn_…). Labels on the token are applied to the host (source enrollment).
  • The access API key (spe_…) is per instance, stored hashed. Re-enrollment rotates it and re-binds the existing instance only with proof: the instance's bound certificate (or one from a trust anchor for its host name), or the replace_token it was enrolled with (install --id=… --replace-token=…; stored hashed). An id that is already enrolled without that proof is refused (409); without an id, enrollment creates a new instance, and the host name never re-binds one.
  • Elastic Agent persists the key in fleet.enc and the last policy in state.enc (DPAPI on Windows, AES-GCM with a local seed file on Linux).
  • Machines built from one image install and enroll at first boot (Intune, SetupComplete.cmd), never in the image.

4. Policy rendering​

The merged tree of an Elastic Agent instance becomes a full agent policy:

{
"id": "sp-7f3c…",
"revision": 42,
"outputs": {
"logs-kafka": {
"type": "kafka",
"hosts": ["kafka-1:9093", "kafka-2:9093"],
"username": "filebeat",
"password": "<inlined at delivery>"
}
},
"fleet": { "hosts": ["https://strataplane.example.com:8444"] },
"agent": {
"monitoring": { "enabled": false },
"logging": { "level": "info" },
"features": {}
},
"inputs": [
{
"id": "filestream-app-logs",
"type": "filestream",
"use_output": "logs-kafka",
"data_stream": { "namespace": "prod" },
"streams": [
{
"id": "app-logs",
"data_stream": { "dataset": "app.logs" },
"paths": ["/var/log/app/*.log"],
"parsers": [{ "ndjson": { "target": "" } }]
}
],
"processors": [{ "add_fields": { "target": "", "fields": { "team": "team-app" } } }]
}
]
}
  • id and revision are set at delivery, not stored in the artifact, so instances that render alike share one artifact. id is stable per agent instance; revision is the policy_seq of the POLICY_CHANGE action, +1 over the instance's previous one whenever its artifact changes, so the agent's policy_revision_idx maps back to an artifact through the action that carried it.
  • inputs[].id is the instance key name. Inputs are written in raw agent input form, as with standalone agents.
  • agent.monitoring is off by default (§8).
  • Secrets are referenced as { secretRef = "…" } in the spec and stored as versioned placeholders (name@vN) in the artifact. Values are inlined only into the POLICY_CHANGE payload at delivery, after the delivery rule passes. Diffs and the UI show <secret:kafka_logs_password@v3>. Rotating a referenced secret creates a new artifact for exactly the agents that use it.
  • Routes become outputs; every input must name a use_output that is attached to the agent, checked at L1.
  • fleet.hosts is always injected, so agents follow Strataplane URL changes. It stays byte-identical between revisions unless server.agent_url changes, because changed fleet settings make the agent check /api/status before applying the policy.

5. Delivery and applied state​

reconciler: new desired artifact for instance X
→ agent_action POLICY_CHANGE (policy_seq = n), supersedes undelivered older ones
→ NOTIFY 'agent_actions' → the replica holding X's checkin wakes it up
checkin response: actions = [POLICY_CHANGE{id, data.policy (secrets inlined)}] → served_artifact
agent applies, acks → acks endpoint: acked_at
next checkin: policy_revision_idx = n, the policy's inputs among components → applied_artifact
  • Strataplane sets applied_artifact when the action is acked and a later checkin reports policy_revision_idx = n with every input of the policy among its components. An ack alone does not prove the inputs run: a policy whose inputs cannot start is still acked. Component failures after that count as health, not sync (an unreachable output alone makes components DEGRADED).
  • A failed POLICY_CHANGE is never acked. The failure shows in the next checkin as status: error with message: Actions failed: … and the old policy_revision_idx; Strataplane stores that message as the action's ack_error and sets sync = out_of_sync with reason reload_failed.
  • Undelivered actions are re-sent on the next checkin. The agent does not de-duplicate actions by id: a re-sent POLICY_CHANGE is applied and acked again, so a delivered action is re-sent only after the ack timeout, and never once a checkin has reported policy_revision_idx = n.
  • Actions survive restarts and replica failover because they live in agent_action. The ack_token is a delivery cursor: the agent echoes the token of the last checkin response it received, whether or not its actions succeeded; only acks confirm.
  • After acking UNENROLL the agent stops its components and sends nothing more, while its process keeps running; the ack is final.
  • Forgetting an agent that checked in within the last 10 minutes queues UNENROLL first; its ack revokes the credential and forgets the instance. Other agents are forgotten at once.

Long-poll across replicas. A checkin parks on a per-instance tokio::sync::Notify in the replica that received it. Action inserts NOTIFY agent_actions, '<instance id>'; every replica's listener wakes local waiters for that id. On timeout or wake-up, the handler re-reads pending actions from the database, so a lost notification only delays delivery until the next poll.

6. Status ingest​

Checkin fieldStrataplane
status online / DEGRADED / error / startinghealth healthy / degraded / failed / pending; error with Actions failed is a failed policy change (§5)
components[].units[]agent_component rows; a FAILED unit makes health degraded (or failed for outputs)
local_metadata.host, .os, .elastic.agent.versionhost labels (agent source), instance version
agent_policy_id, policy_revision_idxsync state (§5)
upgrade_detailsshown on the instance

Staleness (not_reporting) counts in pollTimeout intervals (agent_instance).

7. Validation​

LevelImplementation
L1Every input has a known type, a unique id, a use_output that exists.
L2Policy JSON Schema (core structure, hand-maintained) plus the input types each component supports in that version, read from the component spec files in the agent package (components/*.spec.yml, extracted to JSON by the catalog bundle builder so the server parses no YAML). Unknown input types are errors; input options are mostly free-form and checked by L3.
L3Optional sandbox: the matching elastic-agent binary in a container in standalone mode with the rendered policy, checking that components start (elastic-agent status / inspect). Slow, therefore per distinct artifact and only at submit. Needs a container runtime on the server.

8. Monitoring​

agent.monitoring is off by default; health comes from checkins. A layer enables it with settings.agent.monitoring, pointing at a route, like any other setting.