Skip to content

Instantly share code, notes, and snippets.

@crappyrules
Last active January 31, 2026 21:43
Show Gist options
  • Select an option

  • Save crappyrules/a8b78942decd19d62c94426d430af8a0 to your computer and use it in GitHub Desktop.

Select an option

Save crappyrules/a8b78942decd19d62c94426d430af8a0 to your computer and use it in GitHub Desktop.
debian-unattended.sh
#upon first boot run sudo /root/upgrade-to-sid.sh if you want to upgrade your apt to the latest unstable version of Debian
#!/usr/bin/env bash
set -euo pipefail
OUTDIR="${1:-.}"
# Early check for xorriso
if ! command -v xorriso >/dev/null 2>&1; then
echo "ERROR: xorriso is not installed."
echo "Install it first, e.g.:"
echo " sudo apt install xorriso -y"
exit 1
fi
echo "=== Unattended Debian preseed/late.sh + ISO builder for VMs ==="
echo
echo "NOTE: You must have a valid Debian *netinst* ISO in the current directory."
echo " (e.g. debian-13.1.0-amd64-netinst.iso)"
# Find a default netinst ISO if possible
DEFAULT_ISO=""
shopt -s nullglob
for f in debian-*-netinst*.iso; do
DEFAULT_ISO="$f"
break
done
shopt -u nullglob
if [[ -n "${DEFAULT_ISO}" ]]; then
read -rp "Base Debian netinst ISO [${DEFAULT_ISO}]: " BASE_ISO
BASE_ISO="${BASE_ISO:-$DEFAULT_ISO}"
else
read -rp "Base Debian netinst ISO filename (must exist in this dir): " BASE_ISO
fi
if [[ ! -f "$BASE_ISO" ]]; then
echo "ERROR: ISO '$BASE_ISO' not found in current directory." >&2
exit 1
fi
read -rp "Name for the new unattended ISO [unattended-${BASE_ISO}]: " NEW_ISO
NEW_ISO="${NEW_ISO:-unattended-${BASE_ISO}}"
echo
read -rp "Full name for primary user: " FULLNAME
while :; do
read -rp "Username (e.g. crappy): " USERNAME
[[ -n "$USERNAME" ]] && break
echo "Username cannot be empty."
done
while :; do
read -rsp "Password for $USERNAME: " PASSWORD; echo
read -rsp "Confirm password: " PASSWORD2; echo
if [[ "$PASSWORD" == "$PASSWORD2" && -n "$PASSWORD" ]]; then
break
fi
echo "Passwords do not match or are empty. Try again."
done
while :; do
read -rp "Hostname for this VM: " HOSTNAME
[[ -n "$HOSTNAME" ]] && break
echo "Hostname cannot be empty."
done
echo
echo "Select the timezone for the installed system:"
echo " 1) America/New_York (US Eastern)"
echo " 2) America/Chicago (US Central)"
echo " 3) America/Denver (US Mountain)"
echo " 4) America/Los_Angeles (US Pacific)"
echo " 5) Europe/London"
echo " 6) Europe/Berlin"
echo " 7) Asia/Tokyo"
echo " 8) UTC"
echo " 9) Other (enter manually, e.g. America/Denver)"
read -rp "Choice [1-9, default 1]: " TZ_CHOICE
TZ_CHOICE="${TZ_CHOICE:-1}"
case "$TZ_CHOICE" in
1) TIMEZONE="America/New_York" ;;
2) TIMEZONE="America/Chicago" ;;
3) TIMEZONE="America/Denver" ;;
4) TIMEZONE="America/Los_Angeles" ;;
5) TIMEZONE="Europe/London" ;;
6) TIMEZONE="Europe/Berlin" ;;
7) TIMEZONE="Asia/Tokyo" ;;
8) TIMEZONE="Etc/UTC" ;;
9)
read -rp "Enter timezone (e.g. America/Denver, Europe/Paris): " TIMEZONE
TIMEZONE="${TIMEZONE:-Etc/UTC}"
;;
*) TIMEZONE="America/New_York" ;;
esac
echo
echo "Using timezone: $TIMEZONE"
echo
echo "Paste the SSH public key for $USERNAME (single line, e.g. ssh-ed25519 ...):"
read -r SSH_KEY
echo
echo "The next question is about the DISK *inside the VM* that Debian will install to."
echo "Examples:"
echo " - /dev/sda (common for ESXi/VMware)"
echo " - /dev/vda (virtio on KVM/QEMU)"
echo
echo "This is NOT your USB stick and has nothing to do with how the ISO is written."
read -rp "Target install disk device inside the VM [/dev/sda]: " DISK
DISK="${DISK:-/dev/sda}"
echo
read -rp "Any additional Debian packages to install (space-separated, optional): " EXTRA_PKGS
mkdir -p "$OUTDIR"
###############################################################################
# Generate preseed.cfg
###############################################################################
cat >"$OUTDIR/preseed.cfg" <<EOF
### Localization
d-i debian-installer/locale string en_US.UTF-8
d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/xkb-keymap select us
### Network
d-i netcfg/choose_interface select auto
d-i netcfg/disable_dhcp boolean false
d-i netcfg/get_hostname string $HOSTNAME
d-i netcfg/get_domain string localdomain
d-i netcfg/hostname string $HOSTNAME
d-i hw-detect/load_firmware boolean true
### Mirror
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i apt-setup/use_mirror boolean true
d-i apt-setup/contrib boolean true
d-i apt-setup/non-free boolean true
d-i apt-setup/non-free-firmware boolean true
d-i mirror/suite string stable
### Clock and time zone
d-i clock-setup/utc boolean true
d-i time/zone string $TIMEZONE
d-i clock-setup/ntp boolean true
### Users
d-i passwd/root-login boolean false
d-i passwd/make-user boolean true
d-i passwd/user-fullname string $FULLNAME
d-i passwd/username string $USERNAME
d-i passwd/user-password password $PASSWORD
d-i passwd/user-password-again password $PASSWORD
d-i user-setup/allow-password-weak boolean true
### Partitioning (single disk, / + swap)
d-i partman-auto/disk string $DISK
d-i partman-auto/method string regular
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-md/confirm boolean true
d-i partman-md/confirm_nooverwrite boolean true
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
### Base system
d-i base-installer/kernel/override-image string linux-image-amd64
### Package selection
tasksel tasksel/first multiselect standard, ssh-server
# Include wget so late.sh can fetch MOTD; sudo for passwordless sudo script
# EXTRA_PKGS comes from the interactive prompt and may be empty
d-i pkgsel/include string openssh-server wget sudo ${EXTRA_PKGS}
d-i pkgsel/upgrade select safe-upgrade
popularity-contest popularity-contest/participate boolean false
### Boot loader
d-i grub-installer/only_debian boolean true
d-i grub-installer/bootdev string $DISK
### Finishing the installation
d-i finish-install/reboot_in_progress note
### Late command: run /root/late.sh from the ISO
d-i preseed/late_command string cp /cdrom/late.sh /target/root/late.sh; in-target /bin/sh /root/late.sh
EOF
###############################################################################
# Generate late.sh
###############################################################################
cat >"$OUTDIR/late.sh" <<EOF
#!/bin/sh
set -e
# Set up SSH key-based auth for user $USERNAME
if id $USERNAME >/dev/null 2>&1; then
USER_HOME=\$(getent passwd $USERNAME | cut -d: -f6)
mkdir -p "\$USER_HOME/.ssh"
printf '%s\n' '$SSH_KEY' > "\$USER_HOME/.ssh/authorized_keys"
chown -R $USERNAME:$USERNAME "\$USER_HOME/.ssh"
chmod 700 "\$USER_HOME/.ssh"
chmod 600 "\$USER_HOME/.ssh/authorized_keys"
fi
# Disable SSH password authentication and root login
if [ -f /etc/ssh/sshd_config ]; then
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true
sed -i 's/^#PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config || true
sed -i 's/^PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config || true
fi
systemctl enable ssh || true
# Give user $USERNAME passwordless sudo
mkdir -p /etc/sudoers.d
printf '%s\n' '$USERNAME ALL=(ALL:ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$USERNAME-nopasswd
chmod 440 /etc/sudoers.d/90-$USERNAME-nopasswd
# Fetch MOTD from your gist (requires wget)
if command -v wget >/dev/null 2>&1; then
wget -qO /etc/motd 'https://gist.githubusercontent.com/crappyrules/ade9dc076f8611cec4332c5decf8e37e/raw/05bcf17bc2356257159703301c9056a68b415a20/motd' || true
fi
# Point APT to sid but don't auto-upgrade here
printf '%s\n' 'deb http://deb.debian.org/debian sid main contrib non-free-firmware' > /etc/apt/sources.list
# Create helper script to upgrade to sid, clean, and reboot
cat >/root/upgrade-to-sid.sh <<'EOS'
#!/bin/sh
set -e
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confnew dist-upgrade
apt-get -y autoremove --purge
apt-get -y autoclean
reboot
EOS
chmod +x /root/upgrade-to-sid.sh
EOF
chmod +x "$OUTDIR/late.sh"
echo "Generated: $OUTDIR/preseed.cfg and $OUTDIR/late.sh"
###############################################################################
# Build new ISO with xorriso
###############################################################################
echo
echo "Building new unattended ISO: $NEW_ISO"
xorriso -indev "$BASE_ISO" \
-outdev "$NEW_ISO" \
-map "$OUTDIR/preseed.cfg" /preseed.cfg \
-map "$OUTDIR/late.sh" /late.sh \
-boot_image any replay
echo "Created ISO: $NEW_ISO"
###############################################################################
# How to use the ISO
###############################################################################
echo
echo "How do you want to proceed with $NEW_ISO?"
echo " 1) Save ISO file only (default)"
echo " 2) Write ISO to a USB drive"
read -rp "Choice [1/2]: " CHOICE
CHOICE="${CHOICE:-1}"
if [[ "$CHOICE" != "2" ]]; then
echo "Leaving ISO as file only. Done."
exit 0
fi
# Option 2: write to USB
read -rp "USB device to OVERWRITE (e.g. /dev/sdb): " USB_DEV
if [[ -z "$USB_DEV" ]]; then
echo "No USB device specified; skipping write."
exit 0
fi
echo
echo "WARNING: This will ERASE ALL DATA on $USB_DEV."
read -rp "Type 'YES' to confirm: " CONFIRM
if [[ "$CONFIRM" != "YES" ]]; then
echo "Confirmation not given; skipping USB write."
exit 0
fi
echo "Writing $NEW_ISO to $USB_DEV ..."
sudo dd if="$NEW_ISO" of="$USB_DEV" bs=4M status=progress conv=fsync
sync
echo "Done writing ISO to $USB_DEV."
crappy@craptastic:~/Downloads/iso/debian
$ cat debian-unattended.sh
#!/usr/bin/env bash
set -euo pipefail
OUTDIR="${1:-.}"
# Early check for xorriso
if ! command -v xorriso >/dev/null 2>&1; then
echo "ERROR: xorriso is not installed."
echo "Install it first, e.g.:"
echo " sudo apt install xorriso -y"
exit 1
fi
echo "=== Unattended Debian preseed/late.sh + ISO builder for VMs ==="
echo
echo "NOTE: You must have a valid Debian *netinst* ISO in the current directory."
echo " (e.g. debian-13.1.0-amd64-netinst.iso)"
# Find a default netinst ISO if possible
DEFAULT_ISO=""
shopt -s nullglob
for f in debian-*-netinst*.iso; do
DEFAULT_ISO="$f"
break
done
shopt -u nullglob
if [[ -n "${DEFAULT_ISO}" ]]; then
read -rp "Base Debian netinst ISO [${DEFAULT_ISO}]: " BASE_ISO
BASE_ISO="${BASE_ISO:-$DEFAULT_ISO}"
else
read -rp "Base Debian netinst ISO filename (must exist in this dir): " BASE_ISO
fi
if [[ ! -f "$BASE_ISO" ]]; then
echo "ERROR: ISO '$BASE_ISO' not found in current directory." >&2
exit 1
fi
read -rp "Name for the new unattended ISO [unattended-${BASE_ISO}]: " NEW_ISO
NEW_ISO="${NEW_ISO:-unattended-${BASE_ISO}}"
echo
read -rp "Full name for primary user: " FULLNAME
while :; do
read -rp "Username (e.g. crappy): " USERNAME
[[ -n "$USERNAME" ]] && break
echo "Username cannot be empty."
done
while :; do
read -rsp "Password for $USERNAME: " PASSWORD; echo
read -rsp "Confirm password: " PASSWORD2; echo
if [[ "$PASSWORD" == "$PASSWORD2" && -n "$PASSWORD" ]]; then
break
fi
echo "Passwords do not match or are empty. Try again."
done
while :; do
read -rp "Hostname for this VM: " HOSTNAME
[[ -n "$HOSTNAME" ]] && break
echo "Hostname cannot be empty."
done
echo
echo "Select the timezone for the installed system:"
echo " 1) America/New_York (US Eastern)"
echo " 2) America/Chicago (US Central)"
echo " 3) America/Denver (US Mountain)"
echo " 4) America/Los_Angeles (US Pacific)"
echo " 5) Europe/London"
echo " 6) Europe/Berlin"
echo " 7) Asia/Tokyo"
echo " 8) UTC"
echo " 9) Other (enter manually, e.g. America/Denver)"
read -rp "Choice [1-9, default 1]: " TZ_CHOICE
TZ_CHOICE="${TZ_CHOICE:-1}"
case "$TZ_CHOICE" in
1) TIMEZONE="America/New_York" ;;
2) TIMEZONE="America/Chicago" ;;
3) TIMEZONE="America/Denver" ;;
4) TIMEZONE="America/Los_Angeles" ;;
5) TIMEZONE="Europe/London" ;;
6) TIMEZONE="Europe/Berlin" ;;
7) TIMEZONE="Asia/Tokyo" ;;
8) TIMEZONE="Etc/UTC" ;;
9)
read -rp "Enter timezone (e.g. America/Denver, Europe/Paris): " TIMEZONE
TIMEZONE="${TIMEZONE:-Etc/UTC}"
;;
*) TIMEZONE="America/New_York" ;;
esac
echo
echo "Using timezone: $TIMEZONE"
echo
echo "Paste the SSH public key for $USERNAME (single line, e.g. ssh-ed25519 ...):"
read -r SSH_KEY
echo
echo "The next question is about the DISK *inside the VM* that Debian will install to."
echo "Examples:"
echo " - /dev/sda (common for ESXi/VMware)"
echo " - /dev/vda (virtio on KVM/QEMU)"
echo
echo "This is NOT your USB stick and has nothing to do with how the ISO is written."
read -rp "Target install disk device inside the VM [/dev/sda]: " DISK
DISK="${DISK:-/dev/sda}"
echo
read -rp "Any additional Debian packages to install (space-separated, optional): " EXTRA_PKGS
mkdir -p "$OUTDIR"
###############################################################################
# Generate preseed.cfg
###############################################################################
cat >"$OUTDIR/preseed.cfg" <<EOF
### Localization
d-i debian-installer/locale string en_US.UTF-8
d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/xkb-keymap select us
### Network
d-i netcfg/choose_interface select auto
d-i netcfg/disable_dhcp boolean false
d-i netcfg/get_hostname string $HOSTNAME
d-i netcfg/get_domain string localdomain
d-i netcfg/hostname string $HOSTNAME
d-i hw-detect/load_firmware boolean true
### Mirror
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i apt-setup/use_mirror boolean true
d-i apt-setup/contrib boolean true
d-i apt-setup/non-free boolean true
d-i apt-setup/non-free-firmware boolean true
d-i mirror/suite string stable
### Clock and time zone
d-i clock-setup/utc boolean true
d-i time/zone string $TIMEZONE
d-i clock-setup/ntp boolean true
### Users
d-i passwd/root-login boolean false
d-i passwd/make-user boolean true
d-i passwd/user-fullname string $FULLNAME
d-i passwd/username string $USERNAME
d-i passwd/user-password password $PASSWORD
d-i passwd/user-password-again password $PASSWORD
d-i user-setup/allow-password-weak boolean true
### Partitioning (single disk, / + swap)
d-i partman-auto/disk string $DISK
d-i partman-auto/method string regular
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-md/confirm boolean true
d-i partman-md/confirm_nooverwrite boolean true
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
### Base system
d-i base-installer/kernel/override-image string linux-image-amd64
### Package selection
tasksel tasksel/first multiselect standard, ssh-server
# Include wget so late.sh can fetch MOTD; sudo for passwordless sudo script
# Also always install fail2ban for basic SSH hardening on servers
# EXTRA_PKGS comes from the interactive prompt and may be empty
d-i pkgsel/include string openssh-server wget sudo fail2ban ${EXTRA_PKGS}
d-i pkgsel/upgrade select safe-upgrade
popularity-contest popularity-contest/participate boolean false
### Boot loader
d-i grub-installer/only_debian boolean true
d-i grub-installer/bootdev string $DISK
### Finishing the installation
d-i finish-install/reboot_in_progress note
### Late command: run /root/late.sh from the ISO
d-i preseed/late_command string cp /cdrom/late.sh /target/root/late.sh; in-target /bin/sh /root/late.sh
EOF
###############################################################################
# Generate late.sh
###############################################################################
cat >"$OUTDIR/late.sh" <<EOF
#!/bin/sh
set -e
# Set up SSH key-based auth for user $USERNAME
if id $USERNAME >/dev/null 2>&1; then
USER_HOME=\$(getent passwd $USERNAME | cut -d: -f6)
mkdir -p "\$USER_HOME/.ssh"
printf '%s\n' '$SSH_KEY' > "\$USER_HOME/.ssh/authorized_keys"
chown -R $USERNAME:$USERNAME "\$USER_HOME/.ssh"
chmod 700 "\$USER_HOME/.ssh"
chmod 600 "\$USER_HOME/.ssh/authorized_keys"
fi
# Disable SSH password authentication and root login
if [ -f /etc/ssh/sshd_config ]; then
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true
sed -i 's/^#PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config || true
sed -i 's/^PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config || true
fi
systemctl enable ssh || true
# Give user $USERNAME passwordless sudo
mkdir -p /etc/sudoers.d
printf '%s\n' '$USERNAME ALL=(ALL:ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-$USERNAME-nopasswd
chmod 440 /etc/sudoers.d/90-$USERNAME-nopasswd
# Fetch MOTD from your gist (requires wget)
if command -v wget >/dev/null 2>&1; then
wget -qO /etc/motd 'https://gist.githubusercontent.com/crappyrules/ade9dc076f8611cec4332c5decf8e37e/raw/05bcf17bc2356257159703301c9056a68b415a20/motd' || true
fi
# Point APT to sid but don't auto-upgrade here
printf '%s\n' 'deb http://deb.debian.org/debian sid main contrib non-free-firmware' > /etc/apt/sources.list
# Create helper script to upgrade to sid, clean, and reboot
cat >/root/upgrade-to-sid.sh <<'EOS'
#!/bin/sh
set -e
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confnew dist-upgrade
apt-get -y autoremove --purge
apt-get -y autoclean
reboot
EOS
chmod +x /root/upgrade-to-sid.sh
EOF
chmod +x "$OUTDIR/late.sh"
echo "Generated: $OUTDIR/preseed.cfg and $OUTDIR/late.sh"
###############################################################################
# Build new ISO with xorriso
###############################################################################
echo
echo "Building new unattended ISO: $NEW_ISO"
xorriso -indev "$BASE_ISO" \
-outdev "$NEW_ISO" \
-map "$OUTDIR/preseed.cfg" /preseed.cfg \
-map "$OUTDIR/late.sh" /late.sh \
-boot_image any replay
echo "Created ISO: $NEW_ISO"
###############################################################################
# How to use the ISO
###############################################################################
echo
echo "How do you want to proceed with $NEW_ISO?"
echo " 1) Save ISO file only (default)"
echo " 2) Write ISO to a USB drive"
read -rp "Choice [1/2]: " CHOICE
CHOICE="${CHOICE:-1}"
if [[ "$CHOICE" != "2" ]]; then
echo "Leaving ISO as file only. Done."
exit 0
fi
# Option 2: write to USB
read -rp "USB device to OVERWRITE (e.g. /dev/sdb): " USB_DEV
if [[ -z "$USB_DEV" ]]; then
echo "No USB device specified; skipping write."
exit 0
fi
echo
echo "WARNING: This will ERASE ALL DATA on $USB_DEV."
read -rp "Type 'YES' to confirm: " CONFIRM
if [[ "$CONFIRM" != "YES" ]]; then
echo "Confirmation not given; skipping USB write."
exit 0
fi
echo "Writing $NEW_ISO to $USB_DEV ..."
sudo dd if="$NEW_ISO" of="$USB_DEV" bs=4M status=progress conv=fsync
sync
echo "Done writing ISO to $USB_DEV."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment