0069. Control-plane metrics are exposed by spec.controlPlaneMetrics and fenced by one source-restricting firewall rule
Status: Accepted · Date: 2026-10-26 · Design section: Monitoring and registry mirrors
Decided: TalosCluster.spec.controlPlaneMetrics {enabled, allowedCIDRs} (off by default; enabled needs at least one CIDR, /0 rejected) renders, on control-plane nodes only and in the system layer, bind-address: 0.0.0.0 for kube-controller-manager and kube-scheduler, listen-metrics-urls: http://0.0.0.0:2381 for etcd, and one NetworkRuleConfig for TCP 2381, 10257 and 10259 that drops every source outside allowedCIDRs, with Talos's default firewall action left at accept; the firewall kinds are no-reboot in pkg/fieldmeta, the etcd argument stays reboot-required; the chart gets no new monitoring resource.
Context
architecture.md ("Monitoring and registry mirrors") says Ichor can expose the control-plane metrics Talos
binds locally and restrict scraping to a CIDR with Talos's ingress firewall. ADR 0040 left it as a
pkg/render system-layer unit and listed what it needs. Everything below was read from the Talos
v1.14.3 source (siderolabs/talos@v1.14.3 and pkg/machinery@v1.14.3), not recalled.
1. What moves the bind
- kube-controller-manager.
controllers/k8s/control_plane_final.gobuilds its arguments from a default set containing"bind-address": 127.0.0.1and merges the document'sextraArgsover it.bind-addressis not in that controller'sMergeDeniedlist, soextraArgscan replace it. The 1.14 document isKubeControllerManagerConfig(config/types/k8s/controller_manager.go), fieldextraArgs; below the 1.14 contract it iscluster.controllerManager.extraArgs. - kube-scheduler. Same file, default
"bind-address": 127.0.0.1,MergeDeniedonly for the kubeconfig andconfigarguments. DocumentKubeSchedulerConfig, fieldextraArgs; legacycluster.scheduler.extraArgs. - The kubelet probes keep working. Both static pods probe
Host: localhost(k8stemplates/controller-manager.go,scheduler.go), which a0.0.0.0bind still answers. - etcd.
services/etcd.gohas a deny list of arguments Talos owns (listen-peer-urls,listen-client-urls,listen-client-http-urls, certificates, ...).listen-metrics-urlsis not in it, and Talos sets nothing for it, socluster.etcd.extraArgscan carry it. etcd has no standalone 1.14 document (v1alpha1_etcdconfig.go);cluster.etcd.extraArgsis the only form, so the legacyConfigdocument is rendered for etcd under every contract. Not verified in source: that etcd v3.7.2 honours the flag (the etcd source is not in the module cache); it is an upstream etcd flag and Talos passes it through untouched. Needs a real node. - Ports. Controller-manager 10257 and scheduler 10259 (HTTPS, bearer token with RBAC for
/metrics), etcd 2381 (plain HTTP). Talos 1.14 already serves etcd's/metricsover mTLS on 2383 (constants.EtcdClientHTTPPort) on all addresses; that needs a client certificate, so it is not the port a scraper uses and is not touched here.
2. The firewall: one rule, default action untouched
Documents (config/types/network): NetworkRuleConfig (name, portSelector {ports, protocol},
ingress [{subnet, except}]) and NetworkDefaultActionConfig (ingress: accept|block).
controllers/network/nftables_chain_config.go shows the evaluation order:
- chain policy is accept (drop only under
block); - input from
lo,kubespanandsiderolinkis accepted; - conntrack established/related is accepted, invalid is dropped;
- under
blockonly, ICMP, pod/service CIDR and host-DNS accepts are added; - each
NetworkRuleConfigbecomes one rule: under default accept its verdict is drop and the source match is inverted (drop TCP to these ports when the source is not in the subnets); underblockit would be an accept.
DefaultAction() returns accept when no NetworkDefaultActionConfig exists (config/network.go,
"zero value is accept"). A rule with default accept therefore changes nothing for any port the rule
does not name: the Kubernetes API (6443), etcd client and peer (2379, 2380), kubelet (10250) and the
Talos API (50000) are never matched. Because lo is accepted before the rule, a node scraping itself
still works. Ichor therefore never renders NetworkDefaultActionConfig; switching it to block
would drop all of the above and is exactly what this design must not do. The test asserts the render
contains no NetworkDefaultActionConfig and that the rule names no port besides the three.
Consequences to know: the rule also drops sources that are not the scraper, such as other nodes, so
allowedCIDRs must cover wherever Prometheus runs (pod CIDR or node CIDR, depending on CNI and
masquerading). Talos also builds a prerouting chain once any rule exists; with default accept it only
adds drops for NEW connections to the same ports, so it widens nothing. The rule is rendered only
for control-plane nodes (workers run none of these services).
3. The spec
spec.controlPlaneMetrics: # optional pointer, unset = today's behaviour
enabled: false # default false
allowedCIDRs: [10.20.0.0/16] # max 8, items <= 43 chars
CEL (the spec.discovery style): self.all(c, isCIDR(c) && cidr(c).prefixLength() > 0) on the list
(constant cost, bounded by MaxItems) and !self.enabled || (has(self.allowedCIDRs) && self.allowedCIDRs.size() > 0) on the object. Enabled without a CIDR is rejected in the CRD, and again
in render (an error, never an open port): with default accept an empty subnet list would be no
restriction at all. A /0 is rejected for the same reason; IPv4 and IPv6 are both accepted since the
Talos rule accepts either. Disabled keeps allowedCIDRs so toggling does not lose them.
4. Render layer
System layer, applied after the registries and storage and before every patch, taking the cluster
(Render already receives it), so SystemInputs does not grow: nothing here is read from the
cluster at run time. The machine controller's input hash already includes the cluster generation.
Patch override: the typed patch kinds are Config, SysctlConfig, KernelModuleConfig and the
volume kinds, so there is no way to write NetworkRuleConfig, KubeSchedulerConfig or
KubeControllerManagerConfig in a patch. Under a 1.14 contract a legacy cluster.scheduler patch
is also rejected by Talos next to the standalone document (already true before this ADR). What a patch
can override is etcd's argument (cluster.etcd.extraArgs), tested. For the rest the answer is the
switch in the spec, as with ADR 0066. Disabled renders byte-identically to unset (tested).
5. Reboot necessity (pkg/fieldmeta)
NetworkRuleConfigandNetworkDefaultActionConfig: no reboot.NfTablesChainConfigControllertakes the activeMachineConfigas an input and rewrites its chains on each change; the apply RPC's no-reboot mode only persists the config (v1alpha1_server.go). Rules added.KubeControllerManagerConfig/KubeSchedulerConfigextraArgs: already no-reboot (static pods).cluster.etcd.*: reboot-required stays.services/etcd.goreads the extra arguments when the service starts and no controller restarts it on change, so nothing was verified that would make it safe. Consequence: togglingenabledon a running cluster needs a reboot of each control-plane node, one at a time, through the existing reboot path.
Chart
No ServiceMonitor for the control plane was added. A Prometheus scrape of node IPs needs either a
selector-less Service plus Endpoints naming the node addresses, which the chart cannot know, or a
Service in kube-system, outside the release namespace the chart owns; neither can be checked here. The
scrape targets are https://<node>:10257/metrics, https://<node>:10259/metrics (bearer token) and
http://<node>:2381/metrics, for the operator's own Prometheus (Ichor installs none).
Not verified, needs a real node
etcd's listen-metrics-urls taking effect on etcd 3.7.2; the firewall rule dropping a non-listed
source in practice; the scheduler/controller-manager /metrics RBAC for the scraper's identity.