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
@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