How can I install Ubuntu Server on a headless machine with ssh access only?

Ubuntu Version:
Ubuntu server 25.04

Problem Description:
I need to install Ubuntu server on a machine that doesn’t have a keyboard or screen attached. I don’t have a way of connecting either to it. The machine lives in my local network. I do have physical access.

What I’ve Tried:
When I boot the machine with the ubuntu-25.04-live-server-amd64.iso on a USB-stick, it soon shows up in my network with ubuntu-server as a hostname. When I ssh to it from my PC, the key/fingerprint thing happens, but I can’t login since I don’t have the login information. I’ve looked around, but apparently the ssh password is randomly generated on boot or something?
I’m looking for a way to define ssh login credentials on the install medium so I can ssh into the installer after it boots. I’ve found some information online that this can be done via editing the grub.cfg on the USB-stick, but nothing definitive. Can you help me?


The installer already starts an SSH server. It creates a throw-away user called installer and gives it a random password that only appears on the (missing) screen – that’s why you can’t log in.
Tell cloud-init what password or key you want. Drop a tiny “NoCloud” seed onto the same USB stick and point the kernel at it:On the USB stick

/nocloud/
    ├─ user-data   # your settings
    └─ meta-data   # just one line: instance-id: nocloud

user-data (minimal example)

#cloud-config
autoinstall:
  version: 1
  interactive: true        # keep the TUI instead of a full autopilot
ssh_authorized_keys:
  - ssh-ed25519 .your_public_key
chpasswd:
  expire: false
  list:
    - installer:$6$hash_of_a_password_you_choose

*Add one kernel parameter. Open grub.cfg on the stick, find the “linux” line, and append:

ds=nocloud;s=/cdrom/nocloud/ autoinstall

Boot, then SSH straight in:

ssh installer@<machine-IP>

Use the password you set (or rely on your SSH key). You’ll be dropped into the familiar Subiquity TUI to finish the install.

That’s it — no keyboard, no monitor, just a quick ISO tweak and you’re in. For the full syntax of the user-data file, the official autoinstall reference is handy.

2 Likes

Thank you very much, this seems to be going in the right direction, however it’s not working yet. Maybe you can help me narrow down why:

Here’s my grub.conf:

set timeout=30

loadfont unicode

set menu_color_normal=white/black
set menu_color_highlight=black/light-gray

menuentry "Try or Install Ubuntu Server" {
	set gfxpayload=keep
	linux	/casper/vmlinuz ds=nocloud;s=/cdrom/nocloud/ autoinstall ---
	initrd	/casper/initrd
}
grub_platform
if [ "$grub_platform" = "efi" ]; then
menuentry 'Boot from next volume' {
	exit 1
}
menuentry 'UEFI Firmware Settings' {
	fwsetup
}
else
menuentry 'Test memory' {
	linux16 /boot/memtest86+x64.bin
}
fi

And here’s my user-data

#cloud-config
autoinstall:
  version: 1
  identity:
	hostname: augustus
  interactive: true        # keep the TUI instead of a full autopilot
ssh_authorized_keys:
  - ssh-ed25519 SHA256:REDACTED
chpasswd:
  expire: false
  list:
    - installer:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

(don’t worry, the password hash isn’t the one I’m actually using)

And finally, meta-data:

instance-id: nocloud

As far as I can tell, I’ve done it all correctly, however it doesn’t seem to be parsed at all, since the hostname the machine requests from my router isn’t “augustus” as I’ve specified in user-data, but ubuntu-server as is default.
Any ideas?

Kernel line: make sure the parameter is

before

the

and : —

- linux /casper/vmlinuz ds=nocloud;s=/cdrom/nocloud/ autoinstall ---
+ linux /casper/vmlinuz autoinstall ds=nocloud;s=/cdrom/nocloud/ ---

Everything after the first — is not passed to the kernel, so autoinstall

or ds= written on the wrong side never reach cloud-init.


Folder position

nocloud must sit at the top of the ISO/USB:

/cdrom/nocloud/{user-data,meta-data}

(the same level as casper/, not inside it).


YAML formatting

Use spaces, not tabs — YAML quietly breaks on tabs.
hostname belongs at top level (or as local-hostname:), not underidentity:. For example:

#cloud-config
hostname: augustus

autoinstall:
  version: 1
  interactive: true

ssh_authorized_keys:
  - ssh-ed25519 your_key

chpasswd:
  expire: false
  list:
    - installer:$6$your_hash

meta-data

needs a newline

Make sure instance-id: nocloud ends with an actual newline; some editors

omit it and cloud-init treats the file as empty.


Fix those four things, re-boot, and the box should appear on the network as

augustus and accept the SSH key / password you set. If it still doesn’t,

watch the boot logs (journalctl -b | grep cloud-init) for clues. Good luck!

1 Like

I’ve done that, but I still can’t make it work - the hostname is still unchanged, and then I can’t log in.
I’m afraid I don’t have a way of looking at the boot logs, since I can’t access the machine. Or does it write the boot logs somewhere on the USB stick so I can read them later on another machine?

Next-step debugging tips

Show cloud-init on the console Edit the same kernel line again and append

autoinstall debug --console=tty1

debug makes Subiquity + cloud-init extremely chatty.
console=tty1 keeps that chatter on the main screen, so you can watch it scroll instead of being dropped after the —.
Drop into the emergency shell Still on the kernel line, add one of these (pick whichever you prefer):

break=mount (stops right after the root fs is mounted)
systemd.unit=emergency.target (spawns a root shell early)Either gives you a prompt before the installer finishes, so you can run

journalctl -b | less         # full boot log
cat /var/log/cloud-init.log  # cloud-init debug

Grab logs from another machine If you can’t get a shell at all, you can still pull the logs offline:

# Live-USB, any Linux box
sudo mkdir /mnt/tgt
sudo mount /dev/sdX2 /mnt/tgt   # replace sdX2 with the installed root
sudo less /mnt/tgt/var/log/cloud-init.log
sudo less /mnt/tgt/var/log/installer/subiquity-server-debug.log

The cloud-init and Subiquity logs are kept on the target disk even if the install aborts.


Two quick sanity checks

Hostname still wrong? Make sure the very first line in user-data is either

hostname: augustus

or

local-hostname: augustus

Login failure: Check the key line really begins with ssh-ed25519 (or ssh-rsa) and that the full key is on one line with no extra spaces or line-breaks. Also confirm you placed it under ssh_authorized_keys: without a dash if you only list one key.

Once you have the debug output (or if any of those quick checks fix the issue) post it back here and we can dig deeper. Good luck hope we will solve it.

You have a semi-colon after ds=nocloud, which is required, but it means grub will treat the rest of the line as a comment. So you have to either escape it with a \ or put the value in quotes.

Do this…

linux /casper/vmlinuz autoinstall ds=nocloud\;s=/cdrom/nocloud/ ---

or this…

linux /casper/vmlinuz autoinstall "ds=nocloud;s=/cdrom/nocloud/" ---

GRUB syntax the semicolon does act as a command separator, so everything that follows is ignored unless you escape or quote it. Either of the two forms you showed works:

grub

# escape just the semicolon
linux /casper/vmlinuz autoinstall ds=nocloud\;s=/cdrom/nocloud/ ---

# …or quote the whole key=value pair
linux /casper/vmlinuz autoinstall "ds=nocloud;s=/cdrom/nocloud/" ---

Once GRUB passes that intact to the kernel you should see cloud-init pick up the seed at /cdrom/nocloud/ and the installer will come up with the hostname, SSH key, and password you defined in user-data.

If you still don’t get an SSH prompt after boot, run

bash

journalctl -b | grep -Ei 'cloud-init|subiquity'

from the console (or add a cheap USB keyboard temporarily) to confirm the NoCloud datasource was detected. But with the semicolon fixed, it normally just works. Thanks for spotting it!