Talos Linux

The cluster runs Talos Linux v1.14.0 with Kubernetes v1.36.1. Talos is a purpose-built, immutable operating system for Kubernetes: it has no shell, no SSH, and no package manager, and all management goes through its gRPC API.

The node lifecycle (render, apply, upgrade, reset, bootstrap) is managed with topf. talosctl is only used for read-only diagnostics.

Why Talos

PropertyBenefit
ImmutableThe OS is read-only. No drift, no manual changes, no configuration surprises.
API-drivenAll operations go through the Talos API. Infrastructure is code, not a series of SSH commands.
Minimal attack surfaceNo shell, no SSH, no unnecessary services. The only way in is the API.
DeclarativeMachine configs are YAML documents that describe the desired state of each node.
Atomic upgradesUpgrades swap the entire OS image atomically. Rollback is automatic on failure.

Layout

Everything for the cluster lives in talos/pitower/:

talos/pitower/
├── topf.yaml            # cluster name, endpoint, versions, schematic, node list
├── secrets.sops.yaml    # cluster secrets (CA, tokens), SOPS + age
├── all/                 # patches for every node
│   ├── 01-general.yaml        # kubelet, containerd, host DNS, sysctls
│   ├── 02-hostname.yaml.tpl   # hostname from topf.yaml
│   └── 03-network.yaml.tpl    # VLAN 20, VIP, dual-stack subnets
├── control-plane/       # patches for worker-01..03
│   ├── 01-cluster.yaml        # etcd, API server, reserved resources, CNI/kube-proxy off
│   └── 02-uinput.yaml         # /dev/uinput for Sunshine
├── node/<host>/         # per-node patches (install disk, labels, taints, volumes)
├── extensions/          # Image Factory schematics
├── addons/              # Cilium + kubelet-csr-approver, applied at bootstrap
└── justfile             # recipes, imported from ../talos.justfile

topf.yaml

topf.yaml is the single source for the cluster definition:

talos/pitower/topf.yaml (excerpt)
clusterName: pitower
clusterEndpoint: https://10.20.10.0:6443
kubernetesVersion: 1.36.1
talosVersion: 1.14.0
schematicId: "@extensions/amd.yaml"
secretsPath: secrets.sops.yaml

nodes:
  - host: worker-01
    ip: 10.20.10.1
    role: control-plane
    data:
      mac: "1c:83:41:40:88:41"
      lanLinks: [enp4s0]
  - host: worker-04
    ip: 10.20.10.4
    role: worker
    schematicId: "@extensions/intel.yaml"
    ...

Each node has a host name, IP, role, an optional schematicId override, and free-form data (MAC address, untagged-LAN links, untagged: true for worker-07) that the .tpl patches read. topf decrypts secrets.sops.yaml itself through sops, using SOPS_AGE_KEY_FILE.

Factory Schematics

Each node type boots a custom image from the Image Factory. The schematic YAML in extensions/ declares the system extensions and overlays; topf resolves @extensions/<file>.yaml to a schematic ID locally.

SchematicNodesContents
amd.yaml (cluster default)worker-01..03util-linux-tools, amd-ucode, amdgpu-firmware, amdgpu, uinput
intel.yamlworker-04..06util-linux-tools, i915-ucode, intel-ucode
r630.yamlworker-07util-linux-tools, intel-ucode, zfs
rpi-poe.yamlworker-08..10sbc-raspberrypi overlay (rpi_generic) with PoE HAT fan settings, util-linux-tools
nvidia.yamlworker-ai-01util-linux-tools, amd-ucode, nvidia-open-gpu-kernel-modules-production, nvidia-container-toolkit-production

Patch Layering

Talos 1.14 uses multi-document machine configs. topf runs talosctl gen config against each node's running version, then layers the patches in order: all/, then control-plane/ (control planes only), then node/<host>/. Files ending in .tpl are Go templates rendered with the node's entry from topf.yaml.

