kubexa-agent

module
v0.10.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2026 License: GPL-3.0

README

kubexa-agent

Secure agent that connects Kubernetes clusters to the Kubexa platform for monitoring, control, and real-time operations.

Features

  • Collects Kubernetes pod logs (with checkpointing and namespace rules).
  • Watches cluster state (pods, deployments, ingresses, and more).
  • Scrapes Kubernetes Metrics API and custom Prometheus endpoints.
  • Streams data to the Kubexa gateway over a resilient gRPC connection.
  • Deployable with the included helm/kubexa-agent chart.

Helm install

Local chart
helm install kubexa-agent ./helm/kubexa-agent \
  --namespace kubexa \
  --create-namespace \
  --set secret.tenantToken="<tenant-token>" \
  --set gateway.host=gateway.example.com \
  --set gateway.port=443 \
  --set gateway.tls=true
OCI registry (GHCR)

Published by .github/workflows/helm-oci-release.yaml on push to main:

helm install kubexa-agent oci://ghcr.io/kubexa/charts/kubexa-agent \
  --version 0.2.0 \
  --namespace kubexa \
  --create-namespace \
  --set secret.tenantToken="<tenant-token>" \
  --set gateway.host=gateway.example.com
Classic Helm repo

Published by .github/workflows/helm-release.yaml to the kubexa/helm-charts GitHub Pages repo:

helm repo add kubexa https://kubexa.github.io/helm-charts
helm repo update
helm install kubexa-agent kubexa/kubexa-agent \
  --version 0.2.0 \
  --namespace kubexa \
  --create-namespace \
  --set secret.tenantToken="<tenant-token>" \
  --set gateway.host=gateway.example.com

Singleton deployment

The agent runs one replica per cluster. Each instance has its own agent_id, gRPC session, and collectors; scaling beyond 1 duplicates logs, state events, and metrics at the gateway (leader election is not implemented yet).

The Helm chart fixes replicas: 1 and uses strategy: Recreate so upgrades do not briefly run two collectors side by side. Do not scale the Deployment manually or via HPA until HA is supported.

Configuration

Chart values map directly to pkg/config.Config. Key sections:

Values path Config field Description
secret.tenantToken agent.tenant_token Injected via KUBEXA_TENANT_TOKEN
gateway.* gateway.* Gateway address, TLS, reconnect
collect.logs.* collect.logs.* Log tail/follow, rules
collect.state.* collect.state.* Resource watch rules
collect.state.redactSecrets collect.state.redact_secrets Whether Secret data/stringData are stripped before leaving the cluster; default false
collect.metrics.* collect.metrics.* K8s metrics + custom endpoints
query.* query.* Live, on-demand resource reads; omit to inherit collect.state
query.redactSecrets query.redact_secrets Strips Secret data/stringData from live query responses; unset inherits collect.state.redactSecrets
exec.pod.* exec.pod.* A shell in a container the platform may open. rules (namespace / names / containers patterns), defaultShell, maxSessionSec [10, 86400], maxSessions [1, 64], resumeWindowSec (0 = 60, negative = no resume, at most 600). Needs rbac.exec. A rule that fails validation is fatal when enabled, a warning otherwise -- unless exec.node is enabled, which needs a valid exec.pod section (rules AND scalars: both are validated at load while exec.node is on).
exec.node.* exec.node.* A shell on the node itself, through a privileged helper Pod the agent creates and enters with nsenter. Three gates, all off: rbac.nodeShell, exec.node.enabled, and the platform's own cluster:node_console admin permission. image has no default on purpose -- any image with sleep and nsenter works. A node session's resume window is exec.pod.resumeWindowSec (the two consoles share one Manager). maxSessionSec must be at least helperReadyTimeoutSec: the session clock starts at open and the helper wait (image pull included) runs under it. The boot sweep removes orphan helpers from exec.node.namespace AND the release namespace; helpers left in any other namespace (one configured before a change) are not swept and end on activeDeadlineSeconds. With exec.node.namespace set, the rbac.nodeShell Role does not reach it: grant pods create/get/list/watch/delete there yourself.
mutate.node.* mutate.node.* Node jobs the platform may run: cordon, uncordon, drain. Three gates, all off: rbac.nodeOps, mutate.node.enabled, and the platform's own cluster:node_write permission at write level. nodes (patterns; empty matches nothing), verbs (subset of cordon/uncordon/drain; empty grants nothing and is refused), maxTimeoutSec [30, 86400] caps a drain's timeout. Shares nothing with mutate.enabled/mutate.rules. Drain follows kubectl drain: DaemonSet and mirror pods are skipped, PodDisruptionBudgets are honoured (blocked pods retry every 5 s until the timeout), pods with no controller need force, pods mounting emptyDir need delete_emptydir_data; the agent's OWN pod is never evicted (skipped: agent_self). A refused drain (needs force / needs emptydir) has changed nothing -- it does not cordon first. dry_run sends every write with dryRun=All. One job runs at a time.
buffer.* buffer.* Memory/disk queue
observability.* observability.* Health and metrics ports
log.* log.* Agent logger level/format

