SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Cloud-init user not created: how to log in

Locked out because cloud-init never made your login user? Find the real username, read cloud-init status and the logs, and get back in from the console.

Why you cannot log in

Cloud-init did not create your user, so there is no account on the server for your key to match. SSH answers Permission denied (publickey) because the login name you typed either does not exist, or exists with an empty ~/.ssh/authorized_keys. Nothing you change on your laptop can fix that. The repair happens on the server, through your provider's console.

Cloud-init is the program that runs on the first boot of a cloud image. It reads the user-data you pasted into the provider's panel, then creates accounts, writes SSH keys, sets the hostname and installs packages. If it never ran, or it ran and one module failed, the machine boots into a working Linux system with none of your configuration applied. The server is healthy. Your account is simply not on it.

Why the publickey checklist will not help here

Permission denied (publickey) has two very different causes, and they need opposite work. The client-side causes are a wrong key file, a wrong path, an agent that offers some other identity first, or bad permissions on your private key. Those are worth ruling out in two minutes, and the usual permission denied publickey checklist walks through them. The server-side cause is that the account is missing entirely.

No amount of ssh -vvv tells you which one you have. A correctly configured SSH server refuses an unknown user in exactly the same way it refuses a known user with the wrong key, and that is deliberate: identical refusals stop an attacker from using SSH to discover which usernames exist. So the client output looks the same in both cases, and you cannot debug your way out from that side.

One signal does separate them. If you can log in as root with the password your provider emailed you, but your intended user is refused, stop looking at SSH. The service is running and accepting logins. The account is missing.

Get a console session first

You need a shell that does not travel over SSH. Every serious VPS provider gives you one, usually labelled Console, VNC or Web terminal in the control panel. It attaches to the virtual machine's screen and keyboard through the hypervisor, so it keeps working when SSH, the network or the firewall is broken.

Log in as root. If you do not have the root password, the panel usually offers a reset that takes effect on the next boot, and resetting the root password on an Ubuntu VPS covers the recovery-mode route for when that button is not enough. Some images ship with root's password locked, which prints Login incorrect for every attempt. In that case boot the provider's rescue image, mount the disk and work from there.

Every command below runs in that console session, as root or through sudo.

Ask cloud-init what happened

cloud-init status --long

A finished first boot reports done:

status: done
extended_status: done
boot_status_code: enabled-by-generator
last_update: Sun, 23 Aug 2026 09:14:02 +0000
detail: DataSourceNoCloud
errors: []
recoverable_errors: {}

Three fields carry the diagnosis. boot_status_code says whether cloud-init was allowed to run at all: any value containing disabled means it was switched off, by the marker file /etc/cloud/cloud-init.disabled, by a kernel command line argument, or by whoever built the image, and your user-data was never going to be applied. errors lists failures that stopped a stage. recoverable_errors lists problems cloud-init logged and then carried on past, which is where a rejected users block usually appears, because cloud-init skips that module and finishes the boot looking almost normal.

Cloud-init runs in stages, and each stage is a systemd unit. Check which ones completed:

systemctl status cloud-init-local.service cloud-init-network.service \
  cloud-config.service cloud-final.service

On releases from before the unit was renamed, the second one is called cloud-init.service. Accounts are created early, in that network stage, by the module named users-groups, and the keys are written straight after by the module named ssh. Both are listed under cloud_init_modules in the base config:

grep -n -A25 'cloud_init_modules' /etc/cloud/cloud.cfg

This matters because anything that kills the network stage takes your user with it. A datasource that cannot be reached fails before users-groups ever gets a turn, and the accounts never happen, even though your user-data was perfect.

Is the username the one you assumed?

Cloud images ship with a default user, and the name is chosen by whoever built the image. It is not the same on every distribution, and it is not always what the provider's welcome email implies. Do not guess it. The running system will tell you:

grep -n -A6 'default_user' /etc/cloud/cloud.cfg

The name: under default_user is the account this image intends you to use. It sits under system_info, and the cloud-init documentation is explicit that values there cannot be overridden by user-data or vendor-data. Your image's page in the provider's documentation states the same name.

Now list the real accounts on the box:

getent passwd | awk -F: '$3 >= 1000 && $3 < 65534 { print $1, $3, $6 }'
ls -la /home

If your intended name is absent from that output, cloud-init never created it and the diagnosis is confirmed. If the name is present, look at the key instead:

sudo ls -la /home/devops/.ssh
sudo cat /home/devops/.ssh/authorized_keys

A missing or empty authorized_keys means the account module ran but the key did not arrive with it, usually because the key was indented under the wrong block. A key that is present and still refused sends you back to the client, where matching the key your client offers against the one on the server is the faster route.

There is one more way to lose the default user, and it surprises people. A users: list in your user-data replaces the default list. It does not add to it. This file creates devops and nothing else:

