Cloud

Install K3s on Ubuntu 24.04 (Noble Numbat)

K3s puts a working Kubernetes node on Ubuntu 24.04 in under a minute, so the install is the easy part. What bites comes after it. The kubeconfig is readable only by root, UFW rules do not cover the ports you think they cover, and the bundled Traefik chart renamed a provider key and ignores the old spelling without a word.

Original content from computingforgeeks.com - post 155715

Here you get the single-node install. You install K3s on Ubuntu 24.04 from the stable channel, check the bundled components and run kubectl as a normal user. Then you put a test app behind Traefik and turn on secrets encryption on the running server. A worker join is covered briefly, and the full multi-node build lives in our K3s cluster guide for Ubuntu 24.04. Every command below was run on Ubuntu 24.04 with K3s v1.36.5+k3s1 from the stable channel in November 2026, and the output blocks come from that lab.

Check what you need

K3s needs at least 2 CPU cores and 2 GB of RAM for a server, and an agent needs 1 core and 512 MB. We gave the server VM 4 GB because the bundled add-ons settle at about 1.2 GB used system-wide once everything is up, which leaves a 2 GB box very little room for your own workloads. You need:

  • Ubuntu 24.04 server with a static or reserved IP
  • 2 vCPU, 4 GB RAM, 20 GB disk for the server
  • A user with sudo and outbound HTTPS to github.com

The lab used two VMs on the same LAN: k3s01 as the server and k3s02 as the optional worker. Everything on the server runs from one k3s binary, and only ports 6443, 80 and 443 matter to clients:

K3s on Ubuntu 24.04 architecture diagram: admin and the k3s02 agent reach the k3s server on 6443, the server keeps state in SQLite via kine and runs containerd, Traefik with ServiceLB serves 80/443 on 192.168.1.174 and routes Ingress to the app pods, and the agent uses VXLAN 8472/udp

Patch the box before you start, because the installer pulls nothing from apt and will not do it for you. Our clone moved from 24.04.4 to 24.04.5 during this upgrade, which is why the server and the worker report different point releases later on. Run the upgrade now.

sudo apt update && sudo apt -y upgrade

Reboot if a new kernel landed. A reboot now costs nothing, because nothing runs on the box yet. Once workloads land, the same reboot means draining the node first, so take it while it is free.

Open the firewall with UFW

UFW ships inactive on Ubuntu 24.04, and the K3s docs recommend leaving it off. If you want it on, these sources and ports must be allowed in, or CoreDNS and metrics-server cannot reach the API server:

  • 6443/tcp for the API server and agent joins
  • 10.42.0.0/16, the pod network
  • 10.43.0.0/16, the service network
  • 8472/udp and 10250/tcp from the LAN, once you add a worker

Scope the last two to the LAN because the flannel VXLAN port should never face the internet. Add the rules and enable the firewall in one go.

sudo ufw allow OpenSSH
sudo ufw allow 6443/tcp
sudo ufw allow from 10.42.0.0/16 to any
sudo ufw allow from 10.43.0.0/16 to any
sudo ufw allow from 192.168.1.0/24 to any port 8472 proto udp
sudo ufw allow from 192.168.1.0/24 to any port 10250 proto tcp
sudo ufw --force enable

Replace 192.168.1.0/24 with your own LAN, then check the result with sudo ufw status numbered. The rule list should look like this:

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 6443/tcp                   ALLOW IN    Anywhere
[ 3] Anywhere                   ALLOW IN    10.42.0.0/16
[ 4] Anywhere                   ALLOW IN    10.43.0.0/16
[ 5] 8472/udp                   ALLOW IN    192.168.1.0/24
[ 6] 10250/tcp                  ALLOW IN    192.168.1.0/24
[ 7] OpenSSH (v6)               ALLOW IN    Anywhere (v6)
[ 8] 6443/tcp (v6)              ALLOW IN    Anywhere (v6)

There is no rule for 80 or 443 in that list. You’ll see in the Traefik section that the ingress still answered from another machine on the LAN, so UFW is not what protects those ports on a K3s node.

