# Bare metal<no value>

<!-- Generated by website/sync-docs.py from docs/install/baremetal.md. Edit that file, not this one. -->

This page turns a spare tower or mini-PC into a single-node Kubernetes cluster.
You need two USB sticks. One stick installs the operating system. The other
stick tells the node what it is.

For a dedicated server that you reach through the rescue system of a provider,
see [rescue mode](/docs/install/rescue/). For a VM, see [Proxmox](/docs/install/proxmox/) or the
[quick start](/docs/guides/quickstart/).

> A CI run tests the [Proxmox](/docs/install/proxmox/) page, but no CI run tests this page.
> Nothing in the project boots the ISO on physical hardware. If your machine
> does not behave as a step here says, open an issue.

## Before you start

- **A 64-bit x86 machine that boots UEFI.** Corium boots UEFI only.
- **A whole disk that you can lose.** The installer erases its target disk. If
  the machine has more than one disk, disconnect the others.
- **Two USB sticks.** The stick for the ISO must be 4 GB or more, because the
  ISO is about 2.4 GB. The stick for the seed can be any size.
- **Wired ethernet.** Corium does not configure wireless.
- **4 GB of RAM and 32 GB of disk**. 8 GB of RAM is better.
- **A public SSH key.**

## How the install is shaped

Two separate things happen:

1. **The ISO installs the operating system.** This step needs no operator.
   Anaconda deploys the bootc image that is inside the ISO. It shows no prompts,
   and you do not write a kickstart file. It never asks what the node is.
2. **cloud-init configures the node on its first real boot.** cloud-init applies
   the `corium:` block and creates your login account.