flowchart TD
    A[topf.yaml + secrets.sops.yaml] --> B[talosctl gen config]
    B --> C[all/*]
    C --> D{role}
    D -->|control-plane| E[control-plane/*]
    D -->|worker| F[node/&lt;host&gt;/*]
    E --> F
    F --> G[Machine config per node]

To inspect the result without applying anything:

bash
just talos pitower render   # full configs to output/
just talos pitower diff     # pending changes per node (topf apply --dry-run)

Key Patches

All Nodes (all/)

DocumentSettingPurpose
KubeletConfigrotate-server-certificatesKubelet serving certs, approved by kubelet-csr-approver
KubeletConfigImage GC at 60% / 50%, imageMaximumGCAge: 168hBounds the image cache by age as well as disk usage
CRICustomizationConfigdiscard_unpacked_layers, unprivileged ports/ICMP, device ownership from security contextHalves image storage; changing it reboots every node
ResolverConfighostDNS with resolveMemberNamesTalos host DNS; CoreDNS does not forward to it
SysctlConfignet.ipv6.conf.all.forwarding: 1IPv6 forwarding for dual-stack pod traffic
SecurityProfileConfigworkloadIsolation: falseOpts out of 1.14 workload isolation until host-reaching workloads are verified
HostnameConfighostname: {{ .Node.Host }}Hostname from topf.yaml
machine.network.interfacesVLAN 20 subinterface with DHCP, VIP 10.20.10.0 on control planesworker-07 (untagged: true) takes DHCP on the parent link instead
KubeNodeConfigvalidSubnets: 10.20.0.0/16, 2a02:16a:2a0a:2::/64Picks the VLAN 20 addresses as node IPs
KubeNetworkConfigPods 10.244.0.0/16 + fd10:244::/56, services 10.96.0.0/12 + fd10:96::/112Dual-stack, IPv4 first
SysctlConfigdisable_ipv6 on lanLinksStops a second IPv6 default route via the untagged LAN

Control Plane (control-plane/)

DocumentSettingPurpose
cluster.etcdlisten-metrics-urls: http://0.0.0.0:2381etcd metrics for kube-prometheus-stack
KubeletConfigsystemReserved, kubeReserved, evictionHardHeadroom so app spikes cannot starve the API server
KubeNodeConfigDelete the node-role.kubernetes.io/control-plane taintControl planes also run workloads
KubeCoreDNSConfigenabled: falseCoreDNS is deployed by its Helm chart (kube-system/coredns)
KubeTalosAPIAccessConfigos:operator for namespace systemLets the etcd-defrag CronJob talk to the Talos API
KubeAPIServerConfigCert SANs 10.20.10.0, 127.0.0.1; service account issuer on GitHubService account OIDC discovery served from the repo (pitower/kubernetes/openid)
KubeAuthenticationConfigJWT issuer https://idm.wibrow.dev/oauth2/openid/headlampHeadlamp SSO via Kanidm
KubeControllerManagerConfig, KubeSchedulerConfigbind-address: 0.0.0.0Metrics scraping
KubeFlannelCNIConfig, KubeProxyConfigDeleted / disabledCilium is the CNI and kube-proxy replacement
KernelModuleConfig, UdevRulesConfiguinput, mode 0666Input devices for Sunshine in dev/dev-desktop

Per-Node Patches

Patches in node/<host>/ apply to one node. Every node has an UnattendedInstallConfig selecting its install disk, plus whatever is specific to it:

NodeInstall diskExtras
worker-01..03NVMe (disk.model starts with AirDisk)Label feature.node.kubernetes.io/amd-gpu
worker-04eMMC (/dev/mmcblk0)Label intel-gpu, taint dedicated=media-home:NoSchedule
worker-05, worker-06SAMSUNG modelLabel intel-gpu
worker-07md RAID1 boot (two SSDs by WWID)raid1 and zfs modules (ARC capped at 64 GiB), maxPods: 250, X710 ring sizes
worker-08..10Disk of 100 GB or more (the USB SSD, not the SD card)None
worker-ai-011 TB NVMeLabel nvidia-gpu, EPHEMERAL capped at 200 GiB, NVIDIA and VFIO modules, models user volume, taint dedicated=gpu:NoSchedule, ignores the Bazzite NIC

Diagnostics

talosctl needs a talosconfig, which topf generates:

bash
just talos pitower talosconfig         # writes talos/pitower/output/talosconfig
just talos pitower members             # cluster members
just talos pitower health              # talosctl health
just talos pitower services 10.20.10.7 # services on one node