Security & Trust
Talos has no shell, no SSH, no package manager — the only doors are the mTLS Talos API and Kubernetes. Behind those doors sit four layers of defense in depth, and every one of them is just COSI controllers reconciling machine config into resources: a private PKI minted entirely as in-memory resources, a KSPP-hardened kernel that refuses to boot without its safety params, a measured boot that extends PCR 11 through named phases and seals disk keys to a signed policy, and two WireGuard overlays — SideroLink for management, KubeSpan for the node mesh — both deriving their addresses from one ULA function. None of it is a daemon you configure; all of it is desired state that heals itself.
#Defense in depth: four layers, one engine
Most distros assemble security from a heap of independent tools — openssl for certs, sysctl.conf for hardening, a bootloader you sign by hand, a VPN daemon with its own config file. Talos collapses all of it into the same reconciliation model the runtime chapter describes: machine config and platform facts are inputs, COSI controllers are the loops, and resources are the outputs. A certificate is a resource. The security state of the box is a resource. A WireGuard peer is a resource. Because they are all reconciled rather than installed, they self-heal — a clock jump re-mints certs, a flapping link re-establishes a tunnel, a controller restart re-derives a CA.
Private PKI
Three independent root CAs (OS, Kubernetes, etcd) seeded from config secrets; leaf certs minted as resources and refreshed at 50% validity. Authority rides in the cert Organization field.
Hardened kernel
KSPP-required cmdline params enforced at boot, ~17 hardening sysctls, optional SELinux enforcing, module-signature enforcement, an auditd service.
Measured boot
Kernel + initrd + cmdline ship as a signed UKI; PCR 11 is extended through boot phases; disk-encryption keys are TPM-sealed to a signed PCR policy.
Two WireGuard overlays
SideroLink tunnels management traffic to Omni; KubeSpan is a full node mesh for cluster traffic with discovery-driven peers and NAT traversal.
#The secrets / PKI tree
The root of trust is the secrets: bundle in machine config — a set of CA cert+key pairs generated once at talosctl gen config time. Three transform controllers in internal/app/machined/pkg/controllers/secrets/root.go copy that material out of the parsed config and into three sensitive COSI resources, each the issuing authority for one trust domain:
RootOSController→OSRoot— the Talos API / trustd CA, plus accepted CAs, the joinToken, and SANs.RootKubernetesController→KubernetesRoot(control plane only) — the Kubernetes issuing CA, the aggregator CA, the service-account key, the bootstrap token, and the AESCBC/secretbox encryption secrets.RootEtcdController→EtcdRoot— the etcd CA.
The single most important invariant in the whole PKI lives in the OS root controller. A control-plane node holds the OS CA private key; a worker is handed only the CA certificate. The controller detects the difference by the empty key and drops the issuing CA entirely:
osSecrets.IssuingCA = cfgProvider.Machine().Security().IssuingCA()
if osSecrets.IssuingCA != nil {
osSecrets.AcceptedCAs = append(osSecrets.AcceptedCAs,
&x509.PEMEncodedCertificate{Crt: osSecrets.IssuingCA.Crt})
if len(osSecrets.IssuingCA.Key) == 0 {
osSecrets.IssuingCA = nil // worker: only the cert, not the key
}
}
Because a worker has IssuingCA == nil, it cannot sign its own leaf certificates. That forces every worker to route certificate requests through trustd on a control-plane node via CSR — which is exactly the property you want: a compromised worker holds no key that lets it impersonate the cluster's CA. The asymmetry isn't a special case bolted on; it falls out of one len(...Key) == 0 check.
#Leaf certs as resources, and the worker asymmetry
The leaf controllers are the interesting part. Each one mints certificates not as files on disk but as COSI resources, and each lists time.Status among its inputs so a clock jump or NTP sync immediately re-mints — certificates in Talos are ephemeral, regenerated at x509.DefaultCertificateValidityDuration / 2 (50% of validity) long before they expire.
| Controller | Output resource | Mints |
|---|---|---|
APIController | secrets.API | apid server cert + a client cert with Organization(os:impersonator) |
TrustdController | secrets.Trustd | trustd server cert (control plane only) |
EtcdController | secrets.Etcd | etcd server / peer / admin + kube-apiserver client certs |
KubernetesController | secrets.Kubernetes | apiserver / kubelet / admin / aggregator certs |
MaintenanceRootController | secrets.MaintenanceRoot | a one-shot self-signed CA for maintenance mode |
The apid certificate flow in controllers/secrets/api.go dispatches on machine type into three modes. A control-plane node self-signs from its local OS CA key. A worker assembles a CSR and ships it to trustd, which signs it with the CA key the worker never sees. An unknown-type node (one with no config yet) falls back to the throwaway maintenance CA with SkipVerifyingClientCert = true — maintenance mode is intentionally weak-auth, existing only to accept the very first config before any real PKI exists.
Authority itself is carried in the certificate's Organization field — role constants like os:admin, os:reader, os:impersonator from pkg/machinery/role/role.go. The API chapter covers how apid enforces those os: roles per RPC. Finally, every one of these resources is tagged meta.Sensitive, so talosctl get redacts their contents — you can see that a cert exists and when it refreshes, but not the private key inside.
Because leaf certs are resources reconciled from OSRoot + time.Status, there is no cert-rotation cron, no certbot, no manual renewal. The controller re-runs, notices it's past 50% validity (or that the clock jumped), and writes a fresh cert. Downstream consumers watch the resource and pick it up. Rotation is just reconciliation.
#Kernel hardening & KSPP
Talos follows the Kernel Self-Protection Project (KSPP) recommendations, and it does so with teeth: a small set of cmdline parameters is required, and the boot fails if they're absent. The list lives in pkg/kernel/kspp/kspp.go:
var RequiredKSPPKernelParameters = procfs.Parameters{
procfs.NewParameter("slab_nomerge").Append(""),
procfs.NewParameter("pti").Append("on"),
}
Treating these as a boot-time precondition rather than a best-effort default means you can't silently end up on a node where slab merging is on or page-table isolation is off — if the parameters didn't make it onto the cmdline, the machine refuses to come up. On top of the cmdline, GetKernelParams() applies roughly 17 hardening sysctls: kernel.kptr_restrict=2, kernel.dmesg_restrict=1, kernel.perf_event_paranoid=3, kernel.yama.ptrace_scope=2, user.max_user_namespaces=0, kernel.unprivileged_bpf_disabled=1, net.core.bpf_jit_harden=2, and the fs.protected_* family, among others.
Above the kernel knobs sit three more controls. SELinux can run in enforcing mode (gated on selinux.IsEnabled()). Module-signature enforcement rejects unsigned kernel modules. And auditd streams kernel audit events into the machine log. The SecurityStateController in controllers/runtime/security_state.go observes all of this and records it into a single runtime.SecurityState resource — SELinux Disabled/Permissive/Enforcing, ModuleSignatureEnforced, FIPS state, and the SecureBoot/UKI status we turn to next.
#SecureBoot & measured boot
The bootable artifact in a SecureBoot setup is a UKI — a Unified Kernel Image: the kernel, the initramfs, the cmdline, and a signed PCR policy bundled into one signed PE binary. Because the whole thing is one signature, firmware either trusts the entire boot payload or none of it; you cannot swap in a different initrd or cmdline without breaking the signature. SecurityStateController reports SecureBoot = efi.GetSecureBoot() && !efi.GetSetupMode() so you can tell genuine SecureBoot from a board left in setup mode.
Measured boot layers attestation on top of that. PCR 11 (constants.UKIPCR = 11) is the register Talos uses. systemd-stub measures the UKI's sections into it, and then Talos extends it with a phase string at each stage of boot: enter-initrd, leave-initrd (at switchroot), enter-machined, and finally start-the-world. Crucially, only enter-machined is signed — and disk decryption happens inside machined, so sealing the disk key to that phase guarantees the disk only unlocks from a correct, signed kernel/initrd/cmdline that has reached the right point in boot.
The machined side of the extend lives in internal/app/machined/pkg/controllers/hardware/pcr_status.go. It opens a TPM-unsealing window and then closes it again:
// case 0: open the TPM for disk encryption
if err := tpm2.PCRExtend(constants.UKIPCR, []byte(secureboot.EnterMachined)); err != nil {
return err
}
// ... after all volumes are Ready, case 1: lock it again
if err := tpm2.PCRExtend(constants.UKIPCR, []byte(secureboot.StartTheWorld)); err != nil {
return err
}
Read it as a bracket. machined extends with enter-machined, which puts PCR 11 into the exact value the disk-encryption policy was sealed against; it unlocks the encrypted volumes during that brief window; then it extends with start-the-world, which moves PCR 11 forward so the sealing policy is no longer satisfiable. Anything after the world starts — including a compromised workload — can no longer re-derive the unseal. The unlock window is one-shot and narrow by construction.
The seal itself (internal/pkg/secureboot/tpm2/seal.go) binds the key under a policy combining the live PCR-11 value and a signed public-key policy (PubKeyPCRs: []int{constants.UKIPCR}). The signature is precomputed at image-build time: internal/pkg/measure/measure.go runs pcr.CalculateBankData(...) to produce the tpm2-pcr-signature.json that ships inside the UKI. This is the design's cleverest move.
A new Talos version means a new UKI, which means a different PCR-11 value — under a naive "seal to the raw PCR value" scheme, every upgrade would lock you out of your own encrypted disk. Talos instead seals to a signed PCR-11 policy: the new UKI carries a fresh signature over the new expected PCR values, and the TPM accepts it because the public key hasn't changed. New UKI ⇒ new PCR value, but the signed policy still verifies ⇒ the upgrade is disk-safe. PCR 7 separately tracks SecureBoot/firmware-cert state. And the whole extend chain is best-effort: no TPM means the extends are silently skipped — enforcement comes from the sealing policy, not from the extend.
#SideroLink: the management overlay
The first of Talos's two WireGuard overlays is SideroLink — a point-to-point tunnel from each node back to a management plane (Omni/Sidero). siderolink.ManagerController (controllers/siderolink/manager.go) waits for network readiness, then calls the Provision API:
sideroLinkClient := pb.NewProvisionServiceClient(conn)
request := &pb.ProvisionRequest{ NodePublicKey: publicKeyString, /* ... */ }
return sideroLinkClient.Provision(ctx, request)
The server replies with its own WireGuard public key, its address, and the prefix the node is assigned inside the SideroLink ULA (purpose byte ULASideroLink = 0x03). The controller then writes a network.LinkSpec of kind wireguard plus an IPv6 network.AddressSpec, with a single peer — the server — whose AllowedIPs is just the server itself and whose PersistentKeepaliveInterval is 25s to hold NAT mappings open. Two transports are supported: native kernel WireGuard, or WireGuard-over-gRPC (siderolinktun) for environments where raw UDP is blocked. This is the same plumbing the networking chapter describes — a tunnel is just another LinkSpec reconciled into the kernel.
#KubeSpan: the node-to-node mesh
The second overlay, KubeSpan, is a full WireGuard mesh for cluster traffic, so pods on nodes separated by the public internet talk as if they were on one LAN. Two things make it tick: a deterministic identity and discovery-driven peers.
Every node generates a WireGuard keypair (cached in STATE as kubespan-identity.yaml) and then derives its mesh address deterministically from the cluster ID — no IPAM, no DHCP:
a.IdentitySpec.Subnet = network.ULAPrefix(clusterID, network.ULAKubeSpan)
a.IdentitySpec.Address, err = wgEUI64(a.IdentitySpec.Subnet, mac)
ULAPrefix in pkg/machinery/resources/network/ula.go is 0xfd + bytes 8–15 of sha256(clusterID), with byte 7 set to a purpose byte — yielding a stable RFC 4193 ULA per cluster and purpose. This is the unification worth underlining: both overlays share one derivation. KubeSpan uses ULAKubeSpan = 0x02; SideroLink uses ULASideroLink = 0x03. Same function, two purpose bytes, two non-overlapping address spaces, both deterministic.
Peers come from discovery, not from static config. peer_spec.go reads cluster.Affiliate resources that the discovery-service controllers populate — each node publishes its KubeSpan public key, address, and observed endpoints, encrypted before it leaves the node. The controller turns each affiliate into a peer spec:
*res.TypedSpec() = kubespan.PeerSpecSpec{
Address: spec.KubeSpan.Address,
AllowedIPs: ipSet.Prefixes(),
Endpoints: slices.Clone(spec.KubeSpan.Endpoints),
Label: spec.Nodename,
}
Building AllowedIPs isn't naive: the controller subtracts excluded networks and de-overlaps against peers already seen, so two affiliates can't claim conflicting routes. KubeSpan trusts the discovery service to carry peer data but not to keep it secret (affiliates are encrypted) and not to be correct about routing (the de-overlap is defensive).
The last piece is NAT traversal, handled in pkg/machinery/resources/kubespan/adapters/peer_status.go. Each peer runs an Up/Down/Unknown state machine driven by WireGuard's last-handshake time. When a peer goes Down, ShouldChangeEndpoint() fires and PickNewEndpoint() rotates to the next candidate, round-robin through the discovered endpoints until one handshakes — that's the whole NAT-traversal strategy. EndpointController then harvests the working endpoint of up peers and feeds it back to discovery so other nodes converge faster. manager.go programs the kernel WireGuard device and installs nftables chains (kubespan_prerouting, kubespan_outgoing) that fwmark cluster-destined traffic into the KubeSpan interface.
SideroLink is a single-peer star to a management plane; KubeSpan is an N-way mesh among cluster nodes. But both are reconciled the same way — derive a deterministic ULA from ULAPrefix(clusterID, purpose), write a LinkSpec + AddressSpec + peers, and let the kernel-facing controllers converge. No IPAM, no daemon config; addresses are a pure function of the cluster ID.
That closes the trust story: a PKI minted from config, a kernel that refuses to boot insecure, a boot measured into the TPM, and two overlays that need no addressing authority. The one operation that touches all of it at once is an upgrade — a new UKI, a new PCR value, new signed images swapped underneath a running cluster. Chapter 09 follows that artifact through the installer and imager to see how Talos replaces itself without breaking the disks, certs, and tunnels we just built.