Install K3s from the stable channel

The script at get.k3s.io resolves a channel to a release, verifies the binary’s checksum and writes a systemd unit. Setting INSTALL_K3S_CHANNEL=stable is the default anyway, but spelling it out keeps your intent in the shell history. A rerun months from now lands on whatever is stable then rather than on latest, which already carries the next minor.

curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -

Our first run took 24 seconds of wall time and a rerun on the same VM took 2 minutes 29 seconds, and the difference was all GitHub download speed. The second line names the release it picked, so check it before you go further. The symlink lines show that the three CLI tools all point at the same k3s binary.

K3s installer output on Ubuntu 24.04 resolving the stable channel to v1.36.5+k3s1 and creating the systemd unit

To pin a release instead of a channel, swap the variable for INSTALL_K3S_VERSION with the exact tag from the K3s releases page. That is what you want on anything you rebuild from scripts, because the stable channel moves to a new minor without warning and a rebuilt node that comes up one minor ahead of its neighbours is a support headache you don’t need.

Verify the node and container runtime

Check the service first, because a K3s that failed to start still leaves the binary and symlinks in place. You want active (running) and the two modprobe lines ending in SUCCESS.

systemctl status k3s --no-pager | head -12

Then ask the API server for the node and containerd for its version. Use sudo on both for now, and the next section explains why plain kubectl fails.

sudo kubectl get nodes -o wide
sudo crictl version

The node reported Ready 2 seconds after the service started. The ROLES column now reads only control-plane, where the old 1.29-era output on this page showed control-plane,master. Any script or node selector that still matches on “master” will match nothing, so update it before you upgrade an older cluster.

systemctl status k3s, sudo kubectl get nodes -o wide and sudo crictl version on Ubuntu 24.04

The binary reports its own build too, which is the string to quote in a bug report:

k3s --version

It prints the K3s tag with its commit, and the Go toolchain it was built with:

k3s version v1.36.5+k3s1 (3dd98cc5)
go version go1.26.8

With the service, node and runtime confirmed, fix the kubeconfig permissions before going further.

Use kubectl without sudo

K3s writes the admin kubeconfig to /etc/rancher/k3s/k3s.yaml with mode 600 and root ownership. The first kubectl you run as your own user therefore fails with this error.

time="2026-11-04T04:55:04Z" level=warning msg="Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode or --write-kubeconfig-group to modify kube config permissions"
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied

The warning suggests --write-kubeconfig-mode 644, which works but makes a cluster-admin credential readable by every local account on the box. Copy the file into your home directory instead and keep it private.

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config
echo "export KUBECONFIG=~/.kube/config" >> ~/.bashrc
export KUBECONFIG=~/.kube/config

Now kubectl get nodes works without sudo. The copy embeds a client certificate that K3s issues for 365 days and renews on a restart close to expiry, so repeat the copy after a renewal or your kubectl starts failing roughly a year from now. It also points at https://127.0.0.1:6443, so to manage the cluster from a workstation, change that address to the server’s IP after copying the file over. Tools like k9s read the same file.

Check the bundled components

List what K3s deployed into kube-system and the image behind each container. This is the quickest way to answer “which Traefik am I running”, since the version is in the image tag and nowhere else obvious.

kubectl get pods -A
kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u

Two helm-install jobs show Completed, and the Traefik one restarted along the way (once on our first run, twice on the second). That is normal, because the Traefik chart job starts before the CRD job finishes and simply retries. Traefik, CoreDNS, metrics-server and the local-path provisioner are all Running.

kubectl get pods -A showing CoreDNS, Traefik, metrics-server and local-path-provisioner with their image tags on K3s

There’s no etcd pod or process here. A single server without --cluster-init keeps cluster state in SQLite through kine, and you’ll find state.db under /var/lib/rancher/k3s/server/db/. That is fine for one node, and it is lighter than etcd. Embedded etcd only comes into play when you build an HA control plane, so back up that directory along with the server token if this node matters to you.

Expose a test app through Traefik

