Skip to content

Instantly share code, notes, and snippets.

@w3cj
Last active January 5, 2026 19:51
Show Gist options
  • Select an option

  • Save w3cj/cdd447b1a10ce741e4ee968fa6b75553 to your computer and use it in GitHub Desktop.

Select an option

Save w3cj/cdd447b1a10ce741e4ee968fa6b75553 to your computer and use it in GitHub Desktop.
# This config was written for Ubuntu 22.04
# If you are using a more recent version, see the comments of this gist for fixes
#cloud-config
users:
- name: cj
ssh_authorized_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINBlfqermlV44zAU+iTCa5im5O0QWXid6sHqh2Z4L1Cm cj@null.computer"
sudo: ALL=(ALL:ALL) ALL
groups: sudo
shell: /bin/bash
chpasswd:
expire: true
users:
- name: cj
password: changeme
type: text
runcmd:
- sed -i '/PermitRootLogin/d' /etc/ssh/sshd_config
- echo "PermitRootLogin without-password" >> /etc/ssh/sshd_config
- sed -i '/PubkeyAuthentication/d' /etc/ssh/sshd_config
- echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
- sed -i '/PasswordAuthentication/d' /etc/ssh/sshd_config
- echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
- systemctl restart sshd
- echo "\$nrconf{kernelhints} = -1;" > /etc/needrestart/conf.d/99disable-prompt.conf
- apt update
- apt upgrade -y --allow-downgrades --allow-remove-essential --allow-change-held-packages
- reboot
@ahamedzoha
Copy link

Facing an issue where I tried to use the cloud config. I have triple checked to see if my public keys are correct but after the server spins up, i get
ssh root@[xxx.xxx.xxx.xxx]
ssh: connect to host [xxx.xxx.xxx.xxx]: port 22: Connection refused.

If I restart the server, I get
root@[xxx.xxxx.xxx.xxx]: Permission denied (publickey).

FYI: I have attached my ssh keys during server creation

@swrrvr
Copy link

swrrvr commented Jul 12, 2024

Facing an issue where I tried to use the cloud config. I have triple checked to see if my public keys are correct but after the server spins up, i get ssh root@[xxx.xxx.xxx.xxx] ssh: connect to host [xxx.xxx.xxx.xxx]: port 22: Connection refused.

I have also experienced this error. In my testing, it appears as if the issue lies with the command systemctl restart sshd as the SSH service name varies from system to system between ssh and sshd. This can be checked using the command sudo systemctl list-units --type=service | grep ssh.

My solution is to edit line 22 in the clout-init.yml file to systemctl restart ssh || systemctl restart sshd in an attempt to target both SSH service names.

@adrnd
Copy link

adrnd commented Jul 13, 2024

If I restart the server, I get root@[xxx.xxxx.xxx.xxx]: Permission denied (publickey).

Had the same issue as @ahamedzoha .What I did was edit line 22 as @irvdude said to " - systemctl restart ssh" because it looked like in an earlier run the service on my system wasn't called sshd. Ultimately I don't know if that has changed anything.
Because after some troubleshooting the issue was I had to start the ssh login with:
ssh -o "IdentitiesOnly=yes" -i privatekeyfilename root@serveripaddress
privatekeyfilename being the name you gave when creating the ssh key, assuming you're following the coolify guide as well.
Edit: This is also assuming you're running the ssh command in the folder where the privatekey file is located. Otherwise you might need to specificy the path to it as well.

@TomRadford
Copy link

My solution is to edit line 22 in the clout-init.yml file to systemctl restart ssh || systemctl restart sshd in an attempt to target both SSH service names.

Ubuntu 24.04 has removed the d alias for various systemd services (https://www.reddit.com/r/Ubuntu/comments/1cl5qiq/systemctl_restart_sshd_does_not_work_any_more_in/). Since CJ's example used 22.04, its most likely that this is one the reasons that, potentially, we're having some issues when spinning up new 24.04 VM's with this cloud init that was intended for 22.04 :)

@swrrvr
Copy link

swrrvr commented Jul 14, 2024

My solution is to edit line 22 in the clout-init.yml file to systemctl restart ssh || systemctl restart sshd in an attempt to target both SSH service names.

I also left out one small (huge) detail. On line 21 of cloud-init.yml, I also happened to have changed "PasswordAuthentication no" to "PasswordAuthentication yes".

Thus, conveniently disabling PasswordAuthentication, enabling access to the server (my mistake).

One could simply pipe echo "PasswordAuthentication no" >> /etc/ssh/sshd_config back into the session once connected, then wait for the system to reboot and then ssh back in. (unless you are plan adding a new server through Coolify I'd highly recommend doing that well after your new server is configured.)

I'm also testing @adrnd method to ssh into the session using ssh -o "IdentitiesOnly=yes" -i ~/.ssh/id_ed25519 root@serveripaddress, will follow up here.

@joemaffei
Copy link

The server kept asking me for my password. @adrnd's solution worked for me.

@legout
Copy link

legout commented Aug 2, 2024

If I restart the server, I get root@[xxx.xxxx.xxx.xxx]: Permission denied (publickey).

Did you set the permissions right?

chmod -R 644 ~/.ssh/your_key.pub
chmod -R 600 ~/.ssh/authorized_keys

@Jensssen
Copy link

An alternative to @adrnd solution would be to create an ssh config entry like the following:

Host my_awesome_server
 HostName xxx.xxx.xxx.xxx
 User <YOUR_USER_NAME_SPECIFIED_IN_CLOUD_CONFIG>
 Port 22
 IdentityFile ~/.ssh/<PRIVATE_KEY_FILE>

And then run ssh my_awesome_server. This should enforce a user login on the given IP to use your public/private key for authentication. Make sure to also follow @swrrvr suggestion to add systemctl restart ssh || systemctl restart sshd to the cloud config. in line 22

@dziamid
Copy link

dziamid commented Sep 19, 2024

Here's an updated version for ubuntu 24.04: https://gist.github.com/dziamid/0de2761e0ecc4b3e68e2461c60f82930

@runejac
Copy link

runejac commented Oct 9, 2024

I am struggling with getting a new public key stored in known_hosts locally when trying to run ssh root@{ip-adress} rather than the public key I set in cloud.init script. Using @dziamid new file for Ubuntu 24.04, also added the systemctl restart ssh || systemctl restart sshd, does anyone know why this happens?

@ConspiredMinds
Copy link

Here's an updated version for ubuntu 24.04: https://gist.github.com/dziamid/0de2761e0ecc4b3e68e2461c60f82930

+1

Thanks!

@dwatek
Copy link

dwatek commented Nov 21, 2024

@dziamid I still can't get in to log in to the username I create with the password 'changeme' it says Permission denied, please try again. Is this config sure to be correct?

@SebastianArce
Copy link

SebastianArce commented Dec 1, 2024

Hey @dwatek , I'm facing the same issue. Did you have any luck?

@dwatek
Copy link

dwatek commented Dec 2, 2024

@SebastianArce remember to also add #cloud-config at top of file. I thought it was just a comment, but without it the whole config didn't work

@SebastianArce
Copy link

@dwatek yes, that was it. Thanks!

@shanneykar
Copy link

Can someone explain Disable needrestart prompts ? Should we enable again at the end?
Can we use -apt full-upgrade -y as - apt upgrade -y --allow-downgrades --allow-remove-essential --allow-change-held-packages seems risky?

@dsfaccini
Copy link

Can someone explain Disable needrestart prompts ? Should we enable again at the end? Can we use -apt full-upgrade -y as - apt upgrade -y --allow-downgrades --allow-remove-essential --allow-change-held-packages seems risky?

disbaling needrestart is perfect for the cloud-init automation, we reboot at the end anyway, so there’s no need to re-enable it here
prompts would only matter for later manual updates, and you can remove this file post-setup if you want them back

as for the apt upgrade yeah, unless cj was fixing some specific issue requiring downgrades or removals, I agree apt full-upgrade -y is a safer bet

@arthureberledev
Copy link

i kept getting the error root@[xxx.xxxx.xxx.xxx]: Permission denied (publickey) error in the newest version till i found out that coolify somehow added a key (something like <key>/x coolify) to the authorized_keys key file but didnt write it to a new line, so it was appended after the already existing key. Moving the key to a new line fixed the issue for me

@zfbx
Copy link

zfbx commented Apr 7, 2025

@SebastianArce remember to also add #cloud-config at top of file. I thought it was just a comment, but without it the whole config didn't work

Thank you x_x that is so dumb I've been struggling for a while because of that simple thing

@florianmartens
Copy link

florianmartens commented Apr 25, 2025

Hm, not of the provided fixes worked for me :(

Here's a version that worked for me (it has some bigger changes compared to the original):

#cloud-config
users:
  - name: yourname
    ssh_authorized_keys:
      - "<SSH_KEY>"
    sudo: ALL=(ALL:ALL) ALL
    groups: sudo
    shell: /bin/bash
chpasswd:
  expire: true
  users:
    - name: yourname
      password: changeme
      type: text
write_files:
  - path: /etc/ssh/sshd_config.d/99-custom.conf
    content: |
      PermitRootLogin without-password
      PubkeyAuthentication yes
      PasswordAuthentication no
runcmd:
  - systemctl restart sshd
  - echo "\$nrconf{kernelhints} = -1;" > /etc/needrestart/conf.d/99disable-prompt.conf
  - apt update
  - apt upgrade -y --allow-downgrades --allow-remove-essential --allow-change-held-packages
  - reboot

@vlad1mirJ
Copy link

I have also encountered issues with running cloud init on Hetzner Ubuntu 24.04 ARM VPS. Unfortunately, sshd was not aliased so I had to use systemclt restart ssh instead.

Here is a config that ended up working for me

#cloud-config
users:
  - name: <username>
    ssh_authorized_keys:
      - <pub_ssh_key>
    sudo: ALL=(ALL:ALL) ALL
    groups: sudo
    shell: /bin/bash
chpasswd:
  expire: true
  users:
    - name: <username>
      password: changeme
      type: text
package_update: true
package_upgrade: true
runcmd:
  - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin without-password/' /etc/ssh/sshd_config
  - sed -i -e '/^\(#\|\)PubkeyAuthentication/s/^.*$/PubkeyAuthentication yes/' /etc/ssh/sshd_config
  - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
  - systemctl restart ssh
  - echo "\$nrconf{kernelhints} = -1;" > /etc/needrestart/conf.d/99disable-prompt.conf
power_state:
  delay: 1
  timeout: 60
  mode: reboot
  message: Rebooting after cloud init

@zeropaper
Copy link

@vlad1mirJ your solution worked like a charm for me (Ubuntu 24.04). Thanks!

@stumbata
Copy link

@vlad1mirJ on Hetzner Ubuntu 24.04, if you add ssh key, root user don't have a password, so we don't need that extra config I think

@ahsan-abrar
Copy link

ahsan-abrar commented Dec 27, 2025

I created this script with the help of AI

This script hardens and automates your server setup in 3 quick steps:

  • Hardens Access: Creates a new user, moves SSH to new port, and bans passwords/root login (keys only).
  • Locks Firewall: Blocks all traffic except your SSH port and Cloudflare IPs (hides your server from the public).
  • Deploys Coolify: Installs the app, injects automation keys, and patches the database to use your custom settings.

I am also using Cloudflare DNS and origin certificates by following this video: https://www.youtube.com/watch?v=HZFNt-Grw0U

Note: I am not an expert in Linux and self-hosting, please guide me if I am doing anything wrong, I will correct it.

#!/bin/bash
set -e

# ================= CONFIG =================
REMOTE_IP="23.92.65.231"
USERNAME="tolres2"
NEW_SSH_PORT="2311"
LOCAL_KEY_DIR="$HOME/Documents/coolify-new-imp/$USERNAME"
# =========================================

mkdir -p "$LOCAL_KEY_DIR"

# Generate Keys
[ ! -f "$LOCAL_KEY_DIR/id_ed25519_personal" ] && ssh-keygen -t ed25519 -f "$LOCAL_KEY_DIR/id_ed25519_personal" -N "" -C "$USERNAME-personal"
[ ! -f "$LOCAL_KEY_DIR/id_ed25519_coolify" ] && ssh-keygen -t ed25519 -f "$LOCAL_KEY_DIR/id_ed25519_coolify" -N "" -C "coolify-automation"

PERSONAL_PUB=$(cat "$LOCAL_KEY_DIR/id_ed25519_personal.pub")
COOLIFY_PUB=$(cat "$LOCAL_KEY_DIR/id_ed25519_coolify.pub")
COOLIFY_PRIV_DATA=$(cat "$LOCAL_KEY_DIR/id_ed25519_coolify")

echo "πŸš€ Starting Deployment on $REMOTE_IP..."

ssh root@$REMOTE_IP "PERSONAL_PUB='$PERSONAL_PUB' COOLIFY_PUB='$COOLIFY_PUB' COOLIFY_PRIV='$COOLIFY_PRIV_DATA' USERNAME='$USERNAME' NEW_SSH_PORT='$NEW_SSH_PORT'" 'bash -s' <<'EOF'
set -e

# 1. USER SETUP
if ! id -u "$USERNAME" >/dev/null 2>&1; then useradd -m -s /bin/bash "$USERNAME"; fi
usermod -aG sudo "$USERNAME"
echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USERNAME
mkdir -p /home/$USERNAME/.ssh
echo "$PERSONAL_PUB" > /home/$USERNAME/.ssh/authorized_keys
echo "$COOLIFY_PUB" >> /home/$USERNAME/.ssh/authorized_keys
chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh
chmod 700 /home/$USERNAME/.ssh
chmod 600 /home/$USERNAME/.ssh/authorized_keys

# 2. SSH HARDENING (Clean & Safe)
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
# Remove any existing Port or AllowUsers lines to prevent duplicates
sed -i "/^Port /d" /etc/ssh/sshd_config
sed -i "/^AllowUsers /d" /etc/ssh/sshd_config
sed -i "/^PermitRootLogin /d" /etc/ssh/sshd_config

echo "Port $NEW_SSH_PORT" >> /etc/ssh/sshd_config
echo "AllowUsers $USERNAME" >> /etc/ssh/sshd_config
echo "PermitRootLogin no" >> /etc/ssh/sshd_config
echo "PasswordAuthentication no" >> /etc/ssh/sshd_config

# 3. FIREWALL (Including Cloudflare)
apt-get update && apt-get install -y ufw curl
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow $NEW_SSH_PORT/tcp
ufw allow 22/tcp # EMERGENCY BACKUP PORT - Close this manually later!
ufw allow from 172.17.0.0/16 to any # Allow Docker Bridge

echo "Adding Cloudflare IPs..."
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do ufw allow from "$ip" to any port 80,443 proto tcp; done
for ip in $(curl -s https://www.cloudflare.com/ips-v6); do ufw allow from "$ip" to any port 80,443 proto tcp; done

ufw --force enable

# 4. INSTALL COOLIFY
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

# 5. RESTART SSH
sshd -t
systemctl restart ssh

# -------- 6. FIX PERMISSIONS & KEY INJECTION --------
echo "πŸ”§ Applying final permission fixes..."

# 1. Add your user to the root group FIRST
# This ensures that once we set '750' (Group access), you are already in that group.
usermod -aG root "$USERNAME"

# 2. Set the internal Coolify user (9999) as the owner
# and 'root' as the group for the entire directory.
chown -R 9999:root /data/coolify

# 3. Create/Verify the SSH directory
mkdir -p /data/coolify/ssh/keys

# 4. Inject the Automation Key
echo "$COOLIFY_PRIV" > /data/coolify/ssh/keys/id.root@host.docker.internal

# 5. Apply the 750 Permission (Owner: Full, Group: Enter/Read, Others: None)
# This allows you (as part of the root group) to 'cd' into these folders.
chmod 750 /data/coolify
chmod 750 /data/coolify/ssh
chmod 750 /data/coolify/proxy

# 6. Critical Security: Keep the private key locked to the owner only
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
chown 9999:9999 /data/coolify/ssh/keys/id.root@host.docker.internal

echo "βœ… Permissions updated. Group 'root' now has access to /data/coolify."

# 7. DATABASE PATCH (WAITING FOR DB)
echo "⏳ Patching Coolify DB (User: $USERNAME, Port: $NEW_SSH_PORT)..."
for i in {1..60}; do
  DB=$(docker ps --filter name=coolify-db --format "{{.Names}}")
  if [ -n "$DB" ] && docker exec $DB pg_isready -U coolify >/dev/null 2>&1; then
    docker exec $DB psql -U coolify -d coolify -c "UPDATE servers SET \"user\"='$USERNAME', port=$NEW_SSH_PORT WHERE name='localhost';"
    echo "βœ… DB Patched."
    break
  fi
  [ $i -eq 60 ] && echo "❌ DB Patch timed out. You must update the server settings in Coolify UI."
  sleep 5
done

EOF

echo "-------------------------------------------------------"
echo "βœ… SETUP FINISHED"
echo "1. Attempt login: ssh -p $NEW_SSH_PORT $USERNAME@$REMOTE_IP -i $LOCAL_KEY_DIR/id_ed25519_personal"
echo "2. If successful, run 'sudo ufw delete allow 22/tcp' on the server."
echo "-------------------------------------------------------"

@Lenostatos
Copy link

Lenostatos commented Jan 5, 2026

I dug way too deep into cloud-init πŸ˜… and used the following result in the end:

#cloud-config

# Keyboard settings
keyboard:
  layout: de # might be useful if you have a non-english keyboard layout

# System timezone
# (might help, e.g. with correctly timing Coolify updates)
timezone: Europe/Berlin

# SSH config
disable_root: false # coolify needs the root user SSH login
allow_public_ssh_keys: true
ssh_genkeytypes: [ed25519] # modify this, if you use other SSH key types

# User config
user:
  name: lenos
  sudo: ALL=(ALL:ALL) ALL # google "sudoers" file or syntax to understand this
  groups: sudo
  shell: /bin/bash
  ssh_authorized_keys:
    - ssh-ed25519 AAAAC3NzaC1l...

users:
  - name: root
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1l...

# Password setup

# disable password authentication, allow only SSH key authentication
ssh_pwauth: false

# set expired passwords that have to be reset on first login
chpasswd:
  expire: true
  users:
    - name: root # will only need root pw for rescue operation via vps provider terminal
      password: changeme
      type: text
    - name: lenos
      password: changeme
      type: text

# Extra SSH security settings
# Source: https://community.hetzner.com/tutorials/basic-cloud-config
# sshd config docs at: https://man.openbsd.org/sshd_config
# Sidenote: The docs say the following on AllowAgentForwarding: "Note that disabling agent forwarding does not improve security unless users are also denied shell access, as they can always install their own forwarders."
write_files:
  - path: /etc/ssh/sshd_config.d/ssh-hardening.conf
    content: |
      Port 2222
      AllowUsers root lenos
      PermitRootLogin prohibit-password
      KbdInteractiveAuthentication no
      ChallengeResponseAuthentication no
      MaxAuthTries 3
      AllowTcpForwarding yes
      X11Forwarding no
      AllowAgentForwarding no
      AuthorizedKeysFile .ssh/authorized_keys

# Package management
packages:
  - fail2ban
  - ufw # uncomplicated firewall | Caveat (!): Coolify docs mention that ufw doesn't work with Docker applications https://coolify.io/docs/knowledge-base/server/firewall#closing-ports-using-a-firewall
package_update: true
package_upgrade: true
package_reboot_if_required: true

# Commands to run at the end of the cloud-init process
runcmd:
  - printf "[sshd]\nenabled = true\nport = 2222\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
  - systemctl enable fail2ban
  - ufw allow 2222 # randomly selected SSH port with https://monocalc.com/tool/networking/random_port_generator
  - ufw allow 80 # SSL certificate generation via reverse proxy
  - ufw allow 443 # HTTPS
  - ufw allow 8000 # HTTP access to the Coolify dashboard (close this after setting up custom domain)
  - ufw allow 6001 # Real-time communications (close this after setting up custom domain)
  - ufw allow 6002 # Terminal access (close this after setting up custom domain)
  - ufw enable
  - echo "\$nrconf{kernelhints} = -1;" > /etc/needrestart/conf.d/99disable-prompt.conf # <- Claude 4.5 says: "Creates a config file that prevents `needrestart` from prompting about kernel/service restarts during package updates"
  - apt update
  - apt -y --with-new-pkgs upgrade
  - reboot # ! without this, the SSH service won't be properly configured
##############################
# TODO-list after first login:
# - cloud-init status --wait
# - check if reboot is required with `ls /var/run/reboot-required`
# - change user pw
# - change root pw by switching to root user and executing `passwd`

# Simulate machine for cloud-init to run on: https://cloudinit.readthedocs.io/en/latest/tutorial/qemu.html

# Python server serving cloud-init config files is started in those files' folder with:
# `python3 -m http.server --directory .`

# Custom startup command from folder containing cloud image:
# qemu-system-x86_64                                            \
# -net nic                                                    \
# -net user,hostfwd=tcp::2222-:2222                          \
# -machine accel=kvm:tcg                                      \
# -m 512                                                      \
# -smp 4                                                      \
# -nographic                                                  \
# -hda noble-server-cloudimg-amd64.img                        \
# -smbios type=1,serial=ds='nocloud;s=http://10.0.2.2:8000/'

# Comments:
# -net user,hostfwd=tcp::2222-:2222 -> forwarding your custom SSH port. Change the right port number on this line to your own custom SSH port number if you use one
# -m 512 -> allocate 512 MB of system memory to qemu VM
# -smp 4                                                      \ # allocate 4 CPU cores to qemu VM
# -smbios type=1,serial=ds='nocloud;s=http://10.0.2.2:8000/'  \ # last part is address of server serving user-data file

# Validate cloud-init.yaml: https://cloudinit.readthedocs.io/en/latest/howto/debug_user_data.html
# (`cloud-init schema -c path/to/your/user-data/file --annotate`)
# Rerun local cloud-init tests: https://cloudinit.readthedocs.io/en/latest/howto/rerun_cloud_init.html#

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment