Talos: Where COSI Was Born
This chapter is the origin story: Talos Linux is what COSI was built for. The entire operating system — networking, configuration, services, upgrades, hardware discovery — is modeled as a graph of controllers reconciling typed resources against a central state broker. COSI was extracted from Talos to be reusable. Reading Talos is reading the library at full depth, in full production, on real machines.
All resource types, namespace names, controller names, file paths, and code excerpts are drawn from the public siderolabs/talos repository (github.com/siderolabs/talos). Resource types live in pkg/machinery/resources/; controllers live in internal/app/machined/pkg/controllers/. The architecture described here is accurate as of the Talos v1.x series. Fit assessment and synthesis are labelled as such.
- What Talos is
- The origin: COSI as extracted infrastructure
- The topology: machined and its runtimes
- The bootstrap sequence
- The resource catalog: namespaces and types
- The config plane
- The network plane: spec/status at scale
- Multi-source merge: the network controller graph
- The runtime plane: services as resources
- The Talos API: COSI state over gRPC
- Omni: distributed COSI at fleet scale
- Lessons from a real OS
- Verdict
#What Talos is
Talos Linux is an immutable, API-only operating system purpose-built for running Kubernetes. It has no SSH, no shell, no package manager, no user accounts. Every configuration change is a structured API call. Every observable fact about the machine — its IP addresses, its hostname, the status of its services — is a structured API response. The system is designed around one guarantee: if you know the desired state, the machine will converge to it, deterministically, from any starting point.
Immutable by design
The OS root is a read-only SquashFS image loop-mounted into memory. Configuration is applied through the machine API — no SSH, no shell, no /etc edits. The entire runtime state lives in COSI.
~120 controllers, 13 namespaces
Talos registers ~120 controllers across config, network, etcd, k8s, runtime, hardware, block, kubespan, and other domains. Every subsystem is a controller. Every subsystem communicates only through state.
API = COSI state
The external Talos API (apid) exposes the COSI state broker over gRPC. talosctl get addresses is a COSI List. talosctl watch is a COSI WatchKind. The dependency graph is inspectable with talosctl inspect dependencies (Graphviz output).
#The origin: COSI as extracted infrastructure
COSI was not designed in the abstract and then applied to Talos — it emerged from Talos. Sidero Labs built the resource/controller engine inside the Talos monorepo, discovered it was the load-bearing architecture for the entire OS, and then extracted it into github.com/cosi-project/runtime so other systems could use the same substrate. The current cosi-project/runtime library is a direct descendant of code that has been running on bare metal in production since Talos v0.x.
Every concept in chapters 01–05 was first real in Talos. The conformance tests in cosi-project/runtime/pkg/controller/conformance/ test exactly what Talos's controllers depend on. If you understand how Talos uses COSI, you understand what the library was designed for. This is the ground truth.
#The topology: machined and its runtimes
On each Talos node, the main system daemon is machined — effectively the init daemon and PID 1. It owns the COSI runtime and registers every controller. The COSI state.State is the machine's memory — everything transient and derived is held entirely in-memory; only the MachineConfig is persisted to disk and is re-loaded as a resource on each boot. All other resources are reconstructed from scratch by their controllers.
machined, wired to the same COSI State. apid is a thin gRPC bridge — it adds auth, then proxies directly to the state broker. External tools (talosctl, Omni) are just COSI clients.#The bootstrap sequence: resources coming to life
COSI state is entirely in-memory. On every boot, it starts empty and reconstructs from scratch. The order in which resources appear is exactly the dependency graph the runtime has computed. The sequence below shows how a Talos control-plane node goes from power-on to a running Kubernetes cluster — every step is a controller waking up because its inputs appeared:
- machined starts (PID 1 equivalent). COSI runtime initializes. All namespaces exist but contain zero resources. Controllers are registered but blocked on
r.EventCh(). - ConfigAcquireController reads persistent config from disk and creates the
config.MachineConfigresource (ID:v1alpha1). This is the trigger event for everything else. - ~12 config controllers wake simultaneously.
MachineTypeController,TimeServerConfigController,NetworkConfigController, and others all watchedMachineConfig. They fan out, writing their typed sub-resources intonetwork,k8s,etcd, etc. - Network controllers cascade.
LinkConfigControllerwritesLinkSpecresources →LinkSpecControllerapplies them to the kernel →LinkStatusControllerreflects actual state back asLinkStatus. Same for addresses and routes.NodeAddressControlleraggregatesAddressStatus→ writesNodeAddress. - etcd and Kubernetes controllers unblock. They were waiting for
NodeAddress(needed to compute peer/advertise addresses). Nowetcd.SpecControllerwritesetcd.Spec→ etcd service starts → becomes healthy. - KubeletStaticPodController writes
k8s.StaticPodresources forkube-apiserver,kube-controller-manager,kube-scheduler. A local HTTP server (part of machined) serves these manifests to kubelet. Kubernetes control plane comes up. - Cluster controllers join.
cluster.Memberandcluster.Affiliateresources appear as the node joins the etcd cluster and the Kubernetes API becomes available.
The key property: no step happens before its dependencies are satisfied, and no step polls — everything is triggered by the appearance of the resources it was waiting for. If you change the machine config mid-run, only the controllers whose inputs changed wake up; everything else stays quiet. The dependency graph, not a startup script, is the operating system's boot logic.
#The resource catalog: namespaces and types
Talos organizes every resource into a namespace. The namespace is the first axis of isolation — controllers in the network namespace only declare outputs there; no other controller can write network addresses. Here is the full catalog as of Talos v1.x:
| Namespace | Package | What lives here | Who writes |
|---|---|---|---|
config | resources/config | MachineConfig, MachineType, ConfigStatus, ConfigPatch | machined on apply; config controllers |
network | resources/network | AddressSpec/Status, LinkSpec/Status, RouteSpec/Status, HostnameSpec/Status, DNSResolvConf, NfTablesAddressSet, NodeAddress, … | network controllers exclusively |
runtime | resources/runtime | MachineStatus, UniqueMachineToken, ExtensionServiceConfig, KernelModuleSpec, MountStatus, … | runtime controllers |
hardware | resources/hardware | Processor, MemoryModule, SystemInformation | hardware discovery controllers |
cluster | resources/cluster | Member, Affiliate, Config, Identity | cluster and discovery controllers |
secrets | resources/secrets | Etcd, Kubernetes, Root, OSRoot, API | secrets bootstrap controller |
block | resources/block | Disk, Volume, VolumeConfig, VolumeStatus, SystemDisk | block and volume controllers |
filesystems | resources/filesystems | Mountpoint — active filesystem mounts | mount controllers |
k8s | resources/k8s | APIServerConfig, ControllerManagerConfig, KubeletConfig, StaticPod, StaticPodStatus, Nodename, NodeIP, manifest specs, etcd peer config | Kubernetes bootstrap controllers |
etcd | resources/etcd | Config, Spec, Member, PKIStatus — control-plane only | etcd controllers |
kubespan | resources/kubespan | Config, Identity, Endpoint, PeerSpec/Status — WireGuard overlay | KubeSpan controllers |
time | resources/time | Status, AdjtimeStatus — NTP sync state | time sync controllers |
files | resources/files | EtcFileSpec/Status — managed /etc files | file management controllers |
storage | resources/storage | LVM logical/physical volume specs and statuses | storage/LVM controllers |
v1alpha1 | — | boot sequence trackers, legacy migration resources | various |
Every type name resolves through resource.ResourceType. For example, network.AddressSpecType = "AddressSpecs.net.talos.dev". The .net.talos.dev group suffix is how Talos scopes its types — a convention you'd replicate in your own application with your own domain suffix.
#The config plane
A Talos machine is configured by writing a MachineConfig resource (a YAML document) over the machine API. Internally, machined creates a config.MachineConfig resource in the config namespace with the stable ID "v1alpha1". From that single resource, approximately 12 config controllers fan out into typed sub-resources that downstream controllers can consume without parsing YAML:
MachineConfig YAML is parsed exactly once at the config plane boundary. Every downstream controller gets a typed, versioned Go struct — no YAML parsing deeper in the stack.This is the key architectural insight of Talos: structured config input is translated to typed resources at the edge, and the rest of the system never sees raw config again. The network controller doesn't know what YAML looks like. It watches network.LinkSpec resources. If the config plane correctly populated them, the network plane is correct.
For simple one-input → one-output transformations, Talos uses the transform.NewController helper from pkg/controller/generic. You provide a MapMetadataOptionalFunc (does the output exist?) and a TransformFunc (compute it) — the runtime loop is handled for you. Most config-plane controllers use this pattern: etcd.ConfigController, k8s.APIServerConfigController, and others. It's the idiomatic COSI "transform" controller from chapter 03, with the boilerplate collapsed.
// etcd.ConfigController uses the transform.Controller generic — no explicit Run() loop.
// The runtime wakes it when MachineConfig changes; TransformFunc does the work.
func NewConfigController() *transform.Controller[*config.MachineConfig, *etcd.Config] {
return transform.NewController(
transform.Settings[*config.MachineConfig, *etcd.Config]{
Name: "etcd.ConfigController",
MapMetadataFunc: func(cfg *config.MachineConfig) *etcd.Config {
return etcd.NewConfig(etcd.NamespaceName, etcd.ConfigID)
},
TransformFunc: func(ctx context.Context, r controller.Runtime, logger *zap.Logger,
cfg *config.MachineConfig, out *etcd.Config) error {
spec := out.TypedSpec()
spec.AdvertiseSubnets = cfg.Config().Cluster().Etcd().Subnet()
spec.ExtraArgs = cfg.Config().Cluster().Etcd().ExtraArgs()
// ... etc.
return nil
},
},
)
}
// When you need full control (multiple inputs, complex logic), plain Controller still applies.
// MachineTypeController shows the hand-rolled pattern.
func (ctrl *MachineTypeController) Inputs() []controller.Input {
return []controller.Input{{
Namespace: config.NamespaceName,
Type: config.MachineConfigType,
ID: optional.Some(config.ActiveID),
Kind: controller.InputWeak,
}}
}
func (ctrl *MachineTypeController) Outputs() []controller.Output {
return []controller.Output{{Type: config.MachineTypeType, Kind: controller.OutputExclusive}}
}
func (ctrl *MachineTypeController) Run(ctx context.Context, r controller.Runtime, _ *zap.Logger) error {
for {
select {
case <-ctx.Done(): return nil
case <-r.EventCh():
}
cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID)
// … write config.MachineType based on cfg.Config().Machine().Type()
}
}
#The network plane: spec/status at scale
The network plane is where COSI earns its keep most visibly in Talos — and it's the most developed network management stack you'll find built on this model. Every network concept has a strict Spec/Status split:
| Concept | Spec (desired) | Status (observed) | Who writes Status |
|---|---|---|---|
| IP address | network.AddressSpec | network.AddressStatus | AddressStatusController (netlink) |
| Network interface | network.LinkSpec | network.LinkStatus | LinkStatusController (netlink) |
| Routing table entry | network.RouteSpec | network.RouteStatus | RouteStatusController (netlink) |
| Hostname | network.HostnameSpec | network.HostnameStatus | HostnameStatusController (syscall) |
| DNS resolvers | network.DNSResolvConf | — | kernel writes /etc/resolv.conf |
| Node's external IPs | — | network.NodeAddress | NodeAddressController (derived) |
The Status controllers are wrappers around the Linux kernel's netlink API. They do one job: translate kernel state into COSI resources. They never read Spec resources. They are the ingest edge of the system — real-world facts flowing in. The applier controllers (e.g., AddressConfigController) do the other half: they read Spec, compare to Status, and call netlink to reconcile the difference. The two halves never directly coordinate — they talk only through shared resources.
#Multi-source merge: the network controller graph
The most sophisticated pattern in Talos's network plane is multi-source spec merging. A machine's IP address can come from three sources: the MachineConfig, a DHCP response, or the kernel cmdline. Each source is a separate controller that writes AddressSpec resources with a layer weight in the resource ID. A merge controller collects all the sources and produces the final desired spec.
AddressSpec resources (shared output) with priority-encoded IDs. The merge controller collects all and produces the winning desired state. The applier then reconciles Spec against Status using netlink.This is the shared output pattern from chapter 03 at full scale. Multiple controllers contribute to the same resource type without conflict, because each one owns distinct resource IDs (keyed by source and interface). The merge controller is the only one that reads across all of them.
#The runtime plane: services as resources
Every system service in Talos — kubelet, etcd, apid, cri, extension services — is managed by a COSI controller. The pattern is identical to the network plane:
- A config controller reads the relevant
MachineConfigsection and writes a typed config resource (e.g.,k8s.KubeletConfig,k8s.EtcdConfig). - A service controller reads that config resource and manages the actual system service via
go-runner(Talos's process runner). It starts, restarts, and stops the service based on resource phase changes. - A status controller reflects the service's actual health back as a
runtime.ServiceStatusor similar resource.
For example, the kubelet lifecycle: MachineConfig → k8s.KubeletConfig (config plane) → KubeletController starts the kubelet process → ServiceStatusController writes runtime.ServiceStatus{Running: true}. If the machine config changes (say, the kubelet extra args), the config plane updates k8s.KubeletConfig, which wakes the KubeletController, which restarts kubelet with the new args. The controller never parses YAML — it reads a typed Go struct.
func (ctrl *KubeletController) Inputs() []controller.Input {
return []controller.Input{
{Namespace: k8s.NamespaceName, Type: k8s.KubeletConfigType, Kind: controller.InputWeak},
{Namespace: secrets.NamespaceName, Type: secrets.KubernetesType, Kind: controller.InputWeak},
// also watches network.NodeAddress so it can update --node-ip on address change
{Namespace: network.NamespaceName, Type: network.NodeAddressType, Kind: controller.InputWeak},
}
}
func (ctrl *KubeletController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
for {
select {
case <-ctx.Done(): return nil
case <-r.EventCh():
}
kubeletCfg, _ := safe.ReaderGetByID[*k8s.KubeletConfig](ctx, r, k8s.KubeletID)
nodeAddresses, _ := safe.ReaderListAll[*network.NodeAddress](ctx, r)
// build the args list, reconcile against the running process
ctrl.runner.Reconcile(ctx, buildArgs(kubeletCfg, nodeAddresses))
}
}
Notice that KubeletController also watches network.NodeAddress — so when the machine's IP changes (say, a DHCP renewal), the kubelet is automatically restarted with the updated --node-ip flag. The network plane and the service plane are decoupled; they communicate through state. This is exactly the "no controller calls another controller" design principle from chapter 00, at OS scale.
#The Talos API: COSI state over gRPC
From the outside, the Talos API looks like a conventional gRPC service. Under the hood, apid is a thin auth layer on top of the COSI gRPC transport from chapter 05. When you run talosctl get addresses, here's what actually happens:
| talosctl command | COSI operation | What it returns |
|---|---|---|
get addresses | state.List(AddressStatusType) | All AddressStatus resources in the network namespace |
get links | state.List(LinkStatusType) | All LinkStatus resources |
get machineconfig | state.Get(MachineConfigType, "v1alpha1") | The active MachineConfig resource |
watch machineconfig | state.WatchKind(MachineConfigType) | Streaming change events as the config is updated |
apply-config | state.Update(MachineConfigType) | Triggers the config plane reconcile cascade |
get services | state.List(ServiceType) | All runtime.Service resources |
The COSI gRPC server in apid wraps the state.State with an access-control layer (AdminRole can read all namespaces; OS-role can only read public resources), then connects it directly to the gRPC State service defined in chapter 05. There is no translation layer, no separate API model — the external API is the resource model. This is what the README means by "the interface runs from local to gRPC without changing your controllers."
// apid registers the COSI gRPC State service using the same state.State
// that the controller runtime uses. External clients get a live, watched
// view of everything inside machined — with RBAC on top.
func (b *Backend) Register(server *grpc.Server) {
cosiv1.RegisterStateServer(server,
server_state.NewState(b.cosState, authz.NewRBACInterceptor(b.roles)),
)
machine.RegisterMachineServiceServer(server, &machineService{backend: b})
}
#Omni: distributed COSI at fleet scale
Sidero's Omni is the multi-node control plane that manages fleets of Talos machines. It illustrates the gRPC transport from chapter 05 at real-world scale: Omni maintains a persistent gRPC connection to each Talos node and aggregates their individual COSI states into a single federated view. From Omni's perspective, each node is a remote CoreState — reads and watches go over the wire, and Omni's own controllers react to changes on any of them.
What Omni's COSI runtime watches
Omni watches each node's network.NodeAddress, runtime.MachineStatus, config.MachineConfig, cluster.Member, hardware.SystemInformation, and health resources — across all managed nodes simultaneously. Change events from node A trigger Omni controllers that may update aggregated resources, kubeconfig secrets, or schedule a Talos upgrade.
What Omni writes back
Omni's controllers can write to a node's COSI state as well — most notably by pushing a new MachineConfig (which triggers the full node-side config plane cascade), or by writing cluster.Identity resources during bootstrap. The channel is bidirectional: Omni's state reads drive its own reconcile; its writes drive the node's reconcile.
#Lessons from a real OS
Talos is the longest-running COSI application. Several patterns appear here that you won't find spelled out in the library documentation:
One YAML parse, many typed consumers
Parse user input exactly once at the system boundary. Every downstream controller gets typed Go structs. This eliminates the "each subsystem interprets config differently" class of bugs. In Talos: MachineConfig → typed sub-resources → controllers that never see YAML.
Shared output + merge controller
When multiple sources can contribute to the same final state (static config + DHCP + cmdline for addresses), model each source as a shared-output writer and add a single merge controller that picks the winner by priority. No locks, no callbacks — just resource priority.
Status controllers as the ingest edge
Every fact from the outside world (kernel netlink, sysfs, process health) flows in through a dedicated Status controller that only writes. It never reads Spec resources. The separation is strict: the "what should be" graph and the "what is" graph share no writers.
Cross-plane watching
A service controller can watch a network resource (NodeAddress) — because the information it needs is already in state, not in a separate API call. This is the "controllers communicate only through state" principle delivering a concrete benefit: the kubelet restarts automatically on IP change with zero inter-controller coupling.
Namespace as trust boundary
The secrets namespace holds all TLS material. No controller outside the secrets subsystem has secrets as an output. The namespace boundary, enforced by the engine, is the security boundary. No special ACL required — the ownership rules are the ACL.
The API is the resource model
Talos chose to expose COSI state directly as its external API rather than building a translation layer. This paid off: external tools like Omni can read typed Talos resources with the same safe.StateGetByID they'd use internally. There's one model, everywhere.
#Verdict
Talos is not a case where COSI was evaluated and chosen — it is the case COSI was designed around. Every chapter of this walkthrough — resources, state, controllers, the runtime engine, gRPC distribution — maps to something Talos does in production at scale on real hardware. The patterns here (typed config fan-out, Spec/Status split, multi-source merge, namespace-as-trust-boundary, API-as-resource-model) aren't guidelines, they're evolved survival traits. If you're building anything that looks like "desired state in, real-world convergence out, observable from the outside" — this is the template. Read the Talos source the same way you'd read the Linux kernel for OS design patterns: it's the canonical application of everything the library is built to enable.
To apply these patterns to your own systems, see Chapter 06 for the decision framework and the domain-to-COSI mapping table.