K3s ships Traefik as the ingress controller and its own ServiceLB (klipper-lb), which gives the Traefik LoadBalancer service the node’s IP. That means an Ingress works on a bare VM with no MetalLB and no cloud load balancer. Create a two-replica nginx deployment, a service and an Ingress for a test hostname.

kubectl create deployment web --image=nginx:stable --replicas=2
kubectl expose deployment web --port=80
kubectl create ingress web --class=traefik --rule="web.example.com/*=web:80"
kubectl rollout status deployment web --timeout=120s

Traefik routes on the Host header, so a request without it should 404 and a request with it should reach nginx. Check the Ingress and the Traefik service, then curl both ways, swapping in your own node IP.

kubectl get ingress web
kubectl get svc -n kube-system traefik
curl -s -H "Host: web.example.com" http://192.168.1.174/ | grep -i "<title>"
curl -s -o /dev/null -w "nohost %{http_code}\n" http://192.168.1.174/

ServiceLB gave the Traefik service, and with it the Ingress, the node IP as its address. The two curls return the nginx welcome page and a 404 as expected.

Ingress and Traefik LoadBalancer service on the K3s node IP, with curl returning the nginx page and a 404 without the Host header

We ran the same Host-header curl from the second VM on the LAN with UFW active on the server, and it returned 200. So did a request to the Traefik NodePort (30172 in that run). Kubernetes rewrites traffic for the LoadBalancer IP and NodePorts in the iptables nat table before routing, so those packets are forwarded to the pod and UFW’s input rules never see them. If the node sits on an untrusted network, restrict 80, 443 and the NodePort range upstream or in a Traefik middleware, because UFW alone won’t. For the Traefik dashboard itself, see enabling the Traefik dashboard on K3s.

Customize Traefik with a HelmChartConfig

Never edit /var/lib/rancher/k3s/server/manifests/traefik.yaml, because K3s rewrites it on every start and your change disappears. Overrides go in a HelmChartConfig named traefik next to it. The bundled chart is now the v40 line, and that matters if you copied an old override from a blog. The ingress-nginx compatibility provider was renamed from kubernetesIngressNginx to kubernetesIngressNGINX, a change of case that is easy to miss in review.

sudo vim /var/lib/rancher/k3s/server/manifests/traefik-config.yaml

This example turns on the provider that lets Traefik serve Ingress objects written for ingress-nginx (class nginx), which helps when you move manifests off an nginx cluster.

apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    providers:
      kubernetesIngressNGINX:
        enabled: true

The helm-controller picks the file up within a few seconds, runs a new helm-install-traefik job and rolls the Traefik pod. Confirm the provider flags made it onto the deployment.

kubectl -n kube-system get deploy traefik -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i -E "nginx|kubernetesingress"

With the new key, eight kubernetesingressnginx flags appear on the Traefik deployment:

Traefik deployment args showing the kubernetesingressnginx provider flags after a HelmChartConfig with kubernetesIngressNGINX on K3s

Then we put the old spelling back to see what breaks. Nothing visibly does, and that is the problem. The Helm upgrade reported STATUS: deployed and “Upgrade complete” on both of our runs, yet the deployment carried zero nginx flags, so the provider was silently off. If ingress-nginx objects stopped routing after a K3s upgrade, check this key first.

Turn on secrets encryption

By default K3s stores Secrets in its datastore base64-encoded, not encrypted, so anyone who copies state.db can read them. Older K3s only took --secrets-encryption at first start. The March 2026 patch releases (v1.35.3, v1.34.6 and v1.33.10) and everything after them, including the whole 1.36 line, let you switch it on for a cluster that is already running. Create a test Secret first so you can check it survives, then look at the current state.

kubectl create secret generic demo --from-literal=password=s3cr3t-lab
sudo k3s secrets-encrypt status

A fresh install reports no encryption config at all:

Encryption Status: Disabled, no configuration file found

Enable it, then add the flag to the K3s config file so the server keeps it across restarts. Use tee -a because a plain tee would overwrite any settings already in that file, and K3s would start without them. The restart is not optional, because the docs warn that not following the procedure can leave the cluster permanently corrupted.