A desktop PC has no metadata service for step 2. You supply the data from a
**NoCloud seed**: a USB stick with a filesystem that has the label `CIDATA`.
Corium looks for this datasource first, and it is the only datasource that works
offline. You can also install without a seed, and that state is
[recoverable](#if-you-install-without-a-seed).

---

## 1. Write the ISO to a stick

Download `corium-0.5.0-x86_64.iso` and verify it. [downloads](/docs/install/downloads/)
gives all three download routes and the verification steps.

**Run the listing command first. If `dd` writes to the wrong device, it destroys
that device.**

```bash
# linux
lsblk -o NAME,SIZE,MODEL,TRAN
sudo dd if=corium-0.5.0-x86_64.iso of=/dev/sdX bs=4M status=progress oflag=sync
```

```bash
# macOS
diskutil list
diskutil unmountDisk /dev/diskN
sudo dd if=corium-0.5.0-x86_64.iso of=/dev/rdiskN bs=4m
```

On macOS, use `/dev/rdiskN` and not `/dev/diskN`. For a write of this size, the
raw device is approximately ten times faster.

A graphical writer also works, for example Rufus, balenaEtcher, or GNOME Disks.
The writer must copy the image exactly as it is. In Rufus, use **DD image mode**,
not ISO mode.

## 2. Write the configuration seed

A complete single-node cluster:

```yaml
#cloud-config
corium:
  role: single

users:
  - name: core
    groups: [wheel]
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... you@example.com
```

`role` is the only required field. See [examples](/docs/reference/examples/) for workers,
add-ons, and a custom CNI. See the [reference](/docs/reference/configuration/) for every field.
If you have a checkout of the repository, validate the file before you go to the
machine. Validation works offline and reports all problems at once. `mise exec`
supplies the pinned Go toolchain, so the command works on a machine that never
installed Go:

```bash
mise exec -- go run ./cmd/corium-agent validate node.yaml
```

Put two files on a filesystem with the label `CIDATA`. cloud-init looks for this
label. Nothing else about the stick is important.

```bash
# linux. /dev/sdY is the *second* stick, not the one holding the ISO.
sudo mkfs.vfat -n CIDATA /dev/sdY1
sudo mount /dev/sdY1 /mnt

sudo cp node.yaml /mnt/user-data
printf 'instance-id: corium-01\nlocal-hostname: corium-01\n' | sudo tee /mnt/meta-data
sudo umount /mnt
```

```bash
# macOS
diskutil eraseDisk FAT32 CIDATA MBRFormat /dev/diskN

cp node.yaml /Volumes/CIDATA/user-data
printf 'instance-id: corium-01\nlocal-hostname: corium-01\n' > /Volumes/CIDATA/meta-data
```

Both files must be at the root, with exactly these names. `meta-data` can be
almost empty, but it must exist. If it does not exist, cloud-init rejects the
seed.

## 3. Set up the firmware

Open the setup screen. Usually you press `Del`, `F2`, or `F12` at power-on. Set
these three values:

| Setting | Value | Why |
|---|---|---|
| Boot mode | UEFI, CSM/legacy off | Corium boots UEFI only |
| Boot order | **Internal disk first, USB second** | See below |
| Secure Boot | Off, if the install does not boot | See below |

**Disk first, USB second** is correct. An empty disk has no UEFI boot entry, so
the firmware goes to the stick and installs. After the install, the disk has an
entry and boots first. If the stick is first, the machine installs itself again
at every reboot. The install shows no prompts, so nothing on the screen tells
you.

**Secure Boot** is not tested on bare metal. The image includes the signed shim
from Fedora, so Secure Boot can work. If the machine does not boot after the
install, turn Secure Boot off.

## 4. Install

Connect **only the ISO stick**, and turn on the machine. The installer then has
one less disk to look at. Anaconda reboots when it is done, after a few minutes
on an SSD. Remove the stick during that reboot.

## 5. Configure it

Connect the seed stick and boot the machine.

cloud-init finds `CIDATA` and creates your user. Then `corium-bootstrap` reads
the `corium:` block and starts k0s. The node is `Ready` approximately one minute
after power-on. The console banner shows the role of the node, its cluster, and
whether k0s is running.

When the node is up, you can remove the seed stick or leave it in.

## 6. Check it worked

```bash
ssh core@<the node's address>
sudo k0s kubectl get nodes
```

For the kubeconfig and the next steps, continue with the
[quick start](/docs/guides/quickstart/#4-check-it-worked). Management of the node with
[`cctl`](/docs/reference/cli/) is a separate step. The management API is off by default,
and you must claim a node before it answers.

---

## If you install without a seed

The seed is optional. Without a seed, the ISO installs a node that waits for its
configuration. The node shows a pairing code on its console, and you can claim
it from your own machine. The seed stick is the path that needs no operator, but
it is not the only path.

| | After the install |
|---|---|
| `corium-bootstrap` | Ran, found nothing, wrote no marker |
| `corium-apid` | Serving on 7443, enrolment open, pairing code on the console |
| k0s | Not started. A node nobody has claimed is in no cluster |
| Accounts | None. Corium creates no default user, and `PermitRootLogin` is `no` |

The console shows the code and the fingerprint. Use the fingerprint to make
sure that you claim the correct node:

```
  This node is not part of a cluster.
  address     192.168.0.42
  pairing     K4M2-8QT9
  fingerprint SHA256:...

  cctl enroll 192.168.0.42 --code K4M2-8QT9
```

Run this command from your own machine. You must already have an operator PKI.
[cctl](/docs/reference/cli/#2-from-nothing-to-a-managed-node) shows how to make one.

```bash
cctl enroll 192.168.0.42 --code K4M2-8QT9 --config node.yaml
```

`node.yaml` is the same document as the seed in step 2, with the `users:` block.
The `users:` block gives the machine its first account. The node pins your CA,
builds the role, and starts as a cluster.

This is the default since [ADR 11](/docs/decisions/adr-0011-api-on-by-default/).
Before ADR 11, a machine installed this way had no account and no API. The only
way in was to go back to the machine with a USB stick.

**The seed stick still works, and it is still the better choice when you have
one.** It needs no operator: you read no console and type no code. Your CA owns
the node from its first boot, not only after the minute between boot and claim.
Connect a `CIDATA` stick and reboot. The node wrote no bootstrap marker, so
step 5 runs as if it were the first boot.

Know these two limits before you rely on enrolment:

- **It closes when the node bootstraps.** You cannot claim a node that built its
  role, whatever its configuration says. This is intentional. It stops a person
  who reads the screen from taking over a machine that runs your workloads. As a
  result, you can claim a node configured by seed only during the minute that
  it takes to come up. After that, you must use `cctl reset` to take it over.
- **It needs the console.** The pairing code is on a channel that an attacker on
  the network cannot see. All of its security comes from this. On a headless
  machine with no IPMI, that channel is a monitor that you must go and connect.

`corium.config=<url>` on the kernel command line is the third option, and the
weakest. It configures the node but creates no account. The result is a cluster
that you cannot log in to.

---

## What goes wrong on a desktop that does not on a hypervisor

**It installs itself again on every boot.** The boot order causes this. Put the
disk first and USB second, and remove the ISO stick after the install.

**The node comes up with no user and no cluster.** cloud-init did not find the
seed. Make sure that the filesystem label is exactly `CIDATA`. Make sure that
`user-data` and `meta-data` are both at the root of the filesystem. Make sure
that `user-data` starts with the `#cloud-config` line. On the node,
`sudo cloud-init query --all` shows which datasource cloud-init selected.

**The node has an address but resolves no names.** The cause is a static address
without a resolver. Keep the machine on DHCP for the first boot. If the address
must be static, set it in the `network:` block of cloud-init, and give that
block its own nameserver.

**Nothing on the console after the firmware.** The cause is almost always the
boot mode. A firmware in CSM/legacy mode finds no bootloader on a UEFI-only
image. Its message makes the machine look dead.

For other problems, see [troubleshooting](/docs/guides/troubleshooting/). It lists them
by symptom.