rbac.* is chart-only — it controls the generated ClusterRole, not a field inside pkg/config.Config, so it is not part of the table above:

Values path Description
rbac.create Whether the chart creates the ClusterRole/ClusterRoleBinding at all. Default true.
rbac.readAll Grants get/list/watch on every apiGroup and resource, CRDs included. Off by default, and deliberately NOT derived from query.rules/collect.state.rules — the agent's config can be supplied as a mounted file this chart never sees, in which case a derived template would find no rules and silently render the narrow role. Pair with a query/collect rule naming resources: ["*"], or the API server refuses everything the enumerated rules do not name.
rbac.write Grants patch/update/delete/create and the */scale subresource, enumerated over the same resource lists readAll's neighboring read rules cover — never a wildcard, because mutate.rules rejects every wildcard resource form. Off by default, and deliberately NOT derived from mutate.rules, for the same mounted-config reason as readAll. mutate.enabled alone is not enough to let the agent write: this flag is the ServiceAccount side of the same decision.
rbac.exec Grants the ServiceAccount pods get (resolve the default container) and pods/exec create. Off by default, and deliberately NOT derived from exec.pod, for the same mounted-config reason as readAll/write.
rbac.nodeShell Grants the ServiceAccount a Role (not a ClusterRole — the helper Pod lives beside the agent) scoped to this release's namespace: create/get/list/watch/delete on pods, plus the same pods/exec create grant rbac.exec gives. NOT derived from exec.node, for the same reason as rbac.exec. A node console is root on the node: leave this false unless you mean it.
rbac.nodeOps Grants nodes get/patch, pods list in every namespace and pods/eviction create. Off by default and NOT derived from mutate.node. A drain evicts every pod on a node: leave this false unless you mean it.
rbac.extraRules Extra PolicyRule entries appended to the generated ClusterRole verbatim.
Example: namespace-scoped log collection
helm upgrade kubexa-agent ./helm/kubexa-agent \
  --namespace kubexa \
  --reuse-values \
  --set collect.logs.rules[0].id=stage-api \
  --set collect.logs.rules[0].namespace=stage \
  --set collect.logs.rules[0].label_selector=log=backend-log \
  --set-json 'collect.logs.rules[0].pod_names=["be-*"]' \
  --set-json 'collect.logs.rules[0].containers=["backend-admin"]'

Keys inside a rules entry are snake_case (pod_names, label_selector), unlike the camelCase chart values around them: the chart copies these lists into the agent's config file verbatim, so the keys are the agent's own config keys. values.schema.json names every key these lists accept, so the same command written with podNames is refused, and the message names the key (helm 3.18 replaced the validator, so older helm 3 words it differently), instead of installing a rule that silently lost its filter -- which would collect more than intended, not less. The check covers the camelCase keys around the lists as well (--set collect.logs.tail_lines=50 is refused), and every other block that ends up inside the agent's config file: agent, gateway, buffer, observability and log. A misspelled key there is refused by name instead of rendering nothing and reading as a setting that took effect, and so is a value of the wrong type -- --set buffer.maxDiskBytes=512Mi never reaches a running agent, because the agent's own loader wants a byte count there. Leaving a rule's field blank stays legal -- yaml.v3 binds a blank to the zero value and skips a blank list entry entirely, so the agent loads it as a filter that was never set.

Durations are the case worth knowing, because the two positions differ:

--set gateway.dialTimeout=30            # refused: reaches the agent as "30"
--set gateway.dialTimeout=30s           # fine
--set collect.state.resyncPeriod=0      # fine: "0" is how a resync is turned off
--set-json 'collect.metrics.rules[0]={"resources":["pods"],"pod_interval":0}'  # refused

The chart quotes a duration written beside a block key, so the agent parses the value's string form and a bare 0 still reads as a zero duration. Inside a rules entry nothing quotes anything, and the agent refuses any number there at startup -- which is a CrashLoopBackOff, so helm refuses it first.

That is also why the refusal reads the way it does. Zero being the one number that works is expressed as a bound, and helm reports the bound rather than the reason:

at '/gateway/dialTimeout': maximum: got 30, want 0

The path names the key; the fix is 30s, not a smaller number.

Charts older than 0.7.5 describe only query.rules, so everything under collect.* installs unchecked there; older than 0.7.6 and the same is true of gateway, buffer, observability and log. Agent images newer than 0.7.3 log unrecognized config key ignored at startup as a second line of defence, but only for keys inside the passed-through lists -- a chart key the template never reads leaves no trace in the rendered config at all.

Or pass a custom values.yaml with full rule definitions.

Existing secret
kubectl create secret generic kubexa-agent-token \
  --namespace kubexa \
  --from-literal=tenant-token=<token>

helm install kubexa-agent ./helm/kubexa-agent \
  --namespace kubexa \
  --set secret.create=false \
  --set secret.existingSecret=kubexa-agent-token
Secret handling

collect.state.redact_secrets (chart: collect.state.redactSecrets) controls whether watched Secret objects have their data/stringData payloads stripped before the agent sends state events to the gateway. Default: false — Secret values are NOT stripped. This is a deliberate choice: with stripping off, Secret values leave the cluster over the agent's existing gRPC stream and are persisted by the Kubexa platform, which is the trade-off required to let cluster admins and owners view Secret values in the resource explorer. Set redactSecrets: true for an installation that must keep Secret values inside the cluster and never send them to the gateway.

Regardless of this setting, managedFields and the kubectl.kubernetes.io/last-applied-configuration annotation are always stripped from every object, Secret or not — for a Secret applied with kubectl apply, that annotation is a second, independent copy of the full manifest including every base64-encoded value.

Live resource query

query.* configures live, on-demand resource reads: a request from the Kubexa platform for the current state of a specific object or list, answered synchronously, as opposed to collect.state's continuous watch-and-push feed. Omit the section to inherit collect.state entirely; set individual fields (e.g. only redactSecrets) to override just those and inherit the rest — see the commented block in values.yaml for the per-field inheritance rules.

