ADR-006: Migrate Falco event ingestion from gRPC output to HTTP output¶
Status¶
Accepted (supersedes the transport choice in ADR-001)
Date¶
2026-07-23
Context¶
ADR-001 adopted an event-driven architecture that consumes Falco alerts over Falco's gRPC output API (pkg/falco/subscriber.go subscribes to outputs.Sub via falcosecurity/client-go). Real-machine verification of the real-time path (#311) surfaced two problems that make the gRPC transport a dead end:
-
Falco removed gRPC output. Falco
0.44+removedgrpc_outputentirely.0.43.0still ships it but logs "deprecated as consequence of gRPC output deprecation" on startup and droppedprivate_key/cert_chain/root_certsfrom the grpc config schema (they still function but emit a schema-validation warning). We pinneddocker-compose.ymlto0.43.0precisely because we could not go newer (#357). Staying on gRPC pins the whole project to an EOL Falco. -
The mTLS gRPC surface is operationally heavy. It requires generating and distributing a CA + server + client cert set, and the
root_certsfoot-gun (must beca.crt, notserver.crt) already cost us real debugging time (#357).
The event ingestion decision of ADR-001 (Cloud API → audit logs → Falco plugin → TFDrift → detector) is still correct and unchanged. Only the Falco → TFDrift transport needs to move off the removed gRPC path.
Note: this ADR does not cover the separate cloudtrail-plugin continuous-ingest emit gap (the plugin consumes SQS-live messages but only emits reliably in S3-batch mode); that is tracked under the same issue (#360) and is orthogonal to the transport.
Options considered¶
- Falco
http_output→ TFDrift HTTP receiver. Falco POSTs each alert as JSON to a URL. TFDrift already runs an HTTP API server, andhttp_outputblocks already exist (disabled) in everydeployments/falco/*.yaml. Supported on all current and future Falco versions. - Falcosidekick in between. Falco → Falcosidekick → (many outputs). Adds a second service to operate. Falcosidekick's value is fan-out to external sinks (Slack, SIEM, OpenCTI) — a concern that belongs to the donation/observability track, not to TFDrift's own event consumption.
- Keep gRPC / vendor an old Falco. Rejected: pins us to EOL Falco and keeps the mTLS operational burden.
Decision¶
Adopt Falco http_output → a TFDrift HTTP receiver as the primary Falco→TFDrift transport, superseding the gRPC output choice in ADR-001.
- Falco is configured with
http_output.enabled: trueandurlpointing at a new TFDrift endpoint (defaultPOST /api/v1/falco/events), one JSON alert per request (json_output: true). - TFDrift gains an HTTP receiver that parses the Falco alert JSON into the existing
types.Eventand feeds the same downstream pipeline (parser → resource mapper → detector → broadcaster). The parsing/extraction logic insubscriber.go(ParseFalcoOutput,ExtractChanges,ExtractResourceID) is reused, not rewritten. - The gRPC subscriber is kept for one release as an opt-in fallback (
falco.transport: grpc|http, defaulthttp) and then removed, so existing 0.43 deployments are not broken on upgrade. - Falcosidekick is explicitly out of scope here and left to the donation track.
The updated pipeline: Cloud API → audit logs → Falco plugin → Falco http_output → TFDrift HTTP receiver → event parser → resource mapper → drift detector → broadcaster → UI.
Consequences¶
Positive¶
- Works on current and future Falco (unpins us from EOL
0.43.0; unblocks0.44+). - Drops the mTLS cert set and the
root_certsfoot-gun for the default path; auth becomes a standard HTTP concern (shared secret / bearer, aligned with the API's own auth work in #341). - Reuses the existing HTTP server and the existing parse/extract code — small blast radius.
- One less streaming/reconnect surface to own (the reconnect logic added in #312 was gRPC-stream-specific).
Negative¶
- Push model: Falco must reach TFDrift's URL (network/DNS/ingress), where gRPC was a pull subscription. Mitigated by co-locating in the same compose/Helm network.
- A brief two-transport window (grpc + http) until the gRPC path is removed.
- The HTTP receiver must be hardened (auth, body size limits, source restriction) — folded into the API-auth hardening (#341) rather than solved twice.