Every Kubernetes node runs Linux. But most of those Linux installations are general-purpose operating systems — Ubuntu, CentOS, Debian — carrying decades of legacy: SSH daemons, package managers, shells, cron jobs, and hundreds of userspace binaries your Kubernetes workloads never need. Each of those components is an attack surface, a configuration drift opportunity, and a maintenance burden.
Talos Linux strips the OS down to exactly what Kubernetes requires and nothing more. There is no SSH. There is no shell. There is no package manager. The entire OS is read-only and immutable — every node boots from the same image, and configuration is applied via a typed API over mTLS. When something needs to change, you update a machine config and Talos applies it atomically, without any manual intervention on the node.
This guide covers Talos from first principles through production: the architecture, bootstrapping a cluster, machine configuration, day-2 operations like upgrades and scaling, and why the “no shell” constraint is a feature, not a limitation.
Why Talos Exists
The Problems with General-Purpose Linux for Kubernetes
Configuration drift: A node provisioned six months ago has accumulated manual changes — a sysctl tweak here, a kernel module loaded there, a version of containerd that doesn’t match the other nodes. Over time, nodes diverge from each other and from your documentation.
Attack surface: Every general-purpose Linux node has SSH (with all its key management complexity), a shell, curl, wget, python, and dozens of other tools. A container breakout or network intrusion gains access to a rich environment for lateral movement.
Upgrade complexity: Upgrading the OS on a Kubernetes node requires draining the node, SSHing in, running the package manager, potentially rebooting, validating the upgrade, and uncordoning. Multiply this by 20 nodes. Mistakes happen.
Accidental mutations: An operator SSHes into a node to debug something. They run a systemctl restart that wasn’t in the runbook. They install a package. They forget what they did. Six months later, that node behaves differently from the others and nobody knows why.
The Talos Philosophy
Talos is built around three constraints that eliminate these problems:
-
Immutable root filesystem: The OS is mounted read-only. You cannot install packages, write to /etc, or modify system binaries at runtime. Configuration changes go through the API.
-
No SSH, no shell: The attack surface shrinks to a single gRPC API endpoint over mTLS. There is no shell to spawn, no SSH daemon to compromise, no credentials to rotate.
-
Declarative machine configuration: Every aspect of a node’s configuration — network, storage, Kubernetes version, kernel flags, system extensions — is expressed in a machine config YAML applied via the API. The config is the source of truth.
Traditional K8s node: Talos node:
┌─────────────────────┐ ┌─────────────────────┐
│ Ubuntu 22.04 │ │ Talos Linux │
│ SSH daemon │ │ (immutable rootfs) │
│ bash, zsh, sh │ │ │
│ apt, dpkg │ │ machined (API) │
│ cron │ │ containerd │
│ systemd (full) │ │ kubelet │
│ Python, Perl, ... │ │ etcd (control plane)│
│ containerd │ │ │
│ kubelet │ │ Nothing else. │
│ ...200+ packages │ └─────────────────────┘
└─────────────────────┘
Architecture
Components
machined: The init process (PID 1) and API server. Everything that happens on a Talos node goes through machined — joining the cluster, applying config changes, rebooting, upgrading the OS. It exposes a gRPC API on port 50000.
apid: The API proxy that forwards talosctl commands to the right node. Runs on every node.
trustd: The PKI component that handles mutual TLS for the Talos API and Kubernetes.
containerd: The container runtime. Talos uses two containerd instances: one for system workloads (kubelet, etcd, kube-apiserver) and one for user workloads.
kubelet: The standard Kubernetes node agent.
The Boot Sequence
Power on
│
▼ UEFI/BIOS
Talos kernel + initramfs (from disk or network)
│
▼ machined starts (PID 1)
Machine config applied (from disk, URL, or metal network boot)
│
├── Network configured
├── Disks partitioned and formatted
├── containerd started
├── kubelet started
└── (control plane nodes) etcd started, kube-apiserver started
│
▼
Node joins Kubernetes cluster
Machine Config
Every Talos node is configured by a single YAML document called a machine config. It covers everything from network interfaces to Kubernetes API server flags to disk encryption. The config is cryptographically signed and validated; Talos rejects malformed configs before applying them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
# Example machine config structure (generated and then customized)
version: v1alpha1
debug: false
persist: true
machine:
type: controlplane # or: worker
token: <bootstrap-token>
ca:
crt: <base64-encoded-cert>
key: <base64-encoded-key>
certSANs: []
kubelet:
image: ghcr.io/siderolabs/kubelet:v1.30.1
extraArgs:
rotate-server-certificates: "true"
nodeIP:
validSubnets:
- 192.168.1.0/24
network:
hostname: control-1
interfaces:
- interface: eth0
addresses:
- 192.168.1.10/24
routes:
- network: 0.0.0.0/0
gateway: 192.168.1.1
dhcp: false
nameservers:
- 1.1.1.1
- 8.8.8.8
install:
disk: /dev/sda
image: ghcr.io/siderolabs/installer:v1.7.1
bootloader: true
wipe: false
sysctls:
net.ipv4.ip_forward: "1"
net.bridge.bridge-nf-call-iptables: "1"
features:
rbac: true
stableHostname: true
apidCheckExtKeyUsage: true
kubernetesTalosAPIAccess:
enabled: true
allowedRoles:
- os:reader
allowedKubernetesNamespaces:
- kube-system
cluster:
id: <cluster-id>
secret: <cluster-secret>
controlPlane:
endpoint: https://192.168.1.10:6443
clusterName: my-cluster
network:
dnsDomain: cluster.local
podSubnets:
- 10.244.0.0/16
serviceSubnets:
- 10.96.0.0/12
cni:
name: flannel # or: none (for Cilium, Calico, etc.)
token: <kubeadm-token>
secretboxEncryptionSecret: <encryption-secret>
ca:
crt: <base64-cert>
key: <base64-key>
apiServer:
image: registry.k8s.io/kube-apiserver:v1.30.1
extraArgs:
feature-gates: "ServerSideApply=true"
admissionPlugins:
- name: PodSecurity
configuration:
defaults:
enforce: restricted
enforce-version: latest
controllerManager:
image: registry.k8s.io/kube-controller-manager:v1.30.1
scheduler:
image: registry.k8s.io/kube-scheduler:v1.30.1
etcd:
image: gcr.io/etcd-development/etcd:v3.5.13
extraArgs:
election-timeout: "5000"
coreDNS:
image: registry.k8s.io/coredns/coredns:v1.11.1
proxy:
image: registry.k8s.io/kube-proxy:v1.30.1
disabled: true # Disable kube-proxy if using Cilium in eBPF mode
|
Getting Started: Bootstrapping a Cluster
Install talosctl
1
2
3
4
5
6
7
8
|
# macOS
brew install siderolabs/tap/talosctl
# Linux
curl -sL https://talos.dev/install | sh
# Verify
talosctl version --client
|
Generating Machine Configs
1
2
3
4
5
6
7
8
9
10
11
|
# Generate secrets and machine configs for a new cluster
talosctl gen config my-cluster https://192.168.1.10:6443 \
--output-dir ./cluster-config \
--install-disk /dev/sda \
--kubernetes-version 1.30.1
# This produces:
# cluster-config/
# controlplane.yaml — config for control plane nodes
# worker.yaml — config for worker nodes
# talosconfig — client credentials (like kubeconfig, but for talosctl)
|
Customizing with Patches
Rather than editing the generated YAML directly, apply patches — strategic merge patches or JSON patches layered on top:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
# patches/controlplane-common.yaml
# Applied to all control plane nodes
machine:
network:
interfaces:
- interface: eth0
dhcp: true
dhcpOptions:
routeMetric: 1024
sysctls:
net.ipv4.ip_local_port_range: "32768 60999"
fs.inotify.max_user_watches: "1048576"
kubelet:
extraMounts:
- destination: /var/local
type: bind
source: /var/local
options:
- bind
- rshared
- rw
features:
kubernetesTalosAPIAccess:
enabled: true
allowedRoles:
- os:reader
allowedKubernetesNamespaces:
- monitoring
cluster:
apiServer:
extraArgs:
audit-log-path: /var/log/audit/kube-apiserver-audit.log
audit-log-maxage: "30"
audit-log-maxbackup: "10"
audit-log-maxsize: "100"
etcd:
extraArgs:
listen-metrics-urls: http://0.0.0.0:2381
|
1
2
3
4
5
6
7
8
9
10
11
12
|
# patches/worker-node-1.yaml
# Node-specific overrides
machine:
network:
hostname: worker-1
interfaces:
- interface: eth0
addresses:
- 192.168.1.21/24
routes:
- network: 0.0.0.0/0
gateway: 192.168.1.1
|
1
2
3
4
5
6
7
8
9
10
11
|
# Re-generate with patches applied
talosctl gen config my-cluster https://192.168.1.10:6443 \
--config-patch @patches/controlplane-common.yaml \
--config-patch-control-plane @patches/controlplane-specific.yaml \
--config-patch-worker @patches/worker-common.yaml \
--output-dir ./cluster-config
# Or patch an individual node config
talosctl machineconfig patch ./cluster-config/controlplane.yaml \
--patch @patches/worker-node-1.yaml \
> ./cluster-config/worker-1.yaml
|
Booting Nodes
Option 1: ISO (physical or VM)
1
2
3
4
5
6
7
8
9
|
# Download the Talos ISO
curl -LO https://github.com/siderolabs/talos/releases/download/v1.7.1/talos-amd64.iso
# Boot from ISO, then apply config over the network
# The node starts in "maintenance mode" with an unauthenticated API
talosctl apply-config \
--nodes 192.168.1.10 \
--file ./cluster-config/controlplane.yaml \
--insecure # Required in maintenance mode (no mTLS yet)
|
Option 2: Metal network boot (iPXE)
1
2
3
4
5
6
7
8
9
10
11
12
|
# Serve the Talos kernel and initramfs over the network
# Nodes boot into Talos, pull config from a URL automatically
# Start a simple HTTP server serving the configs
python3 -m http.server 8080 --directory ./cluster-config
# iPXE script (served by your DHCP server)
# #!ipxe
# kernel https://factory.talos.dev/image/v1.7.1/kernel
# initrd https://factory.talos.dev/image/v1.7.1/initramfs.xz
# imgargs kernel talos.config=http://192.168.1.1:8080/controlplane.yaml
# boot
|
Option 3: Cloud (AWS, GCP, Azure, Hetzner)
1
2
3
4
5
6
|
# Talos maintains official AMIs for major cloud providers
# Get the AMI ID for your region
talosctl image list --region us-east-1 --arch amd64
# Use the AMI in your Terraform/Pulumi config
# User data = your machine config (base64 encoded)
|
Option 4: Docker (local development)
1
2
3
4
5
6
7
8
9
10
|
# Spin up a full Talos cluster in Docker — great for testing
talosctl cluster create \
--name test-cluster \
--controlplanes 3 \
--workers 2 \
--kubernetes-version 1.30.1 \
--install-image ghcr.io/siderolabs/installer:v1.7.1
# Destroy when done
talosctl cluster destroy --name test-cluster
|
Bootstrapping the Cluster
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# Configure talosctl to talk to your cluster
export TALOSCONFIG=./cluster-config/talosconfig
talosctl config endpoint 192.168.1.10
talosctl config node 192.168.1.10
# Wait for all control plane nodes to be in "maintenance" state
talosctl get members --insecure
# Bootstrap etcd (run ONCE on the FIRST control plane node only)
talosctl bootstrap --nodes 192.168.1.10
# Watch the cluster come up
talosctl dmesg --follow
# Once ready, get the kubeconfig
talosctl kubeconfig ./kubeconfig
export KUBECONFIG=./kubeconfig
# Verify
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# control-1 Ready control-plane 5m v1.30.1
# control-2 Ready control-plane 4m v1.30.1
# control-3 Ready control-plane 4m v1.30.1
# worker-1 Ready <none> 3m v1.30.1
# worker-2 Ready <none> 3m v1.30.1
|
Day-2 Operations
Applying Configuration Changes
When you need to change a node’s configuration — add a sysctl, change a network setting, update a kernel flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# Apply a config change to a specific node
# --mode=no-reboot: apply immediately without reboot (if possible)
# --mode=reboot: apply and reboot (required for some changes)
# --mode=staged: apply on next reboot
# --mode=auto: Talos picks the least disruptive mode
talosctl apply-config \
--nodes 192.168.1.21 \
--file ./cluster-config/worker-1.yaml \
--mode auto
# Apply to multiple nodes simultaneously
talosctl apply-config \
--nodes 192.168.1.21,192.168.1.22,192.168.1.23 \
--file ./cluster-config/worker.yaml \
--mode auto
# Check what changed (diff against current config)
talosctl get mc -o yaml | diff - ./cluster-config/worker-1.yaml
|
OS Upgrades
Talos upgrades are rolling, one node at a time. The upgrade process:
- Cordons and drains the node
- Downloads the new OS image
- Writes it to the EFI partition
- Reboots into the new image
- Validates the node is healthy
- Uncordons
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# Upgrade a single node
talosctl upgrade \
--nodes 192.168.1.10 \
--image ghcr.io/siderolabs/installer:v1.7.2 \
--wait # Block until upgrade is complete and node is healthy
# Upgrade all nodes (do control planes first, then workers)
for node in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo "Upgrading control plane: $node"
talosctl upgrade --nodes $node --image ghcr.io/siderolabs/installer:v1.7.2 --wait
done
for node in 192.168.1.21 192.168.1.22; do
echo "Upgrading worker: $node"
talosctl upgrade --nodes $node --image ghcr.io/siderolabs/installer:v1.7.2 --wait
done
# Upgrade Kubernetes version (separate from OS upgrade)
talosctl upgrade-k8s \
--nodes 192.168.1.10 \
--to 1.31.0
# Check upgrade status
talosctl get upgrades --nodes 192.168.1.10
|
Kubernetes Version Upgrades
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Check current versions across nodes
talosctl version --nodes 192.168.1.10,192.168.1.11,192.168.1.21
# Upgrade Kubernetes components to a new version
# Talos handles updating kube-apiserver, controller-manager, scheduler, kubelet
talosctl upgrade-k8s \
--nodes 192.168.1.10 \
--to 1.31.0
# talosctl will:
# 1. Check compatibility between Talos and the target K8s version
# 2. Update control plane components (apiserver, controller-manager, scheduler)
# 3. Update kube-proxy and CoreDNS
# 4. Update kubelet on all nodes
|
Adding and Removing Nodes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Add a new worker node
# 1. Boot the node from Talos ISO
# 2. Apply worker config
talosctl apply-config \
--nodes 192.168.1.23 \
--file ./cluster-config/worker-3.yaml \
--insecure
# The node automatically joins the cluster (no manual kubeadm join command)
kubectl get nodes # worker-3 appears when ready
# Remove a node gracefully
kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data
kubectl delete node worker-3
talosctl reset --nodes 192.168.1.23 --graceful # Wipes the node
|
Disaster Recovery: Etcd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Take an etcd snapshot (backup)
talosctl etcd snapshot ./etcd-backup-$(date +%Y%m%d).db \
--nodes 192.168.1.10
# Restore from snapshot (disaster recovery)
# 1. Boot a fresh single control plane node with the same config
# 2. Apply config with --insecure
# 3. Bootstrap with the backup
talosctl bootstrap \
--nodes 192.168.1.10 \
--recover-from=./etcd-backup-20260326.db
# Check etcd cluster health
talosctl etcd members
talosctl etcd status
|
Debugging Without SSH
The most common reaction to “no SSH” is “how do I debug anything?” Talos provides alternatives for every common debugging task.
Reading Logs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Kernel messages (dmesg)
talosctl dmesg --nodes 192.168.1.10
# Service logs (systemd-journal equivalent)
talosctl logs --nodes 192.168.1.10 machined
talosctl logs --nodes 192.168.1.10 kubelet
talosctl logs --nodes 192.168.1.10 etcd
talosctl logs --nodes 192.168.1.10 containerd
# Stream logs in real-time
talosctl logs --nodes 192.168.1.10 kubelet --follow
# Get all service logs at once
talosctl logs --nodes 192.168.1.10 "*"
|
Inspecting State
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# Node information
talosctl get systeminfo --nodes 192.168.1.10
talosctl get hardwareaddr --nodes 192.168.1.10
talosctl get hostname --nodes 192.168.1.10
# Network state
talosctl get addresses --nodes 192.168.1.10
talosctl get routes --nodes 192.168.1.10
talosctl get links --nodes 192.168.1.10
# Disk and mount state
talosctl get disks --nodes 192.168.1.10
talosctl get volumes --nodes 192.168.1.10
talosctl get mounts --nodes 192.168.1.10
# Kubernetes-specific
talosctl get nodenames --nodes 192.168.1.10
talosctl get kubeletspec --nodes 192.168.1.10
# Resource usage (CPU, memory)
talosctl stats --nodes 192.168.1.10
# Running containers
talosctl containers --nodes 192.168.1.10 # System containers
talosctl containers --nodes 192.168.1.10 -k # Kubernetes containers (all namespaces)
|
Executing Commands in Containers
When you need to run commands for debugging, you can exec into containers:
1
2
3
4
5
6
|
# Exec into a running container (works for both system and user containers)
talosctl exec --nodes 192.168.1.10 -c etcd -- etcdctl endpoint health
talosctl exec --nodes 192.168.1.10 -c kubelet -- crictl ps
# Or use kubectl exec for user workload pods
kubectl exec -it my-pod -- /bin/sh
|
The Maintenance Console
When a node can’t join the cluster (boot failure, network misconfiguration), Talos exposes an unauthenticated API in maintenance mode. You can read logs and apply config remotely:
1
2
3
4
5
|
# Connect to a node in maintenance mode
talosctl --talosconfig /dev/null dmesg --nodes 192.168.1.10 --insecure
# Apply corrected config
talosctl apply-config --nodes 192.168.1.10 --file fixed-config.yaml --insecure
|
Interactive Shell (When You Really Need It)
Talos doesn’t include a shell, but you can run a privileged debug container:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# Run a debug pod on a specific node (uses the node's host network/PID namespace)
kubectl debug node/worker-1 -it --image=ubuntu -- bash
# Or use nsenter via a privileged container to access the host namespaces
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: debug-host
namespace: kube-system
spec:
nodeName: worker-1
hostPID: true
hostNetwork: true
hostIPC: true
containers:
- name: debug
image: ubuntu:22.04
command: ["sleep", "infinity"]
securityContext:
privileged: true
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /
EOF
kubectl exec -it -n kube-system debug-host -- bash
# nsenter -t 1 -m -u -i -n -p -- bash # Enter host namespaces
|
System Extensions
Talos is minimal by design, but system extensions let you add kernel modules and system packages to the base image — things like GPU drivers, iSCSI tools, or Wireguard — without breaking immutability.
Extensions are baked into the installer image at build time, not installed at runtime.
Finding and Using Extensions
1
2
3
4
5
|
# Browse available extensions
talosctl image list --talos-version v1.7.1
# Or visit the Talos image factory: factory.talos.dev
# Select your extensions, generate a custom installer URL
|
Building a Custom Image with the Image Factory
The Talos Image Factory generates custom installer images with extensions baked in:
1. Go to factory.talos.dev
2. Select Talos version: v1.7.1
3. Select extensions:
- siderolabs/nvidia-container-toolkit (GPU support)
- siderolabs/iscsi-tools (for Longhorn storage)
- siderolabs/util-linux-tools (mount utilities)
4. Get a custom schematic ID
5. Use the custom installer URL in your machine config
1
2
3
4
5
|
# machine config using a custom installer image
machine:
install:
image: factory.talos.dev/installer/YOUR_SCHEMATIC_ID:v1.7.1
disk: /dev/sda
|
1
2
|
# Or build extensions locally with the Talos SDK
# and push to your own registry
|
Common Extensions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# GPU nodes (NVIDIA)
# In your worker machine config:
machine:
install:
image: factory.talos.dev/installer/SCHEMATIC_WITH_NVIDIA:v1.7.1
kernel:
modules:
- name: nvidia
- name: nvidia_uvm
- name: nvidia_drm
- name: nvidia_modeset
# After the node boots, deploy the NVIDIA device plugin:
helm install nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--set runtimeClassName=nvidia
|
Storage
Talos itself is stateless — the OS partition is ephemeral. Data persistence comes from:
- External storage (NFS, Ceph, cloud block storage)
- Local node storage via a separate data partition managed by Talos
- Distributed storage running in Kubernetes (Longhorn, Rook-Ceph, OpenEBS)
Longhorn (Recommended for Most Clusters)
1
2
3
4
5
6
|
# patches/longhorn.yaml — configure the data disk for Longhorn
machine:
disks:
- device: /dev/sdb # Second disk dedicated to Longhorn
partitions:
- mountpoint: /var/lib/longhorn
|
1
2
3
4
5
6
|
# Install Longhorn
helm repo add longhorn https://charts.longhorn.io
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace \
--set defaultSettings.defaultDataPath=/var/lib/longhorn
|
Local Path Provisioner
For simpler setups, use local path provisioner with Talos’s data partition:
1
2
3
4
5
6
7
8
9
10
11
|
# machine config: expose a directory for local storage
machine:
kubelet:
extraMounts:
- destination: /var/local/storage
type: bind
source: /var/local/storage
options:
- bind
- rshared
- rw
|
Managing Talos clusters declaratively with Terraform (using the talos provider):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# terraform/main.tf
terraform {
required_providers {
talos = {
source = "siderolabs/talos"
version = "0.5.0"
}
proxmox = {
source = "bpg/proxmox"
version = "0.57.0"
}
}
}
# Generate cluster secrets (run once, store in state)
resource "talos_machine_secrets" "this" {}
# Generate machine configs
data "talos_machine_configuration" "controlplane" {
cluster_name = "prod-cluster"
cluster_endpoint = "https://192.168.1.10:6443"
machine_type = "controlplane"
machine_secrets = talos_machine_secrets.this.machine_secrets
config_patches = [
yamlencode({
machine = {
install = {
disk = "/dev/sda"
image = "ghcr.io/siderolabs/installer:v1.7.1"
}
network = {
nameservers = ["1.1.1.1", "8.8.8.8"]
}
}
cluster = {
network = {
podSubnets = ["10.244.0.0/16"]
serviceSubnets = ["10.96.0.0/12"]
}
}
})
]
}
data "talos_machine_configuration" "worker" {
cluster_name = "prod-cluster"
cluster_endpoint = "https://192.168.1.10:6443"
machine_type = "worker"
machine_secrets = talos_machine_secrets.this.machine_secrets
}
# Apply config to control plane nodes
resource "talos_machine_configuration_apply" "controlplane" {
count = 3
client_configuration = talos_machine_secrets.this.client_configuration
machine_configuration_input = data.talos_machine_configuration.controlplane.machine_configuration
node = "192.168.1.1${count.index}"
config_patches = [
yamlencode({
machine = {
network = {
hostname = "control-${count.index + 1}"
interfaces = [{
interface = "eth0"
addresses = ["192.168.1.1${count.index}/24"]
routes = [{ network = "0.0.0.0/0", gateway = "192.168.1.1" }]
}]
}
}
})
]
}
# Bootstrap the cluster
resource "talos_machine_bootstrap" "this" {
depends_on = [talos_machine_configuration_apply.controlplane]
node = "192.168.1.10"
client_configuration = talos_machine_secrets.this.client_configuration
}
# Output the kubeconfig and talosconfig
data "talos_cluster_kubeconfig" "this" {
depends_on = [talos_machine_bootstrap.this]
client_configuration = talos_machine_secrets.this.client_configuration
node = "192.168.1.10"
}
output "kubeconfig" {
value = data.talos_cluster_kubeconfig.this.kubeconfig_raw
sensitive = true
}
output "talosconfig" {
value = talos_machine_secrets.this.client_configuration
sensitive = true
}
|
Security Model
Talos’s security posture is significantly stronger than general-purpose Linux nodes by default:
What You Get Out of the Box
✓ No SSH daemon (attack surface eliminated entirely)
✓ No shell (no /bin/sh, /bin/bash — nothing to spawn)
✓ Immutable rootfs (cannot write to OS directories)
✓ Signed OS images (verified at boot)
✓ mTLS for all Talos API communication
✓ Kubernetes API served with proper RBAC
✓ Audit logging for all Talos API calls
✓ Secureboot support (TPM 2.0 disk encryption)
✓ AppArmor enabled by default
✓ No package manager (no supply chain for OS packages)
Disk Encryption
Talos supports TPM-bound disk encryption (STATE and EPHEMERAL partitions):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
machine:
systemDiskEncryption:
state:
provider: luks2
options:
- no-read-workqueue
- no-write-workqueue
keys:
- slot: 0
tpm: {} # TPM 2.0-bound key (node-specific)
ephemeral:
provider: luks2
keys:
- slot: 0
tpm: {}
|
With TPM binding, the encryption key is sealed to the TPM and only released when the system boots into the expected measured boot state. If someone removes the disk and puts it in another machine, the TPM won’t release the key.
Audit Logs
1
2
3
4
5
6
7
8
9
10
|
# Talos logs all API calls
talosctl logs machined | grep "audit"
# Forward audit logs to your SIEM via a DaemonSet that reads from the node
# or configure remote syslog:
machine:
logging:
destinations:
- endpoint: tcp://syslog.internal:514
format: json_lines
|
Talos vs Other Kubernetes-Focused OSes
| Feature |
Talos |
Flatcar Container Linux |
Bottlerocket (AWS) |
K3OS |
| Shell |
None |
Bash (limited) |
Bash (via host containers) |
Bash |
| SSH |
None |
Yes |
Via host containers |
Yes |
| Package manager |
None |
None (update-engine) |
None |
None |
| Configuration API |
gRPC (talosctl) |
Ignition (apply at boot) |
API (apiclient) |
kubectl |
| Kubernetes versions |
All |
All |
Limited (Amazon-maintained) |
K3s only |
| Multi-cloud |
Yes |
Yes |
AWS-focused |
Yes |
| Update model |
Rolling via API |
Atomic image swap |
Automatic via SSM |
Manual |
| Immutable root |
Yes |
Yes |
Yes |
Partial |
| Maintained by |
Sidero Labs |
Flatcar Project |
AWS |
Rancher |
talosctl Quick Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
# Cluster management
talosctl gen config <name> <endpoint> # Generate machine configs
talosctl bootstrap # Bootstrap etcd (once per cluster)
talosctl kubeconfig # Get kubeconfig
talosctl cluster create # Create Docker cluster (local dev)
talosctl cluster destroy # Destroy Docker cluster
# Node management
talosctl apply-config --nodes <ip> --file <config.yaml>
talosctl upgrade --nodes <ip> --image <installer-image>
talosctl upgrade-k8s --nodes <ip> --to <version>
talosctl reset --nodes <ip> # Wipe node
# Inspection
talosctl version --nodes <ip>
talosctl get members
talosctl get systeminfo --nodes <ip>
talosctl get addresses --nodes <ip>
talosctl containers --nodes <ip> # System containers
talosctl containers --nodes <ip> -k # Kubernetes containers
talosctl stats --nodes <ip> # Resource usage
# Logs
talosctl dmesg --nodes <ip>
talosctl logs --nodes <ip> <service>
talosctl logs --nodes <ip> kubelet --follow
# Etcd
talosctl etcd members
talosctl etcd status
talosctl etcd snapshot <file> # Backup
talosctl etcd alarm list # Check for etcd alarms
# Configuration
talosctl get mc --nodes <ip> # Current machine config
talosctl edit mc --nodes <ip> # Edit machine config in $EDITOR
talosctl get machineconfig --nodes <ip> -o yaml
|
When to Choose Talos
Talos is the right choice when:
- Security posture matters and you want to minimize attack surface
- You want truly reproducible, drift-proof node configuration
- Your team is comfortable with the “no shell” constraint and
talosctl
- You’re building a new cluster from scratch (retrofitting is harder)
- You want OS upgrades to be as easy as Kubernetes upgrades
- You’re building a platform that will be operated by multiple people and want strong operational consistency
Stick with a general-purpose Linux when:
- You have existing automation (Ansible playbooks, Chef cookbooks) that SSH into nodes
- Your team is not yet comfortable with Talos’s debugging model
- You need to run workloads that require host-level customization beyond what extensions support
- You’re on a managed Kubernetes service (EKS, GKE, AKS) — you don’t control the OS anyway
The learning curve is real — spending your first day wondering “but how do I SSH in?” is normal. Once you internalize that talosctl replaces SSH for every legitimate operational task, the model clicks, and the security and operational consistency benefits compound over time.
Filed under: Modern Infrastructure Patterns
Comments