sudo k3s secrets-encrypt enable
echo "secrets-encryption: true" | sudo tee -a /etc/rancher/k3s/config.yaml
sudo systemctl restart k3s
sudo k3s secrets-encrypt status

After the restart the status still says Disabled, but the rotation stage is start. The encryption config is staged but no key is active and nothing has been re-encrypted yet, so the demo Secret is still plain base64 on disk.

Encryption Status: Disabled
Current Rotation Stage: start
Server Encryption Hashes: All hashes match

Rotating the keys does the actual work. It generates the AES-CBC key and rewrites every existing Secret with it. The docs ask for one more restart afterwards, so check the status, restart and check again.

sudo k3s secrets-encrypt rotate-keys
sudo k3s secrets-encrypt status
sudo systemctl restart k3s
sudo k3s secrets-encrypt status

To confirm the Secret still reads through the API and is encrypted on disk, decode it and count the encrypted records in the SQLite files. The sh -c wrapper matters, because without it your own shell expands the glob before sudo runs and grep finds no files.

kubectl get secret demo -o jsonpath="{.data.password}" | base64 -d
sudo sh -c "grep -a -c k8s:enc:aescbc:v1 /var/lib/rancher/k3s/server/db/state.db*"

On both of our runs rotate-keys already reported Enabled and reencrypt_finished before the restart, and the status held after it. The demo Secret decoded to its value, and grep counted 10 encrypted lines in state.db and 10 in its WAL file.

k3s secrets-encrypt rotate-keys and status showing Enabled and reencrypt_finished before and after a restart, plus aescbc records in state.db

Back up /var/lib/rancher/k3s/server/cred/encryption-config.json together with the token. A datastore restore without that key leaves every Secret unreadable.

Know what changed since the 1.29 builds

Nothing changed in the install command, and no install flag was added or removed in the 1.36 line, though 1.36.3 started accepting wildcards such as --tls-san=*.example.com. What did change shows up after the install, and each row below was checked on this lab server.

ChangeWhat we saw on the lab server
Node role labelROLES shows control-plane only, no master
Traefik chart v40kubernetesIngressNGINX works, the old kubernetesIngressNginx key is ignored silently
Supervisor anonymous access/metrics and /debug/pprof/ return 403 to anonymous requests
Secrets encryptionEnabled on a running server with an existing Secret
DatastoreSingle server still uses SQLite (kine), no etcd

The supervisor change is the one most likely to break monitoring. From the v1.34.12, v1.35.9, v1.36.5 and v1.37.1 patch releases, K3s rejects anonymous requests to the supervisor’s metrics, pprof and embedded registry routes on port 6443. Both endpoints are off by default, so to test it we turned them on in the server config and restarted K3s.

printf "enable-pprof: true\nsupervisor-metrics: true\n" | sudo tee -a /etc/rancher/k3s/config.yaml
sudo systemctl restart k3s

Then hit the endpoints once anonymously and once with the admin client certificate that K3s keeps on the server:

curl -sk https://127.0.0.1:6443/metrics
curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1:6443/debug/pprof/
sudo curl -sk --cert /var/lib/rancher/k3s/server/tls/client-admin.crt --key /var/lib/rancher/k3s/server/tls/client-admin.key https://127.0.0.1:6443/metrics | grep -m2 ^k3s_

Both anonymous requests get a 403, and the /metrics response names system:anonymous, while the request with the client certificate returns the k3s_certificate_expiration_seconds series:

Anonymous curl to the K3s supervisor metrics and pprof endpoints returns 403 while the admin client certificate gets k3s metrics

So a Prometheus job that scraped the supervisor without credentials stops working after the upgrade, and the only symptom is a target turning red. Give it a client certificate or a bearer token with access to those paths. If you do not scrape them, delete the two lines from config.yaml and restart, since pprof has no business running on a production node.

Add a worker node

A worker needs the server URL and the join token, which lives on the server. Read it there and keep it out of shell history and chat logs, because anyone holding it can join a node to your cluster.