The chart ships with query.enabled: true, query.rules: [] (which inherits collect.state.rules's cluster-wide cluster-core rule, covering secrets with no verbs restriction), and redactSecrets: false — so a plain helm upgrade permits live reads of Secret values through this path. This is not a new exposure: the same cluster-core rule already streams those Secret values to the platform continuously via collect.state (see Secret handling); live query just adds a second, on-demand path to data the platform already receives.

This policy is a second gate stacked on top of Kubernetes RBAC, not a replacement for it. Both must allow a read before the agent returns data: RBAC answers "may this ServiceAccount read this object", and query answers "did the cluster owner agree the Kubexa platform may read it". The two are reported separately in the capability catalog so the UI can tell a viewer which of the two is actually blocking a given resource.

enabled: false refuses every live query with POLICY_DENIED — an explicit, diagnosable refusal, not silence. A gateway or UI waiting on a query gets an answer either way.

verbs controls which operations a rule permits, not how much of an object comes back. Restricting a rule to [list] on secrets prevents fetching an individual Secret by name via get; it does not redact anything — a full-view list still returns every matched Secret's data/stringData, exactly as kubectl get secrets -o json does against the Kubernetes API itself. The knob that actually keeps Secret values inside the cluster is redactSecrets: true (query.redactSecrets, which when unset inherits collect.state.redactSecrets — see Secret handling). To expose Secret names without values, either use the TABLE view (QUERY_VIEW_TABLE), which returns printed columns and PartialObjectMetadata and never includes data/stringData, or set redactSecrets: true.

The TABLE view is not a pass-through of the API server's response. Every row's object goes through the same sanitization as the full view before the payload leaves the agent, which matters more than it sounds: PartialObjectMetadata copies annotations verbatim, and for a Secret written with kubectl apply the kubectl.kubernetes.io/last-applied-configuration annotation is a second complete copy of the manifest — base64 values included. That annotation and managedFields are stripped unconditionally, independent of redactSecrets. Rows are also filtered against the rule's names patterns, so a TABLE listing shows exactly the objects the equivalent full-view list shows. Printed cells are left as the API server rendered them; a CRD's additionalPrinterColumns can aim a cell at any field its author chose, so cells reflect definitions that already exist in the cluster and show what kubectl get shows. Kubernetes' built-in Secret columns are NAME/TYPE/DATA/AGE, where DATA is a key count.

The per-GVR verdict published in the capability catalog is deliberately coarser than the real enforcement: a policy scoped to a namespace or a name prefix cannot be reduced to a single boolean for the whole GVR, so that verdict is only a hint for the UI. The full rule evaluation still runs against every individual request, regardless of what the catalog reported.

Memory and resource sizing

The Helm chart defaults to 256 MiB request / 512 MiB limit (helm/kubexa-agent/values.yaml). Actual RSS depends on cluster size, collector settings, and whether the gateway keeps up with export. Figures below are order-of-magnitude estimates for planning—not hard guarantees.

Where memory goes
Component Default cap Notes
Buffer queue (RAM) 64 MiB (buffer.max_memory_bytes) Hard cap; overflow spills to disk
Buffer queue (disk) 512 MiB (buffer.max_disk_bytes) Requires PVC when persistence is enabled
Log streams Up to 200 concurrent (MaxConcurrentStreams) Per-stream read buffers; each log line is a separate queue item
State informer cache Uncapped Scales with watched objects (pods, secrets, etc.)
Go runtime + client-go + gRPC ~60–80 MiB baseline Always present
Scenarios (estimated RSS)
Scenario Estimated memory
Agent + stream only (all collectors disabled) 60–100 MiB
Default Helm install, small cluster (~50 pods), gateway connected 150–200 MiB
Medium cluster (~500 pods), all collectors enabled 200–280 MiB
Gateway disconnected + heavy log volume 250–400 MiB
Large cluster (5000+ pods), broad state watch + many log streams 400–600+ MiB
What drives spikes
  • Log collector — highest variable cost when enabled. High line rates fill the 64 MiB RAM buffer quickly; if the gateway is slow or offline, streams keep producing and spill to disk. Narrow collect.logs.rules and exclude_namespaces in busy clusters.
  • State watcher — client-go informer caches hold full object copies in memory. Watching secrets across many namespaces is expensive. Prefer namespace-scoped rules and only the resources you need.
  • Metrics scraper — usually low (1–10 MiB transient spikes). Large custom Prometheus /metrics pages can add short-lived pressure.
Tuning
# Reduce in-memory buffering (chart values → config.yaml)
buffer:
  maxMemoryBytes: 33554432   # 32 MiB — lower RAM, earlier disk spill
  maxDiskBytes: 1073741824   # 1 GiB — more headroom when gateway is down

# Raise pod limit for large clusters
resources:
  limits:
    memory: 1Gi

Disable collectors you do not need (collect.logs.enabled, collect.state.enabled, collect.metrics.enabled).

Observability

Expose agent self-metrics on observability.metrics_addr (default :9090). Useful series for capacity planning:

  • Queue depth and memory usage (buffer pressure)
  • Active log streams and dropped lines (log backpressure)
  • Informer cache sync / state event rates (watch scope too broad?)
  • Gateway connection state (disconnected → buffer fills)

Development

make run-dev          # agent with example-local.yaml
make run-dev-grpc     # local demo gateway
make helm-lint        # lint chart
make helm-template    # render manifests
make helm-package     # package to dist/

CI releases

Workflow Trigger Output
helm-oci-release.yaml helm/** on main oci://ghcr.io/<org>/charts/kubexa-agent
helm-release.yaml helm/** on main https://kubexa.github.io/helm-charts
build.yaml push/PR Docker image ghcr.io/<org>/kubexa-agent

Directories

Path Synopsis
cmd
agent command
Command agent is the kubexa-agent process entrypoint.
Command agent is the kubexa-agent process entrypoint.
dev-grpc-server command
Command dev-grpc-server is a local Kubexa gateway mock for agent development.
Command dev-grpc-server is a local Kubexa gateway mock for agent development.
internal
capability
Package capability discovers every API resource in the cluster and the agent's own permission to read each one, so the platform can present an honest resource type list instead of a hardcoded subset.
Package capability discovers every API resource in the cluster and the agent's own permission to read each one, so the platform can present an honest resource type list instead of a hardcoded subset.
collector/logs
Package logs collects Kubernetes pod logs and buffers them for export.
Package logs collects Kubernetes pod logs and buffers them for export.
collector/logs/checkpoint
Package checkpoint persists per-stream log read positions for restart resume.
Package checkpoint persists per-stream log read positions for restart resume.
collector/metrics
Package metrics scrapes Kubernetes Metrics API and custom Prometheus endpoints.
Package metrics scrapes Kubernetes Metrics API and custom Prometheus endpoints.
collector/state
Package state watches Kubernetes API objects via shared informers and enqueues state events.
Package state watches Kubernetes API objects via shared informers and enqueues state events.
exec
Package exec runs interactive console sessions in Pod containers on the gateway's request.
Package exec runs interactive console sessions in Pod containers on the gateway's request.
exec/policy
Package policy decides whether the cluster owner's agent configuration permits a console session in a container.
Package policy decides whether the cluster owner's agent configuration permits a console session in a container.
health
Package health exposes HTTP liveness and readiness probes for kubexa-agent.
Package health exposes HTTP liveness and readiness probes for kubexa-agent.
ingestrules
Package ingestrules holds the ingest rules the gateway pushes, and the agent's own defaults for whatever it does not push.
Package ingestrules holds the ingest rules the gateway pushes, and the agent's own defaults for whatever it does not push.
k8s
Package k8s provides a production-grade Kubernetes API client for kubexa-agent.
Package k8s provides a production-grade Kubernetes API client for kubexa-agent.
k8s/k8sconfig
Package k8sconfig holds configuration for the Kubernetes API client.
Package k8sconfig holds configuration for the Kubernetes API client.
logger
Package logger provides a context-aware structured logger built on zerolog.
Package logger provides a context-aware structured logger built on zerolog.
metrics
Package metrics defines and registers all Prometheus metrics for the kubexa-agent.
Package metrics defines and registers all Prometheus metrics for the kubexa-agent.
mutate
Package mutate applies write operations requested by the gateway.
Package mutate applies write operations requested by the gateway.
mutate/policy
Package policy decides whether a mutation is permitted by the cluster owner's agent configuration.
Package policy decides whether a mutation is permitted by the cluster owner's agent configuration.
nodeops
internal/nodeops/drain.go
internal/nodeops/drain.go
pprof
Package pprof serves Go profiles on a dedicated, opt-in listener.
Package pprof serves Go profiles on a dedicated, opt-in listener.
query
Package query answers live, on-demand resource reads from the gateway.
Package query answers live, on-demand resource reads from the gateway.
query/policy
Package policy decides whether a live resource query is permitted by the cluster owner's agent configuration.
Package policy decides whether a live resource query is permitted by the cluster owner's agent configuration.
queue
Package queue provides a durable two-tier buffer between collectors and the gRPC export stream.
Package queue provides a durable two-tier buffer between collectors and the gRPC export stream.
stream
Package stream manages the outbound gRPC connection from kubexa-agent to the Kubexa Gateway.
Package stream manages the outbound gRPC connection from kubexa-agent to the Kubexa Gateway.
pkg
buildinfo
Package buildinfo exposes agent release metadata injected at link time.
Package buildinfo exposes agent release metadata injected at link time.
config
Package config loads and validates kubexa-agent runtime configuration.
Package config loads and validates kubexa-agent runtime configuration.
config/k8sresource
Package k8sresource maps configured resource names to Kubernetes API metadata.
Package k8sresource maps configured resource names to Kubernetes API metadata.
protoversion
Package protoversion defines the agent/gateway protobuf API version contract and handshake negotiation helpers.
Package protoversion defines the agent/gateway protobuf API version contract and handshake negotiation helpers.
proto

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL