LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Talos Linux: The Immutable, API-Driven Kubernetes OS with No SSH

taloskuberneteslinuximmutable-infrastructuresecurityhomelabplatform-engineering

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:

  1. 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.

  2. 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.

  3. 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:

  1. Cordons and drains the node
  2. Downloads the new OS image
  3. Writes it to the EFI partition
  4. Reboots into the new image
  5. Validates the node is healthy
  6. 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:

  1. External storage (NFS, Ceph, cloud block storage)
  2. Local node storage via a separate data partition managed by Talos
  3. Distributed storage running in Kubernetes (Longhorn, Rook-Ceph, OpenEBS)
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

Cluster Configuration with Terraform

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