#cloud-config
users:
  - name: devops
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3Nza... you@laptop

Naming default first keeps both:

#cloud-config
users:
  - default
  - name: devops
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3Nza... you@laptop

That single word matters when your panel injects its own key into the image's default account. Replace the list and you throw away the access route the panel expected you to use, so a typo anywhere else in the file leaves you with no way in at all.

Did your user-data reach the server at all?

Before blaming your YAML, look at what actually arrived:

sudo cloud-init query userdata
sudo cat /var/lib/cloud/instance/user-data.txt

/var/lib/cloud/instance is a symlink to the directory for the currently active instance, and user-data.txt inside it is the raw payload the datasource handed over. If that file is empty, or holds a provider template you never wrote, the panel did not deliver your text and no YAML fix will change anything. Re-check the field in the order form after any change to the image or the region, since the form may reset it, and confirm the panel accepts user-data at creation time rather than after the server has already booted.

Is the user-data valid cloud-config?

Most of the remaining failures are user-data that is perfectly valid YAML and still wrong.

The first is a missing first line. A cloud-config document must begin with #cloud-config on line one, with no blank line above it, no leading spaces and no --- in front. Without that line cloud-init does not treat the payload as cloud-config, so it is stored, ignored, and never reaches the accounts module. The cloud-init documentation names this and broken YAML indentation as the two most common problems people hit.

The second is a key that is shaped wrong. users takes a list. ssh_authorized_keys takes a list and uses underscores. ssh-authorized-keys with hyphens is a different key, which is to say it is no key at all, and cloud-init has no reason to warn you about a word it does not recognise unless you ask it to.

Asking is one command. Run the validator against what the server received:

sudo cloud-init schema --system --annotate

A valid document reports that the schema is valid. A broken one prints the offending line with the reason attached underneath, in the form of messages like:

Additional properties are not allowed ('ssh-authorized-keys' was unexpected)
is not of type 'array'

The same validator reads a file directly, which is how you check work before it ever reaches a server:

cloud-init schema --config-file user-data.yaml --annotate

--annotate is the flag that earns its place. Without it you get the error. With it you get the error printed under the exact line that caused it.

Why does cloud-init ignore my edited user-data on reboot?

Because most cloud-init modules run once per instance, not once per boot. Cloud-init records that a module has run by writing a semaphore file into /var/lib/cloud/instance/sem/, and on every later boot it sees that marker and skips the module. So you fix the user-data in the panel, reboot, and nothing at all happens. The boot looks clean, the account is still missing, and no error is printed anywhere. This is the most confusing part of the whole failure.

ls -1 /var/lib/cloud/instance/sem/

A file named for the accounts module means cloud-init already considers that work done for this instance.

To make the next boot behave like a first boot:

sudo cloud-init clean --logs --reboot

clean removes cloud-init's state under /var/lib/cloud, --logs also removes /var/log/cloud-init.log and /var/log/cloud-init-output.log, and --reboot restarts the machine so the full sequence runs again from the start. Understand what that means before you type it. Every module runs again, so every package install and every script in your user-data happens a second time, and the logs that would have explained the original failure are gone. clean does not delete accounts that already exist and does not uninstall anything, so a re-run can leave you with a mix of old and new state. On a server that already carries real data, skip it and create the account by hand instead.

Why does console login fail even though the user exists?

Because cloud-init locks the password. lock_passwd defaults to true for accounts created through the users: list, which leaves the account usable over SSH with a key and unusable with a password. On the console it prints Login incorrect for every password you try, including the correct one, because there is no password hash to match.

Check the state from root:

sudo passwd -S devops

The second field is the answer. L means locked, NP means no password is set, and P means a usable password exists. Set one and unlock the account:

sudo passwd devops
sudo usermod -U devops
sudo passwd -S devops

The last command should now print P.

Password logins over SSH need one extra step, because cloud images normally ship with password authentication turned off in the SSH server. In user-data, ssh_pwauth: true is the key that turns it back on. There is a second trap in the same area: cloud-init expires a password it sets unless you tell it not to, so the account demands a new password at first login and any script that logs in with it stops dead.

#cloud-config
ssh_pwauth: true
chpasswd:
  expire: false
  users:
    - name: devops
      password: $6$rounds=4096$...
      type: hash

Keys remain the better answer. Use a password to reach the console, then go back to keys.

Read the logs and find the module that failed

Two log files, with different jobs. /var/log/cloud-init.log is cloud-init's own record: which module ran, in what order, and what it returned. /var/log/cloud-init-output.log holds the standard output and standard error of the commands cloud-init ran, so a failing package install prints its real message there and nowhere else.

Start at the end of both:

sudo grep -iE 'error|traceback|warning' /var/log/cloud-init.log | tail -n 40
sudo tail -n 60 /var/log/cloud-init-output.log

Then search for the two modules that matter to you by name:

sudo grep -n -iE 'users.groups|cc_ssh' /var/log/cloud-init.log

Cloud-init writes a Running module line as each module starts. A Running module line for users-groups with a Python traceback under it is your failure, and the traceback names the reason. No users-groups line at all means the module never ran, which sends you back to the stage that died earlier in the boot, or to user-data that was never parsed as cloud-config.

When the boot hangs instead of failing, time is the useful measure:

sudo cloud-init analyze blame | head -n 20

That prints modules ordered by how long each one took, so a datasource waiting on a metadata service it cannot reach shows up immediately as the most expensive line.

Create the user by hand and get back in

Once you know the cause, do not wait for a rebuild. Create the account from the console:

sudo useradd -m -s /bin/bash devops
sudo install -d -m 700 -o devops -g devops /home/devops/.ssh
sudo tee /home/devops/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3Nza... you@laptop
EOF
sudo chown devops:devops /home/devops/.ssh/authorized_keys
sudo chmod 600 /home/devops/.ssh/authorized_keys

The ownership and the mode are not decoration. The SSH server ignores a key file that other users can write, and it logs that decision on the server while telling your client only Permission denied (publickey). A wrong mode therefore looks exactly like a wrong key from where you are sitting.

Grant administrative rights next. The group name differs between distributions, so read it rather than assume it:

getent group sudo || getent group wheel
sudo usermod -aG sudo devops

Add a sudoers drop-in only if you want sudo without a password prompt, and check it before you close the console:

echo 'devops ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/90-devops
sudo chmod 440 /etc/sudoers.d/90-devops
sudo visudo -c

visudo -c prints parsed OK for each file it reads. A syntax error in that directory breaks sudo for every user on the machine, so read the output before you log out.

Now test from your laptop while the console session stays open:

ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 devops@203.0.113.10

IdentitiesOnly=yes stops your agent from offering other keys ahead of the one you named, which matters once you hold more than a couple. If it still fails, read the server side while you retry:

sudo journalctl -u ssh -u sshd -n 50 --no-pager

A line such as Authentication refused: bad ownership or modes for file /home/devops/.ssh/authorized_keys names the real problem, and it is visible only from the server.

Validate before you provision next time

The cheapest fix is to never ship user-data you have not validated. Install cloud-init on a Linux box you already have, or keep one throwaway VPS for the purpose, and run the validator against your file before it goes anywhere near a panel:

cloud-init schema --config-file user-data.yaml --annotate

That catches the missing #cloud-config line and the key names that are spelled wrong, which is most of what goes wrong. It cannot catch a username you did not mean to use, or a panel that quietly drops the field. So keep a second way in for every new server: a root password you have already tested through the console, or the provider's rescue image. Writing cloud-init user-data for a new VPS covers the shape of a file that works, and the first ten minutes on a new VPS covers what to do once you are back inside.

FAQ

Why does SSH say publickey denied when the user was never created?

An SSH server refuses an unknown username with the same message it uses for a known username with the wrong key. That is intentional, because different messages would let anyone probe the server for valid account names. So Permission denied (publickey) on a brand new VPS tells you authentication failed, and nothing more. Open the provider's console, run getent passwd and look for your account. If it is not listed, cloud-init never created it and no client-side change will help.

How do I find the default username for my VPS image?

Read it from the machine itself with grep -n -A6 'default_user' /etc/cloud/cloud.cfg in a console session. The name: value under default_user is the account the image ships with. That setting lives under system_info, which user-data cannot override. Your provider's documentation page for the image states the same name, so use the two together rather than guessing from the distribution.

Does rebooting make cloud-init read my new user-data?

No. Most modules, including the one that creates accounts, run once per instance. Cloud-init drops a semaphore file into /var/lib/cloud/instance/sem/ when a module completes, then skips that module on every later boot. Editing user-data in the panel and rebooting produces a clean boot with no change and no error. Use sudo cloud-init clean --logs --reboot to clear that state and re-run everything, or create the account by hand.

Is it safe to run cloud-init clean on a running server?

On a server you have already configured, no. clean removes cloud-init's state under /var/lib/cloud, and with --reboot the next boot re-runs every module in your user-data, so package installs and scripts execute a second time. Adding --logs also deletes /var/log/cloud-init.log and /var/log/cloud-init-output.log, which are the files that would have explained the original failure. Read the logs first. On a box with real data, create the missing account with useradd instead.

The account exists but console login says Login incorrect. Why?

Cloud-init sets lock_passwd to true by default for accounts in the users: list, so the account has no usable password hash and every console password is rejected. Run sudo passwd -S devops from root: a second field of L means locked and NP means no password is set. Fix it with sudo passwd devops followed by sudo usermod -U devops, then confirm the field reads P. Password logins over SSH additionally need ssh_pwauth: true, since cloud images disable them.