Install Ichor with Helm
This guide installs Ichor itself with helm install and then registers an
existing Talos cluster with ichor adopt.
Status: built and hardware-verified (cases E1 to E7 on Talos v1.14 nodes, 2026-10-30). Not yet run on bare metal.
The chart installs Ichor, not a cluster: CRDs (kept on uninstall), the
controller and its RBAC, the webhooks, the UI and the optional Cilium policies.
Registering a cluster needs your talosconfig and secrets bundle, so that stays
with ichor adopt. The chart does not ship Cilium.
Where the chart lives
The chart is published by a workflow that runs when a v* tag is pushed, as a
Helm repository on the docs site and as an OCI artifact, with the images next
to it. Both hold the same signed archive:
helm repo add ichor https://ichor.maesh.dev/charts
helm repo update
oci://ghcr.io/maeshinshin/charts/ichor
No release has been tagged yet, so both have nothing in them today. Until the first release, install from a checkout:
helm install ichor ./dist/chart --namespace ichor-system --create-namespace \
--set manager.image.repository=REPO --set ichorUI.image.repository=REPO
The default image repositories are ghcr.io/maeshinshin/ichor-controller and
ghcr.io/maeshinshin/ichor-ui, which will not exist until the first release
either, so set your own. The chart version has no leading v (for example
0.1.0); appVersion and the image tags have it (v0.1.0). The published
images are linux/amd64 only. Helm is not provided by Ichor; install it
yourself.
Install
For a cluster that already has a pod network:
helm install ichor ichor/ichor --version X.Y.Z \
--namespace ichor-system --create-namespace
ichor adopt --cluster NAME --node ADDR --recover-secrets --recover-from ADDR
The OCI form installs the same chart without adding the repository:
helm install ichor oci://ghcr.io/maeshinshin/charts/ichor --version X.Y.Z ….
Values worth knowing (see dist/chart/values.yaml for all of them):
| Value | Default | Meaning |
|---|---|---|
networkPolicy.cilium.enabled | null | Ichor's CiliumNetworkPolicy objects (see Network policies). null renders them when the cluster serves Cilium's CiliumNetworkPolicy API, true renders them regardless (Cilium's CRD must exist), false renders none. |
networkPolicy.cilium.metricsNamespaces, metricsPodLabels | [], {} | Who may open the controller's metrics port. Empty: nobody. Name your Prometheus's namespace, and optionally its pod labels. |
preflight.cni | true | Refuse to install when every node is NotReady with the network-plugin message. Skipped by helm template and --dry-run. |
installer.enabled | false | Leave off: a Helm install is upgraded by Helm. |
ichorUI.port | 8443 | The HTTPS port of ichor-ui. |
ichorUI.tls.secretName | unset | Your own certificate for ichor-ui. Unset uses a leaf from the Ichor serving CA. |
ichorUI.tls.caConfigMap, caSecret, caKey | ichor-ui-ca, unset, ca.crt | The CA the ServiceMonitor (prometheus.enabled) verifies the HTTPS port against: the ConfigMap, or the Secret when caSecret is set. |
settings.readOnly | false | Hand the runtime settings to the chart: the IchorInstallation settings fields are ignored and the webhook denies writes. |
updateCheck.enabled | true | Look for a newer Ichor release in the chart repository and show it on the dashboard (see Settings). false stops the requests and removes their egress rule. |
Adopt does not apply the network policies on a Helm install. When they apply, it tells you to run:
helm upgrade ichor ichor/ichor --version X.Y.Z \
--namespace ichor-system --reuse-values --set networkPolicy.cilium.enabled=true
Network policies
On a cluster whose CNI is Cilium, Ichor's namespace is default-deny in both
directions with one allow per flow. The chart renders them
(networkPolicy.cilium.enabled), and ichor init and the installer Job apply
the same set on any other install. Nothing outside the cluster is allowed by a
fixed rule. What Ichor reaches is named by the controller, which keeps three
policies of its own from the objects that name them:
| Policy | Allows | Taken from |
|---|---|---|
ichor-controller-talos-egress | the controller and ichor-ui to Talos nodes and cluster endpoints, and the controller to S3 stores | the endpoints and addresses of TalosCluster, TalosMachine, MachineReclaim and TalosEtcdBackup |
ichor-controller-release-check | the controller to the chart and image registries of the release check | the repositories the controller is configured with; absent while updateCheck is off |
ichor-ui-oidc-egress | ichor-ui to its external OIDC issuer | ICHOR_OIDC_ISSUER or --oidc-issuer of the ichor-ui Deployment; absent without one |
Names are matched with Cilium's toFQDNs, which needs the DNS proxy: Cilium's
enable-l7-proxy must be on (it is in the Cilium Ichor installs) and the pods'
DNS goes through the rule to kube-dns that Ichor's policies carry. On a Cilium
you manage yourself with the L7 proxy off, a name is never allowed: set
networkPolicy.cilium.dynamicTalosEgress=false to fall back to world rules, or
turn Ichor's policies off with networkPolicy.cilium.enabled=false.
externalCIDRs and talosCIDRs add fixed addresses next to the named ones.
A new issuer on the ichor-ui Deployment reaches ichor-ui within about a minute.
Metrics. The controller's metrics port (8443) is closed to every pod until
you name a scraper. With the chart, set
networkPolicy.cilium.metricsNamespaces (and metricsPodLabels):
helm upgrade ichor ichor/ichor --version X.Y.Z \
--namespace ichor-system --reuse-values \
--set 'networkPolicy.cilium.metricsNamespaces={monitoring}' \
--set 'networkPolicy.cilium.metricsPodLabels.app\.kubernetes\.io/name=prometheus'
With ichor init or the installer Job there is no value to carry the
namespace; add a policy of your own, which adds to Ichor's (change the
namespace and labels to your Prometheus's, and the port if you changed it):
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-metrics-scrape
namespace: ichor-system
spec:
endpointSelector:
matchLabels:
control-plane: controller-manager
app.kubernetes.io/name: ichor
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: monitoring
k8s:app.kubernetes.io/name: prometheus
toPorts:
- ports:
- port: "8443"
protocol: TCP
The scraper also needs the metrics-reader ClusterRole. ichor-ui's /metrics
shares the port the UI is served on, so no network policy can separate it; it
answers only a caller whose token may get /metrics.
Verify a release
Every release image (ichor-controller, ichor-ui, ichor-installer) and the
chart is signed keylessly with cosign by the release workflow, and carries an
SPDX SBOM and a build provenance attestation. Verify by digest, with
the identity of this repository's workflow:
cosign verify ghcr.io/maeshinshin/ichor-controller:vX.Y.Z \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/maeshinshin/ichor/\.github/workflows/release\.yml@refs/tags/v.*$'
The same command verifies ichor-ui, ichor-installer and the chart
(ghcr.io/maeshinshin/charts/ichor:X.Y.Z). The provenance attestation is
checked with gh attestation verify oci://ghcr.io/maeshinshin/ichor-controller:vX.Y.Z --repo maeshinshin/ichor.
An Ichor upgrade made by the installer Job (ichor upgrade ichor) checks the
installer image's signature itself; see
The installer image on an upgrade. A Helm
install is upgraded with helm upgrade, so there verify the chart and images
yourself as above, or enforce signatures with an admission policy of your own.
The install manifest embedded in the release's installer image names
ichor-controller and ichor-ui by digest (ghcr.io/maeshinshin/ichor-controller:vX.Y.Z@sha256:...),
and the chart's default images carry their digests the same way, so the
installer a verified signature covers deploys exactly the images the release
built, not what a tag points at later.
The base image of each release is pinned by digest and named in the image's
org.opencontainers.image.base.digest annotation. The images are reproducible
from the tag: the same commit and the same Nix shell build the same binaries
(-trimpath, stripped, no build timestamps).
The installer image on an upgrade
ichor upgrade ichor VERSION makes the controller run a one-shot installer Job
whose ServiceAccount holds nearly cluster-admin rights, so the image it runs is
checked first. Before it creates the Job the controller:
- resolves
<installerRepository>:VERSIONto a digest (or takesspec.installerDigest, a pin you set with--installer-digest sha256:...), - verifies that digest's keyless cosign signature, offline against a Sigstore
trusted root built into the controller, for the identity
https://github.com/maeshinshin/ichor/.github/workflows/release.yml@refs/tags/VERSIONissued by GitHub Actions (the identity of thecosign verifycommand above), - runs the Job with the image
<installerRepository>@sha256:..., so what was verified is what runs. The API server refuses an installer Job that names a tag.
If the tag does not resolve or the signature does not verify, the operation is
paused with the reason InstallerImageUnresolved or InstallerImageUnverified,
no Job is created and nothing is applied. ichor operation get shows why.
After you fix the registry or the mirror, ichor operation resume NAME runs the check
again. The digest and the result are in ichor version and in
IchorInstallation status.installerImage.
Mirrors and air gaps. The signature belongs to the digest, not to a
registry, so a mirror verifies as long as it holds the image under the same
digest and the image's OCI referrers (copy with a tool that copies referrers,
for example oras copy --recursive; crane copy and docker push do not).
The controller reads installerRepository anonymously, over an egress rule it
maintains for that host (for ghcr.io also pkg-containers.githubusercontent.com,
where GHCR redirects blobs). No other service is contacted. A registry that
needs credentials or redirects blobs to a host that is not allowed has no way
through except the pin plus the recorded skip below.
Builds nobody signs. Development and demo images (v0.0.N-gSHA) carry no
signature. Skip the check, which Ichor records:
ichor upgrade ichor v0.0.1900000001-g1a2b3c4 --skip-signature-check
The plan prints a warning before you confirm, the controller emits a Warning
Event InstallerSignatureSkipped, status.installerImage.verification becomes
Skipped, ichor version says "signature NOT checked", and the
UI shows an "Installer not verified" badge on every page and an alert on the
cluster page. The skip is never inherited: the next ichor upgrade ichor
without the flag checks again (the CLI and the UI write the setting exactly as
given). There is no controller flag or chart value that turns the check off.
A fork that signs its own images has no way to name its own signer yet; it
skips the check the same way.
This protects the upgrade after the one that installs it: the first upgrade onto a release with this check is made by the previous controller, which creates the Job by tag. The controller and UI images that the installer then applies are still tags, not digests (residual risk).
Hardening the install
- Pod Security: the install manifest labels
ichor-systemwithpod-security.kubernetes.io/enforce: restricted(and audit and warn). Every Ichor pod meets that profile; the Cilium Helm Job needs the host network and runs inkube-system. The chart does not own the namespace, so a Helm install labels it once:kubectl label namespace ichor-system pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/audit=restricted pod-security.kubernetes.io/warn=restricted. - Priority: both pods run under the PriorityClass
ichor-critical(value 1000000), created by the manifest and by the chart (priorityClass.create). It ranks above ordinary workloads and below everysystem-*class, so the controller, the only writer to Talos nodes, is evicted last of the user workloads.system-cluster-criticalis not used: a user namespace may not use it by default. - Replicas: the raw install manifest runs one replica of each pod and has no
PodDisruptionBudget (a budget of
minAvailable: 1on one replica would block every node drain). The chart runs two replicas and ships the budgets. - Network: see Network policies: default-deny plus one
allow per flow, with no
worldegress left. - Releases are published only after the verify job passes, from a protected
releaseGitHub environment. Maintainers: create the environment with required reviewers and a tag ruleset forv*in the repository settings.
ichor-ui is HTTPS only
ichor-ui serves TLS on 8443 and no plain HTTP. The Service port is
named https. The certificate is yours (ichorUI.tls.secretName) or a leaf from
the Ichor serving CA, whose public part the controller publishes as the
ConfigMap and Secret ichor-ui-ca in ichor-system. How to put an Ingress, a
Gateway or a load balancer in front is in Users and login.
Moving an existing release:
- Plain HTTP edge: ichor-ui no longer serves plain HTTP. Re-point the edge at
https://<ichor-ui>:8443and trustichor-ui-cabefore you upgrade. The valueichorUI.plaintextPortwas removed; a values file that still sets it is ignored. - An install that already served TLS on 8080 sets
ichorUI.port=8080to keep its Service and edge as they are. - The ServiceMonitor verifies against
ichor-ui-caby default; override withichorUI.tls.caConfigMap,caSecretandcaKey. It never skips verification.
Settings on a Helm install
See Settings. settings.readOnly: true makes the
chart the only source.
A cluster with no CNI
Its nodes are NotReady, so the chart's pods could not start and helm install --wait would hang. The chart's preflight refuses to install and says so. The
sequence is:
ichor cni bootstrap --kubeconfig FILE --kube-proxy enabled|replaced [--tunnel vxlan|geneve] [--cilium-values FILE]
helm install ichor ichor/ichor --version X.Y.Z \
--namespace ichor-system --create-namespace
ichor adopt --cluster NAME --node ADDR --recover-secrets --recover-from ADDR
ichor cni bootstrapcreatesichor-systemif needed, installs the pinned Cilium as Ichor's own Helm release (releasecilium, records inichor-system), and waits for the nodes to be Ready. It asks for confirmation unless--yes. Running it again is safe.--kube-proxyis required:enabledif Talos runs kube-proxy,replacedif it is disabled in Talos. The command has no Talos credentials, so it cannot read this, and it cannot check KubePrism either. KubePrism must be enabled on port 7445 on every node (Talos's default); adopt checks it later.ichor adoptthen finds Cilium with an Ichor release record, and checks the tunnel and kube-proxy mode against it.
What adopt does on a Helm install is in Adopt a running cluster.
Upgrade and uninstall
Upgrade with helm upgrade; the CRDs are in the chart's templates and upgrade
with it. ichor commands that would run the installer Job refuse on a Helm
install and print the Helm command. The UI server's detection of a Helm install
is limited, so some of its upgrade prompts may still describe the installer
path. helm uninstall keeps the CRDs and therefore every cluster object, and
does not touch Cilium, which is a separate release.