sudo cat /var/lib/rancher/k3s/server/node-token

Patch the worker with apt upgrade first. We skipped that on k3s02, which is why it still reports 24.04.4. Then run the same installer with K3S_URL and K3S_TOKEN set. Keep the channel identical so both nodes run the same release, and paste your real token in place of the placeholder. Our worker kept UFW inactive. If you enable it there too, give it the same pod, service, 8472/udp and 10250/tcp rules as the server.

curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable K3S_URL=https://192.168.1.174:6443 K3S_TOKEN='paste-node-token-here' sh -

The agent installs as k3s-agent.service. It took 1 minute 25 seconds on our first run and 15 seconds on the second, again mostly the binary download. Back on the server, scale the test deployment so the scheduler has a reason to use the new node, then check where the pods landed.

kubectl scale deployment web --replicas=4
kubectl get nodes -o wide
kubectl get pods -l app=web -o wide

Both nodes are Ready on the same release, and the two new replicas went to k3s02:

kubectl get nodes -o wide showing the K3s server and joined worker, with four nginx pods spread across both nodes

To prove the flannel VXLAN path works through UFW, curl a pod on the worker from a pod of the same deployment. Use a pod IP from your own output, here 10.42.1.4 on k3s02:

kubectl exec deploy/web -- curl -s -m 5 -o /dev/null -w "pod -> 10.42.1.4 on k3s02: %{http_code}\n" http://10.42.1.4/

A 200 means pod traffic crosses nodes with UFW active on the server:

pod -> 10.42.1.4 on k3s02: 200

Clustering beyond one worker is out of scope here. Labels, taints, Helm and an HA control plane are in the multi-node K3s guide for Ubuntu 24.04. If you’re on the newer LTS, the Ubuntu 26.04 K3s guide covers the same install there, and our k0s vs K3s vs MicroK8s benchmark shows how K3s compares on memory and startup time.

Uninstall K3s

The installer leaves an uninstall script that stops the service, kills the containers, removes the CNI interfaces and deletes /var/lib/rancher/k3s. Remove workers first and then delete their node objects from the server, or the server keeps listing them as NotReady. On each worker, run the agent script.

sudo /usr/local/bin/k3s-agent-uninstall.sh

Back on the server, delete the node object and then uninstall the server itself:

kubectl delete node k3s02
sudo /usr/local/bin/k3s-uninstall.sh

Both scripts exited 0 and removed the binary, the kubectl symlink, /etc/rancher/k3s and the data directory. A few things survive on the server, so clean them up yourself if the box will not run K3s again:

  • the empty /var/lib/rancher directory
  • /etc/rancher/node with the node password file
  • the UFW rules, removed by number with sudo ufw delete
  • your copy of the kubeconfig in ~/.kube/config

The kubeconfig is harmless once the server is gone, but a stale context pointing at a dead IP wastes time the next time you run kubectl against a new cluster.

For a single box, this is the whole job: one curl, a private kubeconfig, and secrets encryption switched on before real Secrets land. Skip the HelmChartConfig until you need it, but when you do, spell the provider key the v40 way and confirm the flags on the deployment rather than trusting a green Helm status.

Keep reading

Upgrade Ubuntu 24.04 to Ubuntu 26.04 LTS (Step by Step) Ubuntu Upgrade Ubuntu 24.04 to Ubuntu 26.04 LTS (Step by Step) UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Security UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Ubuntu 26.04 vs 24.04 LTS: Should You Upgrade Now? Ubuntu Ubuntu 26.04 vs 24.04 LTS: Should You Upgrade Now? Upgrade a Single Package on Ubuntu / Debian with apt Debian Upgrade a Single Package on Ubuntu / Debian with apt CapRover Docker Install on Debian 13: Deploy Your First App Containers CapRover Docker Install on Debian 13: Deploy Your First App Rancher Quickstart: Deploy Your First Cluster in 15 Minutes Containers Rancher Quickstart: Deploy Your First Cluster in 15 Minutes

Leave a Comment

Press ESC to close