Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ pkg/generated/** linguist-generated
# not hand-edited. Marking them generated keeps the rendered YAML dumps
# out of language stats and collapses them in PR diffs by default.
pkg/engine/testdata/golden/** linguist-generated

# docs/reference/ is rendered from the cobra command tree by
# `make docs`, not hand-edited. Marking it generated keeps it out of
# language stats and collapses it in PR diffs by default.
docs/reference/*.md linguist-generated
49 changes: 49 additions & 0 deletions .github/workflows/docs-validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Documentation Validation

# No paths filter, deliberately. A workflow skipped by a paths filter stays in
# a Pending check, and a merge blocked on a required check that never reports
# is blocked forever — so a filtered workflow can never be a required check at
# all. This build is the only thing that sees a relative link or an anchor
# inside docs/, so it has to be able to block. The cost is well under a minute
# on a Go-only change.
on:
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
validate:
name: Validate documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
# git-revision-date-localized runs in the build below and reads each
# page's last commit; a shallow clone has none.
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements-docs.txt

- name: Install dependencies
run: pip install --requirement requirements-docs.txt

# The site only builds for real on a push to main, where a failure lands
# on the published documentation. Build it here too: relative links and
# anchors inside docs/ are checked by this and nothing else.
#
# --quiet must never be added: --strict fails on the warning COUNT and
# --quiet suppresses the warnings before anything counts them, so the two
# together report success while checking nothing.
- name: Build documentation
run: mkdocs build --strict
78 changes: 78 additions & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Documentation

on:
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yml"
- "requirements-docs.in"
- "requirements-docs.txt"
- ".github/workflows/docs.yaml"
workflow_dispatch:

permissions:
contents: read

# Never cancel a deploy mid-flight: the site is replaced wholesale, so a
# cancelled run can leave the artifact half-uploaded. Queue instead.
concurrency:
group: pages
cancel-in-progress: false

jobs:
deploy:
name: Deploy documentation
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
# git-revision-date-localized reads each page's last commit for the
# footer date; a shallow clone has no history to read it from.
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements-docs.txt

# Provisions the Pages site when the repository has none yet, so the
# deploy below does not fail on a repository where Pages was never
# switched on. Idempotent once it exists. The custom domain is not
# expressible here and has to be set in the repository settings.
- name: Configure Pages
uses: actions/configure-pages@v6
with:
enablement: true

- name: Install dependencies
run: pip install --requirement requirements-docs.txt

# --strict fails the build on warnings; the validation block in
# mkdocs.yml is what makes a dead anchor, an unrecognized link and a page
# missing from the nav warnings at all. Both halves are needed.
- name: Build documentation
run: mkdocs build --strict

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: site

# Unlike a gh-pages branch push, this waits on the Pages deployment
# itself and fails if it does not converge, so a stalled deploy turns
# this job red instead of leaving a stale site behind a green run.
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
talm
dist/
.claude/

# MkDocs build output
/site/
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ TALOS_VERSION=$(shell go list -m github.com/siderolabs/talos | awk '{sub(/^v/,

build:
go build -ldflags="-X 'main.Version=$(VERSION)'"

# Rewrite docs/reference/ from the cobra command tree. TestReferenceDocs
# compares the committed pages against that tree on every `go test ./...`,
# so run this after adding a command or bumping Talos.
docs:
TALM_UPDATE_DOCS=1 go test . -run TestReferenceDocs

.PHONY: build docs
429 changes: 16 additions & 413 deletions README.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions docs/.nav.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Site navigation. This lives here rather than in mkdocs.yml because
# awesome-nav owns the nav tree, and only from here can the generated
# reference/ section expand through its own .nav.yml.
nav:
- Home: index.md
- Getting started:
- Installation: getting-started/installation.md
- Quickstart: getting-started/quickstart.md
- Initializing a project: getting-started/init.md
- Configuration:
- Node files: configuration/node-files.md
- Endpoints and VIPs: configuration/endpoints-and-vips.md
- Talos versions and output format: configuration/talos-versions.md
- Templates and values: configuration/templates.md
- Encryption: configuration/encryption.md
- Operations:
- Applying with side-patches: operations/side-patches.md
- Apply-time safety gates: operations/safety-gates.md
- talosctl-compatible commands: operations/talosctl-commands.md
- Upgrading talm: operations/upgrading.md
- CLI reference: reference
- Cozystack: https://cozystack.io
1 change: 1 addition & 0 deletions docs/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions docs/configuration/encryption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Encryption

Talm provides built-in encryption support using [age](https://age-encryption.org/) encryption. Sensitive files are encrypted with their values stored in SOPS format (`ENC[AGE,data:...]`), while YAML keys remain unencrypted for better readability.

## Encrypting files

To encrypt all sensitive files (secrets.yaml, talosconfig, kubeconfig):

```bash
talm init --encrypt
# or
talm init -e
```

This command will:
- Generate `talm.key` if it doesn't exist
- Encrypt `secrets.yaml` → `secrets.encrypted.yaml`
- Encrypt `talosconfig` → `talosconfig.encrypted`
- Encrypt `kubeconfig` → `kubeconfig.encrypted` (if exists)
- Encrypt `values-secret.yaml` → `values-secret.encrypted.yaml` (if exists)
- Update `.gitignore` with sensitive files

## Decrypting files

To decrypt all encrypted files:

```bash
talm init --decrypt
# or
talm init -d
```

This command will:
- Decrypt `secrets.encrypted.yaml` → `secrets.yaml`
- Decrypt `talosconfig.encrypted` → `talosconfig`
- Decrypt `kubeconfig.encrypted` → `kubeconfig` (if exists)
- Decrypt `values-secret.encrypted.yaml` → `values-secret.yaml` (if exists)
- Update `.gitignore` with sensitive files

## Encrypted user values

Beyond Talos' own PKI/tokens, you can store **arbitrary secret values that chart templates consume** (a registry password, a KMS plugin's secret-id, etc.) encrypted at rest with the same `talm.key`:

1. Author the secrets in plaintext `values-secret.yaml` (git-ignored), e.g.:

```yaml
registryPassword: hunter2
```

2. Encrypt them with `talm init --encrypt` → produces the committable `values-secret.encrypted.yaml` (per-value `ENC[AGE,...]` envelopes; keys stay readable).

3. Reference the **encrypted** file from `Chart.yaml`:

```yaml
templateOptions:
valueFiles:
- values-secret.encrypted.yaml
```

`talm template` and `talm apply` decrypt it **in memory** via `talm.key` — the plaintext never has to be present at render time. Both commands honor the full value-source set (`--values`, `--set`, `--set-string`, `--set-file`, `--set-json`, `--set-literal`) plus `templateOptions.*`, so a value renders identically whether you preview with `template` or push with `apply`.

Secret values are kept out of committed and printed output:

- `talm template -I` **omits** secret-bearing fields from the rendered `nodes/*.yaml`; the real value is re-injected only at `apply` (which re-renders from the encrypted file).
- `talm template` (stdout) and `talm apply` (drift preview) **redact** them to `***` by default. Reveal verbatim with `talm template --show-secrets` / [`talm apply --show-secrets-in-drift`](../operations/safety-gates.md) (debugging only).

!!! danger "Put the encrypted file in Chart.yaml, not only on the command line"

Reference the encrypted file from `Chart.yaml templateOptions.valueFiles` (as shown above), NOT only via `template --values`. The node-file modeline does not persist value files, so `apply` only re-reads what is in `Chart.yaml` (plus its own `--values`). If an encrypted file is passed solely to `template -I`, the omitted secret is absent from the node file AND never re-rendered at apply — silently lost from the applied config. `template -I` prints a warning when it omits secrets from a file that is not in `Chart.yaml`.

!!! warning "Encrypt only high-entropy values"

Sealing matches by exact value across the whole rendered config, so do not encrypt low-entropy values that collide with ordinary config strings (e.g. a bare port, or a password literally set to `controlplane`) — that unrelated field would be sealed too. Prefer high-entropy secrets. Secret values must be strings (quote them in `values-secret.yaml`); the encryption only covers string leaves.

## Key management

The `talm.key` file is generated in age keygen format and contains:
- Creation timestamp
- Public key (for sharing)
- Private key (keep secure!)

!!! danger "Back up talm.key"

Always backup your `talm.key` file! Without it, you won't be able to decrypt your encrypted secrets. The key file is automatically added to `.gitignore` to prevent accidental commits.

Encrypted files (`*.encrypted.yaml`, `*.encrypted`) can be safely committed to Git, while plain files (`secrets.yaml`, `talosconfig`, `kubeconfig`, `talm.key`) are ignored.
28 changes: 28 additions & 0 deletions docs/configuration/endpoints-and-vips.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Endpoints and VIPs

`values.yaml` carries three related network knobs: `endpoint` (the Kubernetes control-plane URL), `floatingIP` (the shared VIP address, if any), and `vipLink` (the link that VIP is pinned to). This page covers how they combine and what the chart derives when you leave them alone.

## Endpoint and floatingIP combinations

- **cozystack VIP setup**: set `endpoint` and `floatingIP` together to the same IP — single shared VIP.
- **single-node cluster**: set `endpoint` to the node's routable IP and leave `floatingIP` blank.
- **multi-node with external load balancer**: set `endpoint` to the LB URL and leave `floatingIP` blank.

## Automatic vipLink selection

When `vipLink` is left empty the chart picks the link automatically using a two-step rule:

1. **Longest-prefix match across configurable links.** If `floatingIP` falls inside the CIDR of any address on a configurable link (physical NIC, bond, VLAN, bridge), the most specific subnet wins. This handles the Hetzner-style topology where a public NIC carries the default route and a VLAN child carries the private cluster subnet — the VIP lands on the VLAN child.
2. **Fallback to the IPv4-default-gateway-bearing link.** Used when no configurable link's CIDR contains the `floatingIP` — typical for upstream-routable VIPs that arrive via the default route.

Addresses on links the chart does not emit a per-link document for (Wireguard, kernel-managed loopback, slave NICs of a bond, anything outside the configurable set) are skipped — a VIP pinned there would have no surrounding network document.

## Overriding vipLink

Set `vipLink` explicitly when the target link does not yet exist on the live system at first apply (typically a VLAN sub-interface). The chart pins `Layer2VIPConfig.link` to it directly and emits the document even on a fresh node where discovery has not yet populated the addresses table. The chart does not auto-emit a `LinkConfig` or `VLANConfig` for the override link; the operator is responsible for ensuring the link comes up, typically by adding a `LinkConfig` or `VLANConfig` for that link to the per-node body overlay alongside `vipLink`.

The [per-node body overlay](node-files.md) is the node file's own Talos config, below its modeline.

## Subnet selectors

Subnet-selector fields (`kubelet.validSubnets`, `etcd.advertisedSubnets`) are derived automatically from the node's default-gateway-bearing link, so no override is needed unless you have a multi-homed node that requires a specific subnet pinned.
Loading
Loading